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

1#!/usr/bin/env python 

2""" 

3XML RPC 

4""" 

5from __future__ import print_function 

6 

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 

15 

16from xmlrpc.server import SimpleXMLRPCServer 

17from xmlrpc.client import ServerProxy 

18 

19from .interpreter import Interpreter 

20from .utils import uname, get_cwd 

21from .utils.jsonutils import encode4js 

22 

23try: 

24 import psutil 

25 HAS_PSUTIL = True 

26except ImportError: 

27 HAS_PSUTIL = False 

28 

29NOT_IN_USE, CONNECTED, NOT_LARCHSERVER = range(3) 

30POLL_TIME = 0.50 

31 

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""" 

41 

42def test_server(host='localhost', port=4966): 

43 """Test for a Larch server on host and port 

44 

45 Arguments 

46 host (str): host name ['localhost'] 

47 port (int): port number [4966] 

48 

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 

60 

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 

71 

72 return CONNECTED 

73 

74 

75def get_next_port(host='localhost', port=4966, nmax=100): 

76 """Return next available port for a Larch server on host 

77 

78 Arguments 

79 host (str): host name ['localhost'] 

80 port (int): starting port number [4966] 

81 nmax (int): maximum number to try [100] 

82 

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 

117 

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 

124 

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 = [] 

131 

132 self.larch = Interpreter(writer=self) 

133 self.larch.input.prompt = '' 

134 self.larch.input.prompt2 = '' 

135 self.larch.run_init_scripts() 

136 

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() 

147 

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) 

153 

154 self.register_introspection_functions() 

155 self.register_function(self.larch_exec, 'larch') 

156 

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) 

162 

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) 

167 

168 def write(self, text): 

169 if text is None: 

170 text = '' 

171 self.out_buffer.append(str(text)) 

172 

173 def flush(self): 

174 pass 

175 

176 def set_keepalive_time(self, keepalive_time): 

177 """set keepalive time 

178 the server will self destruct after keepalive_time of inactivity 

179 

180 Arguments: 

181 keepalive_time (number): time in seconds 

182 

183 """ 

184 self.larch("_sys.client.keepalive_time = %f" % keepalive_time) 

185 

186 def set_client_info(self, key, value): 

187 """set client info 

188 

189 Arguments: 

190 key (str): category 

191 value (str): value to use 

192 

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)) 

201 

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) 

211 

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 

218 

219 def len_messages(self): 

220 "length of message buffer" 

221 return len(self.out_buffer) 

222 

223 def ls(self, dir_name): 

224 """list contents of a directory: """ 

225 return os.listdir(dir_name) 

226 

227 def chdir(self, dir_name): 

228 """change directory""" 

229 return os.chdir(dir_name) 

230 

231 def cd(self, dir_name): 

232 """change directory""" 

233 return os.chdir(dir_name) 

234 

235 def cwd(self): 

236 """change directory""" 

237 ret = get_cwd() 

238 if uname == 'win': 

239 ret = ret.replace('\\','/') 

240 return ret 

241 

242 def signal_handler(self, sig=0, frame=None): 

243 self.kill() 

244 

245 def kill(self): 

246 """handle alarm signal, generated by signal.alarm(t)""" 

247 sleep(POLL_TIME) 

248 self.shutdown() 

249 self.server_close() 

250 

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 

257 

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 

266 

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 

279 

280 def get_rawdata(self, expr): 

281 "return non-json encoded data for a larch expression" 

282 return self.larch.eval(expr) 

283 

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)) 

288 

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 

297 

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' 

309 

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 

320 

321 

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""" 

334 

335 parser = ArgumentParser(description='run larch XML-RPC server', 

336 formatter_class=RawDescriptionHelpFormatter, 

337 epilog=command_desc) 

338 

339 parser.add_argument("-p", "--port", dest="port", default='4966', 

340 help="port number for remote server [4966]") 

341 

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]") 

345 

346 parser.add_argument("-q", "--quiet", dest="quiet", action="store_true", 

347 default=False, help="suppress messaages [False]") 

348 

349 parser.add_argument("command", nargs='?', help="server command ['status']") 

350 

351 args = parser.parse_args() 

352 

353 

354 port = int(args.port) 

355 command = args.command or 'status' 

356 command = command.lower() 

357 

358 def smsg(port, txt): 

359 if not args.quiet: 

360 print('larch_server port=%i: %s' % (port, txt)) 

361 

362 

363 if args.next: 

364 port = get_next_port(port=port) 

365 print(port) 

366 sys.exit(0) 

367 

368 server_state = test_server(port=port) 

369 

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') 

378 

379 elif command == 'stop': 

380 if server_state == CONNECTED: 

381 ServerProxy(f'http://localhost:{port:d}').shutdown() 

382 smsg(port, 'stopped') 

383 

384 elif command == 'next': 

385 port = get_next_port(port=port) 

386 spawn_server(port=port) 

387 smsg(port, 'started') 

388 

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) 

394 

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' 

425 

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') 

441 

442 else: 

443 print(f"larch_server: unknown command '{command}'. Try -h") 

444 

445 

446if __name__ == '__main__': 

447 spawn_server(port=4966)