File: JsonRpc1DPlugin.py

package info (click to toggle)
pymca 5.8.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 44,392 kB
  • sloc: python: 155,456; ansic: 15,843; makefile: 116; sh: 73; xml: 55
file content (598 lines) | stat: -rw-r--r-- 19,171 bytes parent folder | download | duplicates (3)
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
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
# /*#########################################################################
#
# The PyMca X-Ray Fluorescence Toolkit
#
# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
#
# This file is part of the PyMca X-ray Fluorescence Toolkit developed at
# the ESRF by the Software group.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
__author__ = "T. Vincent - ESRF Data Analysis"
__contact__ = "thomas.vincent@esrf.fr"
__license__ = "MIT"
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
__doc__ = """
This module provides a PyMca plugin allowing to either download or receive
`JSON-RPC 2.0 <http://www.jsonrpc.org/specification>`_ requests that are
interpreted as commands for the 1D plot window.

Two modes are supported:

- *A client mode*, where the plugin downloads JSON-RPC files.
  In this mode, it is possible to download files from a given URL
  either on demand or at regular intervals (i.e., polling).
- *A server mode*, where the plugin is a TCP server that parses incoming data
  as JSON-RPC files.

As is, only :meth:`Plugin1DBase.addCurve` is available through RPC, but
more methods can be supported by populating the ``_handlers`` attribute of
:class:`PyMcaJsonRpc1DPlugin`.

Remote code in either server or client mode can use the
:func:`addCurveToJsonRpc` that has the same signature as
:meth:`Plugin1DBase.addCurve` and returns the corresponding JSON-RPC string
that can be interpreted by the plugin.
"""


# import ######################################################################

import json
import numpy as np

import sys
if sys.version_info.major == 2:
    from urllib2 import urlopen
    from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
    from SocketServer import StreamRequestHandler, TCPServer
else:
    from urllib.request import urlopen
    from http.server import BaseHTTPRequestHandler, HTTPServer
    from socketserver import StreamRequestHandler, TCPServer

from PyMca5.PyMcaCore import Plugin1DBase
from PyMca5.PyMcaGui import PyMcaQt as qt


# JSON-RPC ####################################################################
# Light implementation of JSON-RPC 2.0 : http://www.jsonrpc.org/specification

_PARSE_ERROR = -32700
_INVALID_REQUEST = -32600
_METHOD_NOT_FOUND = -32601
_INVALID_PARAMS = -32602
_INTERNAL_ERROR = -32603
_SERVER_ERROR = -32000

_ERRORS = {
    _PARSE_ERROR: 'Parse Error',
    _INVALID_REQUEST: 'Invalid Request',
    _METHOD_NOT_FOUND: 'Method Not Found',
    _INVALID_PARAMS: 'Invalid Parameters',
    _INTERNAL_ERROR: 'Internal Error',
    _SERVER_ERROR: 'Server Error',
}


def _jsonRpcError(code, data=None):
    assert code in _ERRORS
    return {'code': code, 'message': _ERRORS[code], 'data': data}


def jsonRpcProcessRequest(requestFileObj, handlers):
    """Parse a single JSON-RPC request and call the associated handler function

    Limitations: Do not support batch, not checking parameters

    :param requestFileObj: JSON-RPC request document
    :type requestFileObj: A .read()-supporting file-like object
    :param dict handlers: RPC method handlers: {method name: handler function}
    :returns: The JSON-RPC reply or None if request was a notification
    :rtype: str
    """
    try:
        request = json.load(requestFileObj)
    except ValueError:
        return {'jsonrpc': '2.0',
                'id': None,
                'error': _jsonRpcError(_PARSE_ERROR)}

    try:
        reply = {'jsonrpc': '2.0', 'id': request['id']}
    except KeyError:
        reply = None

    try:
        methodName = request['method']
    except KeyError:
        if reply is not None:
            reply['error'] = _jsonRpcError(_INVALID_REQUEST,
                                           'Request has no method name')
        return reply

    try:
        method = handlers[methodName]
    except KeyError:
        if reply is not None:
            data = 'Unknown method: {}'.format(methodName)
            reply['error'] = _jsonRpcError(_METHOD_NOT_FOUND, data)
        return reply

    params = request.get('params', {})

    # Convert list to np.array
    if isinstance(params, dict):
        for key in params:
            value = params[key]
            if isinstance(value, list):
                params[key] = np.array(value)
    else:
        params = [np.array(v) if isinstance(v, list) else v for v in params]

    try:
        if isinstance(params, dict):
            result = method(**params)
        else:
            result = method(*params)
    except Exception as exception:
        if reply is not None:
            if isinstance(exception, TypeError):
                reply['error'] = _jsonRpcError(_INVALID_PARAMS,
                                               params)
            else:
                reply['error'] = _jsonRpcError(_SERVER_ERROR,
                                               (exception.errno,
                                                exception.strerror))
        return reply

    if reply is not None:
        reply['result'] = result
        reply = json.dumps(reply)

    return reply


# dialog box ##################################################################

class _DialogBox(qt.QDialog):
    LOAD = "Load"
    START_POLL = "Start Polling"
    STOP_POLL = "Stop Polling"
    START_SERVER = "Start Server"
    STOP_SERVER = "Stop Server"

    def __init__(self, parent, plugin):
        self._plugin = plugin

        qt.QDialog.__init__(self, parent)
        self.setWindowTitle('JSON-RPC Plugin')

        # Poll GUI
        pollLayout = qt.QFormLayout()
        pollGroup = qt.QGroupBox()
        pollGroup.setTitle('Client mode (Polling)')
        pollGroup.setLayout(pollLayout)

        pollUrlLabel = qt.QLabel("URL:")
        pollUrlLabel.setToolTip(
            "The URL to download JSON-RPC file from\n" +
            "Supported protocols: http:, ftp: file:")
        self.pollUrlLineEdit = qt.QLineEdit(self._plugin.pollUrl)
        pollLayout.addRow(pollUrlLabel, self.pollUrlLineEdit)

        pollTimeoutLabel = qt.QLabel("Timeout (s):")
        pollTimeoutLabel.setToolTip(
            "The interval in seconds at which the plugin is polling the URL")
        self.pollTimeoutLineEdit = qt.QLineEdit(str(self._plugin.pollTimeout))
        # Bounds timeout to avoid to small timeout
        self.pollTimeoutLineEdit.setValidator(
            qt.CLocaleQDoubleValidator(0.02, 1000.0, 2))
        pollLayout.addRow(pollTimeoutLabel, self.pollTimeoutLineEdit)

        self.pollLoadBtn = qt.QPushButton(self.LOAD)
        pollLayout.addRow(self.pollLoadBtn)

        if self._plugin.isPollRunning():
            pollBtnText = self.STOP_POLL
        else:
            pollBtnText = self.START_POLL
        self.pollBtn = qt.QPushButton(pollBtnText)
        pollLayout.addRow(self.pollBtn)

        # Server GUI
        serverLayout = qt.QFormLayout()
        serverGroup = qt.QGroupBox()
        serverGroup.setTitle('TCP Server mode')
        serverGroup.setLayout(serverLayout)

        serverPortLabel = qt.QLabel("TCP Port:")
        serverPortLabel.setToolTip(
            "The TCP port the server is listening to.\n" +
            "Ranging in [1024-65535]")
        self.serverPortLineEdit = qt.QLineEdit(str(self._plugin.serverPort))
        # Bounds port to 'user' ports
        self.serverPortLineEdit.setValidator(qt.QIntValidator(1024, 65535))
        serverLayout.addRow(serverPortLabel, self.serverPortLineEdit)

        if self._plugin.isServerRunning():
            serverBtnText = self.STOP_SERVER
        else:
            serverBtnText = self.START_SERVER
        self.serverBtn = qt.QPushButton(serverBtnText)
        serverLayout.addRow(self.serverBtn)

        # Main layout
        closeBtn = qt.QPushButton('Close')

        mainLayout = qt.QVBoxLayout()
        mainLayout.addWidget(pollGroup)
        mainLayout.addWidget(serverGroup)
        mainLayout.addWidget(closeBtn)

        self.setLayout(mainLayout)

        # Signals
        self.pollTimeoutLineEdit.editingFinished.connect(self.pollTimeoutCB)
        self.pollUrlLineEdit.editingFinished.connect(self.pollUrlCB)
        self.serverPortLineEdit.editingFinished.connect(self.serverPortCB)

        self.pollLoadBtn.clicked.connect(self.pollLoadBtnCB)
        self.pollBtn.clicked.connect(self.pollBtnCB)
        self.serverBtn.clicked.connect(self.serverBtnCB)
        closeBtn.clicked.connect(self.accept)

    def pollTimeoutCB(self):
        self._plugin.pollTimeout = float(self.pollTimeoutLineEdit.text())

    def pollUrlCB(self):
        self._plugin.pollUrl = self.pollUrlLineEdit.text()

    def serverPortCB(self):
        self._plugin.serverPort = int(self.serverPortLineEdit.text())

    def pollLoadBtnCB(self):
        self._plugin.load()

    def pollBtnCB(self):
        if self.pollBtn.text() == self.START_POLL:
            self._plugin.startPoll()
        else:
            self._plugin.stopPoll()

        if self._plugin.isPollRunning():
            pollBtnText = self.STOP_POLL
        else:
            pollBtnText = self.START_POLL
        self.pollBtn.setText(pollBtnText)

    def serverBtnCB(self):
        if self.serverBtn.text() == self.START_SERVER:
            self._plugin.startTcpServer()
        else:
            self._plugin.stopTcpServer()

        if self._plugin.isServerRunning():
            serverBtnText = self.STOP_SERVER
        else:
            serverBtnText = self.START_SERVER
        self.serverBtn.setText(serverBtnText)


# plugin ######################################################################

class PyMcaJsonRpc1DPlugin(Plugin1DBase.Plugin1DBase):
    def __init__(self, *args, **kwargs):
        super(PyMcaJsonRpc1DPlugin, self).__init__(*args, **kwargs)
        self._handlers = {
            'addCurve': self.addCurve,
        }

        # Parameters used by poll and server
        self.pollTimeout = 1.
        self.pollUrl = ''
        self.serverPort = 8000
        self._serverTimeout = 0.1

    def __del__(self):
        self.stopPoll()
        self.stopTcpServer()

    def getMethods(self, plottype=None):
        return ("JSON-RPC",)

    def getMethodToolTip(self, methodName):
        return "Allow to control the plot through JSON-RPC"

    def applyMethod(self, methodName):
        dialogBox = _DialogBox(None, self)
        dialogBox.exec()

    def load(self):
        try:
            fileObj = urlopen(self.pollUrl)
        except ValueError:
            print('PyMcaJsonRpc1DPlugin.load: Bad URL:', self.pollUrl)
            return False
        else:
            jsonRpcProcessRequest(fileObj, self._handlers)
            fileObj.close()
            return True

    def startPoll(self):
        self.stopPoll()

        # Load file once to check if URL is correct
        if not self.load():
            return False
        else:
            self._pollTimer = qt.QTimer()
            self._pollTimer.timeout.connect(self.load)
            self._pollTimer.start(1000. * self.pollTimeout)
            return True

    def stopPoll(self):
        if hasattr(self, '_pollTimer'):
            self._pollTimer.stop()
            del self._pollTimer

    def isPollRunning(self):
        return hasattr(self, '_pollTimer')

    def startTcpServer(self):
        self.stopTcpServer()

        handlers = self._handlers

        class RequestHandler(StreamRequestHandler):
            def handle(self):
                reply = jsonRpcProcessRequest(self.rfile, handlers)
                if reply is not None:
                    self.wfile.write(reply)

        self._server = TCPServer(('', self.serverPort), RequestHandler,
                                 bind_and_activate=False)
        self._server.allow_reuse_address = True
        self._server.timeout = 0.01
        self._server.server_bind()
        self._server.server_activate()

        # Should do it differently
        self._serverTimer = qt.QTimer()
        self._serverTimer.timeout.connect(self._server.handle_request)
        self._serverTimer.start(1000. * self._serverTimeout)

    def stopTcpServer(self):
        if hasattr(self, '_serverTimer'):
            self._serverTimer.stop()
            del self._serverTimer
        if hasattr(self, '_server'):
            del self._server

    def isServerRunning(self):
        return hasattr(self, '_server')


MENU_TEXT = "JSON-RPC"


def getPlugin1DInstance(plotWindow, **kwargs):
    return PyMcaJsonRpc1DPlugin(plotWindow)


# helper ######################################################################

def addCurveToJsonRpc(x, y, legend=None, info=None,
                      replace=False, replot=True, **kw):
    """Generate a JSON-RPC request for calling :meth:`Plugin1DBase.addCurve`
    on a :class:`PyMcaJsonRpc1DPlugin` plugin.

    See :class:`Plugin1DBase` for details.

    :returns: A JSON-RPC request corresponding to the addCurve call
    :rtype: str
    """
    if not isinstance(x, (list, tuple)):
        x = tuple(x)
    if not isinstance(y, (list, tuple)):
        y = tuple(y)

    params = {
        "info": info,
        "x": x,
        "y": y,
        "legend": legend,
        "replace": replace,
        "replot": replot
    }
    params.update(kw)

    request = {
        "jsonrpc": "2.0",
        "method": "addCurve",
        "params": params
    }
    return json.dumps(request)


# demo polling ################################################################

def _testServer(address):
    # Create a test http server
    class TestRequestHandler(BaseHTTPRequestHandler):
        def do_GET(self):
            self.send_response(200)
            self.send_header("Content-type", "application/json")
            self.end_headers()
            request = addCurveToJsonRpc(
                np.arange(100.),
                np.random.random(100) * 10,
                legend="test {:.2f}".format(time.time()),
                replace=True,
                replot=True)
            self.wfile.write(request)

    return HTTPServer(address, TestRequestHandler)


class _DemoClientModeAuto(object):
    def __init__(self, plugin, onFinish):
        self._plugin = plugin
        self._onFinish = onFinish

    def start(self):
        import threading

        # Start server in a thread
        self._httpdServer = _testServer(('localhost', 8000))
        self._httpdThread = threading.Thread(
            target=self._httpdServer.serve_forever)
        self._httpdThread.start()

        # Set-up URL to download
        self._plugin.pollUrl = 'http://localhost:8000'

        print("Load JSON from server once")
        self._plugin.load()

        qt.QTimer.singleShot(2000., self._startPoll)

    def _startPoll(self):
        print("Start polling JSON from server")
        self._plugin.startPoll()
        qt.QTimer.singleShot(5000., self._stopPoll)

    def _stopPoll(self):
        print("Stop polling JSON from server")
        self._plugin.stopPoll()
        self._httpdServer.shutdown()
        self._httpdServer.socket.close()

        self._onFinish()


# demo server mode ############################################################

def _sendJsonRpcAddCurve(address):
    import socket

    request = addCurveToJsonRpc(
        np.arange(100.),
        np.random.random(100) * 10,
        legend="test {:.2f}".format(time.time()),
        replace=True,
        replot=True
    )

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(address)
    sock.send(request)
    sock.close()


class _DemoServerModeAuto(object):
    def __init__(self, plugin, onFinish):
        self._plugin = plugin
        self._onFinish = onFinish

    def start(self):
        import threading

        print("Start TCP server")
        self._plugin.startTcpServer()

        print("Start sending JSON-RPC through TCP")
        self._isSending = True
        self._senderThread = threading.Thread(target=self._sender)
        self._senderThread.start()

        qt.QTimer.singleShot(5000., self._stop)

    def _sender(self):
        while self._isSending:
            _sendJsonRpcAddCurve(('localhost', 8000))
            time.sleep(1)
        print("Stop sending JSON-RPC through TCP")

    def _stop(self):
        self._isSending = False
        self._senderThread.join()

        print('Stop server')
        self._plugin.stopTcpServer()

        self._onFinish()


# main ########################################################################

if __name__ == "__main__":
    import time
    import sys
    import os.path
    from PyMca5.PyMcaGui.plotting.PlotWindow import PlotWindow

    if len(sys.argv) == 1 or \
       sys.argv[1] not in ('plugin', 'demoServer', 'demoClient', 'auto'):
        print("""
Options: plugin, demoServer demoClient, auto

- plugin: Start a 1D plot window with JSPON-RPC plugin available
- demoServer: HTTP server, to load JSON-RPC from URL: http://localhost:8000
- demoClient: TCP client that sends JSPN-RPC to localhost:8000
- auto: Starts demo of polling and server that runs alone

""")
        sys.exit()

    if sys.argv[1] in ('plugin', 'auto'):
        # Create Qt main application
        app = qt.QApplication([])

        # Create plot window
        plot = PlotWindow(roi=True)
        plot.show()

        # Load plugin
        pluginDir = os.path.dirname(os.path.abspath(__file__))
        pluginName = os.path.splitext(os.path.basename(__file__))[0]

        plot.setPluginDirectoryList([pluginDir])
        nbPlugins = plot.getPlugins()  # Update plug-in list
        assert nbPlugins >= 1
        plugin = plot.pluginInstanceDict[pluginName]

        if sys.argv[1] == 'auto':
            # Run automated demos
            serverDemo = _DemoServerModeAuto(plugin, onFinish=app.quit)
            clientDemo = _DemoClientModeAuto(plugin, onFinish=serverDemo.start)
            clientDemo.start()

        app.exec()

    elif sys.argv[1] == 'demoServer':
        httpdServer = _testServer(('localhost', 8000))
        httpdServer.serve_forever()

    elif sys.argv[1] == 'demoClient':
        while True:
            _sendJsonRpcAddCurve(('localhost', 8000))
            time.sleep(1)