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