Coverage for larch/xmlrpc_server.py: 0%
264 statements
« prev ^ index » next coverage.py v7.6.0, created at 2024-10-16 21:04 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2024-10-16 21:04 +0000
1#!/usr/bin/env python
2"""
3XML RPC
4"""
5from __future__ import print_function
7import os
8import sys
9from time import time, sleep, ctime
10import signal
11import socket
12from pathlib import Path
13from subprocess import Popen
14from threading import Thread
15from argparse import ArgumentParser, RawDescriptionHelpFormatter
17from xmlrpc.server import SimpleXMLRPCServer
18from xmlrpc.client import ServerProxy
20from .interpreter import Interpreter
21from .utils import uname, get_cwd
22from .utils.jsonutils import encode4js
24try:
25 import psutil
26 HAS_PSUTIL = True
27except ImportError:
28 HAS_PSUTIL = False
30NOT_IN_USE, CONNECTED, NOT_LARCHSERVER = range(3)
31POLL_TIME = 0.50
33"""Notes:
34 0. test server with HOST/PORT, report status (CREATED, ALREADY_RUNNING, FAILED).
35 1. prompt to kill a running server on HOST/PORT, preferably giving a
36 'last used by {APPNAME} with {PROCESS_ID} at {DATETIME}'
37 2. launch server on next unused PORT on HOST, increment by 1 to 100, report status.
38 3. connect to running server on HOST/PORT.
39 4. have each client set a keepalive time (that is,
40 'die after having no activity for X seconds') for each server (default=3*24*3600.0).
41"""
43def test_server(host='localhost', port=4966):
44 """Test for a Larch server on host and port
46 Arguments
47 host (str): host name ['localhost']
48 port (int): port number [4966]
50 Returns
51 integer status number:
52 0 Not in use.
53 1 Connected, valid Larch server
54 2 In use, but not a valid Larch server
55 """
56 server = ServerProxy(f'http://{host:s}:{port:d}')
57 try:
58 methods = server.system.listMethods()
59 except socket.error:
60 return NOT_IN_USE
62 # verify that this is a valid larch server
63 if len(methods) < 5 or 'larch' not in methods:
64 return NOT_LARCHSERVER
65 ret = ''
66 try:
67 ret = server.get_rawdata('_sys.config.user_larchdir')
68 except:
69 return NOT_LARCHSERVER
70 if len(ret) < 1:
71 return NOT_LARCHSERVER
73 return CONNECTED
76def get_next_port(host='localhost', port=4966, nmax=100):
77 """Return next available port for a Larch server on host
79 Arguments
80 host (str): host name ['localhost']
81 port (int): starting port number [4966]
82 nmax (int): maximum number to try [100]
84 Returns
85 integer: next unused port number or None in nmax exceeded.
86 """
87 # special case for localhost:
88 # use psutil to find next unused port
89 if host.lower() == 'localhost':
90 if HAS_PSUTIL and uname == 'win':
91 available = [True]*nmax
92 try:
93 conns = psutil.net_connections()
94 except:
95 conns = []
96 if len(conns) > 0:
97 for conn in conns:
98 ptest = conn.laddr[1] - port
99 if ptest >= 0 and ptest < nmax:
100 available[ptest] = False
101 for index, status in enumerate(available):
102 if status:
103 return port+index
104 # now test with brute attempt to open the socket:
105 for index in range(nmax):
106 ptest = port + index
107 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
108 success = False
109 try:
110 sock.bind(('', ptest))
111 success = True
112 except socket.error:
113 pass
114 finally:
115 sock.close()
116 if success:
117 return ptest
119 # for remote servers or if the above did not work, need to test ports
120 for index in range(nmax):
121 ptest = port + index
122 if NOT_IN_USE == test_server(host=host, port=ptest):
123 return ptest
124 return None
126class LarchServer(SimpleXMLRPCServer):
127 "xml-rpc server"
128 def __init__(self, host='localhost', port=4966,
129 logRequests=False, allow_none=True,
130 keepalive_time=3*24*3600):
131 self.out_buffer = []
133 self.larch = Interpreter(writer=self)
134 self.larch.input.prompt = ''
135 self.larch.input.prompt2 = ''
136 self.larch.run_init_scripts()
138 self.larch('_sys.client = group(keepalive_time=%f)' % keepalive_time)
139 self.larch('_sys.wx = group(wxapp=None)')
140 _sys = self.larch.symtable._sys
141 _sys.color_exceptions = False
142 _sys.client.last_event = int(time())
143 _sys.client.pid_server = int(os.getpid())
144 _sys.client.app = 'unknown'
145 _sys.client.pid = 0
146 _sys.client.user = 'unknown'
147 _sys.client.machine = socket.getfqdn()
149 self.client = self.larch.symtable._sys.client
150 self.port = port
151 SimpleXMLRPCServer.__init__(self, (host, port),
152 logRequests=logRequests,
153 allow_none=allow_none)
155 self.register_introspection_functions()
156 self.register_function(self.larch_exec, 'larch')
158 for method in ('ls', 'chdir', 'cd', 'cwd', 'shutdown',
159 'set_keepalive_time', 'set_client_info',
160 'get_client_info', 'get_data', 'get_rawdata',
161 'get_messages', 'len_messages'):
162 self.register_function(getattr(self, method), method)
164 # sys.stdout = self
165 self.finished = False
166 signal.signal(signal.SIGINT, self.signal_handler)
167 self.activity_thread = Thread(target=self.check_activity)
169 def write(self, text):
170 if text is None:
171 text = ''
172 self.out_buffer.append(str(text))
174 def flush(self):
175 pass
177 def set_keepalive_time(self, keepalive_time):
178 """set keepalive time
179 the server will self destruct after keepalive_time of inactivity
181 Arguments:
182 keepalive_time (number): time in seconds
184 """
185 self.larch("_sys.client.keepalive_time = %f" % keepalive_time)
187 def set_client_info(self, key, value):
188 """set client info
190 Arguments:
191 key (str): category
192 value (str): value to use
194 Notes:
195 the key can actually be any string but include by convention:
196 app application name
197 user user name
198 machine machine name
199 pid process id
200 """
201 self.larch("_sys.client.%s = '%s'" % (key, value))
203 def get_client_info(self):
204 """get client info:
205 returns json dictionary of client information
206 """
207 out = {'port': self.port}
208 client = self.larch.symtable._sys.client
209 for attr in dir(client):
210 out[attr] = getattr(client, attr)
211 return encode4js(out)
213 def get_messages(self):
214 """get (and clear) all output messages (say, from "print()")
215 """
216 out = "".join(self.out_buffer)
217 self.out_buffer = []
218 return out
220 def len_messages(self):
221 "length of message buffer"
222 return len(self.out_buffer)
224 def ls(self, dir_name):
225 """list contents of a directory: """
226 return os.listdir(dir_name)
228 def chdir(self, dir_name):
229 """change directory"""
230 return os.chdir(dir_name)
232 def cd(self, dir_name):
233 """change directory"""
234 return os.chdir(dir_name)
236 def cwd(self):
237 """change directory"""
238 ret = get_cwd()
239 if uname == 'win':
240 ret = ret.replace('\\','/')
241 return ret
243 def signal_handler(self, sig=0, frame=None):
244 self.kill()
246 def kill(self):
247 """handle alarm signal, generated by signal.alarm(t)"""
248 sleep(POLL_TIME)
249 self.shutdown()
250 self.server_close()
252 def shutdown(self):
253 "shutdown LarchServer"
254 self.finished = True
255 if self.activity_thread.is_alive():
256 self.activity_thread.join(POLL_TIME)
257 return 1
259 def check_activity(self):
260 while not self.finished:
261 sleep(POLL_TIME)
262 # print("Tick ", time()- (self.client.keepalive_time + self.client.last_event))
263 if time() > (self.client.keepalive_time + self.client.last_event):
264 t = Thread(target=self.kill)
265 t.start()
266 break
268 def larch_exec(self, text):
269 "execute larch command"
270 text = text.strip()
271 if text in ('quit', 'exit', 'EOF'):
272 self.shutdown()
273 else:
274 ret = self.larch.eval(text, lineno=0)
275 if ret is not None:
276 self.write(repr(ret))
277 self.client.last_event = time()
278 self.flush()
279 return 1
281 def get_rawdata(self, expr):
282 "return non-json encoded data for a larch expression"
283 return self.larch.eval(expr)
285 def get_data(self, expr):
286 "return json encoded data for a larch expression"
287 self.larch('_sys.client.last_event = %i' % time())
288 return encode4js(self.larch.eval(expr))
290 def run(self):
291 """run server until times out"""
292 self.activity_thread.start()
293 while not self.finished:
294 try:
295 self.handle_request()
296 except:
297 break
299def spawn_server(port=4966, wait=True, timeout=30):
300 """
301 start a new process for a LarchServer on selected port,
302 optionally waiting to confirm connection
303 """
304 topdir = sys.exec_prefix
305 pyexe = Path(topdir, 'bin', 'python3').as_posix()
306 bindir = 'bin'
307 if uname.startswith('win'):
308 bindir = 'Scripts'
309 pyexe = pyexe + '.exe'
311 args = [pyexe, Path(topdir, bindir, 'larch').as_posix(),
312 '-r', '-p', '%d' % port]
313 pipe = Popen(args)
314 if wait:
315 time0 = time()
316 while time() - time0 < timeout:
317 sleep(POLL_TIME)
318 if CONNECTED == test_server(port=port):
319 break
320 return pipe
323###
324def larch_server_cli():
325 """command-line program to control larch XMLRPC server"""
326 command_desc = """
327command must be one of the following:
328 start start server on specified port
329 stop stop server on specified port
330 restart restart server on specified port
331 next start server on next avaialable port (see also '-n' option)
332 status print a short status message: whether server< is running on port
333 report print a multi-line status report
334"""
336 parser = ArgumentParser(description='run larch XML-RPC server',
337 formatter_class=RawDescriptionHelpFormatter,
338 epilog=command_desc)
340 parser.add_argument("-p", "--port", dest="port", default='4966',
341 help="port number for remote server [4966]")
343 parser.add_argument("-n", "--next", dest="next", action="store_true",
344 default=False,
345 help="show next available port, but do not start [False]")
347 parser.add_argument("-q", "--quiet", dest="quiet", action="store_true",
348 default=False, help="suppress messaages [False]")
350 parser.add_argument("command", nargs='?', help="server command ['status']")
352 args = parser.parse_args()
355 port = int(args.port)
356 command = args.command or 'status'
357 command = command.lower()
359 def smsg(port, txt):
360 if not args.quiet:
361 print('larch_server port=%i: %s' % (port, txt))
364 if args.next:
365 port = get_next_port(port=port)
366 print(port)
367 sys.exit(0)
369 server_state = test_server(port=port)
371 if command == 'start':
372 if server_state == CONNECTED:
373 smsg(port, 'already running')
374 elif server_state == NOT_IN_USE:
375 spawn_server(port=port)
376 smsg(port, 'started')
377 else:
378 smsg(port, 'port is in use, cannot start')
380 elif command == 'stop':
381 if server_state == CONNECTED:
382 ServerProxy(f'http://localhost:{port:d}').shutdown()
383 smsg(port, 'stopped')
385 elif command == 'next':
386 port = get_next_port(port=port)
387 spawn_server(port=port)
388 smsg(port, 'started')
390 elif command == 'restart':
391 if server_state == CONNECTED:
392 ServerProxy(f'http://localhost:{port:d}').shutdown()
393 sleep(POLL_TIME)
394 spawn_server(port=port)
396 elif command == 'status':
397 if server_state == CONNECTED:
398 smsg(port, 'running')
399 sys.exit(0)
400 elif server_state == NOT_IN_USE:
401 smsg(port, 'not running')
402 sys.exit(1)
403 else:
404 smsg(port, 'port is in use by non-larch server')
405 elif command == 'report':
406 if server_state == CONNECTED:
407 s = ServerProxy(f'http://localhost:{port:d}')
408 info = s.get_client_info()
409 last_event = info.get('last_event', 0)
410 last_used = ctime(last_event)
411 serverid = int(info.get('pid_server', 0))
412 serverport= int(info.get('port', 0))
413 procid = int(info.get('pid', 0))
414 appname = info.get('app', 'unknown')
415 machname = info.get('machine', 'unknown')
416 username = info.get('user', 'unknown')
417 keepalive_time = info.get('keepalive_time', -1)
418 keepalive_time += (last_event - time())
419 keepalive_units = 'seconds'
420 if keepalive_time > 150:
421 keepalive_time = round(keepalive_time/60.0)
422 keepalive_units = 'minutes'
423 if keepalive_time > 150:
424 keepalive_time = round(keepalive_time/60.0)
425 keepalive_units = 'hours'
427 print(f"""larch_server report:
428 Server Port Number = {serverport}
429 Server Process ID = {serverid}
430 Server Last Used = {last_used}
431 Server will expire in {keepalive_time} {keepalive_units} if not used.
432 Client Machine Name = {machname}
433 Client Process ID = {procid:d}
434 Client Application = {appname}
435 Client User Name = {username}
436""")
437 elif server_state == NOT_IN_USE:
438 smsg(port, 'not running')
439 sys.exit(1)
440 else:
441 smsg(port, 'port is in use by non-larch server')
443 else:
444 print(f"larch_server: unknown command '{command}'. Try -h")
447if __name__ == '__main__':
448 spawn_server(port=4966)