File: xmlrpc_server.py

package info (click to toggle)
xraylarch 0.9.58%2Bds1-5
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 74,640 kB
  • sloc: python: 63,075; fortran: 6,978; makefile: 1,877; ansic: 1,562; sh: 185; javascript: 104
file content (445 lines) | stat: -rw-r--r-- 14,524 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
#!/usr/bin/env python
from __future__ import print_function

import os
import sys
from time import time, sleep, ctime
import signal
import socket
from subprocess import Popen
from threading import Thread
from argparse import ArgumentParser, RawDescriptionHelpFormatter

from xmlrpc.server import SimpleXMLRPCServer
from xmlrpc.client import ServerProxy

from .interpreter import Interpreter
from .site_config import uname
from .utils.jsonutils import encode4js
from .utils import uname

try:
    import psutil
    HAS_PSUTIL = True
except ImportError:
    HAS_PSUTIL = False

NOT_IN_USE, CONNECTED, NOT_LARCHSERVER = range(3)
POLL_TIME = 0.50

"""Notes:
   0.  test server with HOST/PORT, report status (CREATED, ALREADY_RUNNING, FAILED).
   1.  prompt to kill a running server on HOST/PORT, preferably giving a
       'last used by {APPNAME} with {PROCESS_ID} at {DATETIME}'
   2.  launch server on next unused PORT on HOST, increment by 1 to 100, report status.
   3.  connect to running server on HOST/PORT.
   4.  have each client set a keepalive time (that is,
       'die after having no activity for X seconds') for each server (default=3*24*3600.0).
"""

def test_server(host='localhost', port=4966):
    """Test for a Larch server on host and port

    Arguments
      host (str): host name ['localhost']
      port (int): port number [4966]

    Returns
      integer status number:
          0    Not in use.
          1    Connected, valid Larch server
          2    In use, but not a valid Larch server
    """
    server = ServerProxy('http://%s:%d' % (host, port))
    try:
        methods = server.system.listMethods()
    except socket.error:
        return NOT_IN_USE

    # verify that this is a valid larch server
    if len(methods) < 5 or 'larch' not in methods:
        return NOT_LARCHSERVER
    ret = ''
    try:
        ret = server.get_rawdata('_sys.config.user_larchdir')
    except:
        return NOT_LARCHSERVER
    if len(ret) < 1:
        return NOT_LARCHSERVER

    return CONNECTED


def get_next_port(host='localhost', port=4966, nmax=100):
    """Return next available port for a Larch server on host

    Arguments
      host (str): host name ['localhost']
      port (int): starting port number [4966]
      nmax (int): maximum number to try [100]

    Returns
      integer: next unused port number or None in nmax exceeded.
    """
    # special case for localhost:
    # use psutil to find next unused port
    if host.lower() == 'localhost':
        if HAS_PSUTIL and uname == 'win':
            available = [True]*nmax
            try:
                conns = psutil.net_connections()
            except:
                conns = []
            if len(conns) > 0:
                for conn in conns:
                    ptest = conn.laddr[1] - port
                    if ptest >= 0 and ptest < nmax:
                        available[ptest] = False
            for index, status in enumerate(available):
                if status:
                    return port+index
        # now test with brute attempt to open the socket:
        for index in range(nmax):
            ptest = port + index
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
            success = False
            try:
                sock.bind(('', ptest))
                success = True
            except socket.error:
                pass
            finally:
                sock.close()
            if success:
                return ptest

    # for remote servers or if the above did not work, need to test ports
    for index in range(nmax):
        ptest = port + index
        if NOT_IN_USE == test_server(host=host, port=ptest):
            return ptest
    return None

class LarchServer(SimpleXMLRPCServer):
    def __init__(self, host='localhost', port=4966,
                 logRequests=False, allow_none=True,
                 keepalive_time=3*24*3600):
        self.out_buffer = []

        self.larch = Interpreter(writer=self)
        self.larch.input.prompt = ''
        self.larch.input.prompt2 = ''
        self.larch.run_init_scripts()

        self.larch('_sys.client = group(keepalive_time=%f)' % keepalive_time)
        self.larch('_sys.wx = group(wxapp=None)')
        _sys = self.larch.symtable._sys
        _sys.color_exceptions = False
        _sys.client.last_event = int(time())
        _sys.client.pid_server = int(os.getpid())
        _sys.client.app = 'unknown'
        _sys.client.pid = 0
        _sys.client.user = 'unknown'
        _sys.client.machine = socket.getfqdn()

        self.client = self.larch.symtable._sys.client
        self.port = port
        SimpleXMLRPCServer.__init__(self, (host, port),
                                    logRequests=logRequests,
                                    allow_none=allow_none)

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

        for method in ('ls', 'chdir', 'cd', 'cwd', 'shutdown',
                        'set_keepalive_time', 'set_client_info',
                        'get_client_info', 'get_data', 'get_rawdata',
                        'get_messages', 'len_messages'):
            self.register_function(getattr(self, method), method)

        # sys.stdout = self
        self.finished = False
        signal.signal(signal.SIGINT, self.signal_handler)
        self.activity_thread = Thread(target=self.check_activity)

    def write(self, text, **kws):
        if text is None:
            text = ''
        self.out_buffer.append(str(text))

    def flush(self):
        pass

    def set_keepalive_time(self, keepalive_time):
        """set keepalive time
        the server will self destruct after keepalive_time of inactivity

        Arguments:
            keepalive_time (number): time in seconds

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

    def set_client_info(self, key, value):
        """set client info

        Arguments:
            key (str): category
            value (str): value to use

        Notes:
            the key can actually be any string but include by convention:
               app      application name
               user     user name
               machine  machine name
               pid      process id
        """
        self.larch("_sys.client.%s = '%s'" % (key, value))

    def get_client_info(self):
        """get client info:
        returns json dictionary of client information
        """
        out = {'port': self.port}
        client = self.larch.symtable._sys.client
        for attr in dir(client):
            out[attr] = getattr(client, attr)
        return encode4js(out)

    def get_messages(self):
        """get (and clear) all output messages (say, from "print()")
        """
        out = "".join(self.out_buffer)
        self.out_buffer = []
        return out

    def len_messages(self):
        "length of message buffer"
        return len(self.out_buffer)

    def ls(self, dir_name):
        """list contents of a directory: """
        return os.listdir(dir_name)

    def chdir(self, dir_name):
        """change directory"""
        return os.chdir(dir_name)

    def cd(self, dir_name):
        """change directory"""
        return os.chdir(dir_name)

    def cwd(self):
        """change directory"""
        ret = os.getcwd()
        if uname == 'win':
            ret = ret.replace('\\','/')
        return ret

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

    def kill(self):
        """handle alarm signal, generated by signal.alarm(t)"""
        sleep(POLL_TIME)
        self.shutdown()
        self.server_close()

    def shutdown(self):
        "shutdown LarchServer"
        self.finished = True
        if self.activity_thread.is_alive():
            self.activity_thread.join(POLL_TIME)
        return 1

    def check_activity(self):
        while not self.finished:
            sleep(POLL_TIME)
            # print("Tick ", time()- (self.client.keepalive_time + self.client.last_event))
            if time() > (self.client.keepalive_time + self.client.last_event):
                t = Thread(target=self.kill)
                t.start()
                break

    def larch_exec(self, text):
        "execute larch command"
        text = text.strip()
        if text in ('quit', 'exit', 'EOF'):
            self.shutdown()
        else:
            ret = self.larch.eval(text, lineno=0)
            if ret is not None:
                self.write(repr(ret))
            self.client.last_event = time()
            self.flush()
        return 1

    def get_rawdata(self, expr):
        "return non-json encoded data for a larch expression"
        return self.larch.eval(expr)

    def get_data(self, expr):
        "return json encoded data for a larch expression"
        self.larch('_sys.client.last_event = %i' % time())
        return encode4js(self.larch.eval(expr))

    def run(self):
        """run server until times out"""
        self.activity_thread.start()
        while not self.finished:
            try:
                self.handle_request()
            except:
                break

def spawn_server(port=4966, wait=True, timeout=30):
    """
    start a new process for a LarchServer on selected port,
    optionally waiting to confirm connection
    """
    topdir = sys.exec_prefix
    pyexe = os.path.join(topdir, 'bin', 'python3')
    bindir = 'bin'
    if uname.startswith('win'):
            bindir = 'Scripts'
            pyexe = pyexe + '.exe'

    args = [pyexe, os.path.join(topdir, bindir, 'larch'),
            '-r', '-p', '%d' % port]
    pipe = Popen(args)
    if wait:
        t0 = time()
        while time() - t0 < timeout:
            sleep(POLL_TIME)
            if CONNECTED == test_server(port=port):
                break
    return pipe


###
def larch_server_cli():
    """command-line program to control larch XMLRPC server"""
    command_desc = """
command must be one of the following:
  start       start server on specified port
  stop        stop server on specified port
  restart     restart server on specified port
  next        start server on next avaialable port (see also '-n' option)
  status      print a short status message: whether server< is running on port
  report      print a multi-line status report
"""

    parser = ArgumentParser(description='run larch XML-RPC server',
                            formatter_class=RawDescriptionHelpFormatter,
                            epilog=command_desc)

    parser.add_argument("-p", "--port", dest="port", default='4966',
                        help="port number for remote server [4966]")

    parser.add_argument("-n", "--next", dest="next", action="store_true",
                        default=False,
                        help="show next available port, but do not start [False]")

    parser.add_argument("-q", "--quiet", dest="quiet", action="store_true",
                        default=False, help="suppress messaages [False]")

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

    args = parser.parse_args()


    port = int(args.port)
    command = args.command or 'status'
    command = command.lower()

    def smsg(port, txt):
        if not args.quiet:
            print('larch_server port=%i: %s' % (port, txt))


    if args.next:
        port = get_next_port(port=port)
        print(port)
        sys.exit(0)

    server_state = test_server(port=port)

    if command == 'start':
        if server_state == CONNECTED:
            smsg(port, 'already running')
        elif server_state == NOT_IN_USE:
            spawn_server(port=port)
            smsg(port, 'started')
        else:
            smsg(port, 'port is in use, cannot start')

    elif command == 'stop':
        if server_state == CONNECTED:
            ServerProxy('http://localhost:%d' % (port)).shutdown()
            smsg(port, 'stopped')

    elif command == 'next':
        port = get_next_port(port=port)
        spawn_server(port=port)
        smsg(port, 'started')

    elif command == 'restart':
        if server_state == CONNECTED:
            ServerProxy('http://localhost:%d' % (port)).shutdown()
            sleep(POLL_TIME)
        spawn_server(port=port)

    elif command == 'status':
        if server_state == CONNECTED:
            smsg(port, 'running')
            sys.exit(0)
        elif server_state == NOT_IN_USE:
            smsg(port, 'not running')
            sys.exit(1)
        else:
            smsg(port, 'port is in use by non-larch server')
    elif command == 'report':
        if server_state == CONNECTED:
            s = ServerProxy('http://localhost:%d' % (port))
            info = s.get_client_info()
            last_event = info.get('last_event', 0)
            last_used = ctime(last_event)
            serverid  = int(info.get('pid_server', 0))
            serverport= int(info.get('port', 0))
            procid    = int(info.get('pid', 0))
            appname   = info.get('app',     'unknown')
            machname  = info.get('machine', 'unknown')
            username  = info.get('user',    'unknown')
            keepalive_time = info.get('keepalive_time', -1)
            keepalive_time += (last_event - time())
            keepalive_units = 'seconds'
            if keepalive_time > 150:
                keepalive_time = round(keepalive_time/60.0)
                keepalive_units = 'minutes'
            if keepalive_time > 150:
                keepalive_time = round(keepalive_time/60.0)
                keepalive_units = 'hours'

            print('larch_server report:')
            print('   Server Port Number  = %s' % serverport)
            print('   Server Process ID   = %s' % serverid)
            print('   Server Last Used    = %s' % last_used)
            print('   Server will expire in %d %s if not used.' % (keepalive_time,
                                                                 keepalive_units))
            print('   Client Machine Name = %s' % machname)
            print('   Client Process ID   = %s' % str(procid))
            print('   Client Application  = %s' % appname)
            print('   Client User Name    = %s' % username)

        elif server_state == NOT_IN_USE:
            smsg(port, 'not running')
            sys.exit(1)
        else:
            smsg(port, 'port is in use by non-larch server')

    else:
        print("larch_server: unknown command '%s'. Try -h" % command)


if __name__ == '__main__':
    spawn_server(port=4966)