Generateurv2/backend/env/lib/python3.10/site-packages/autobahn/__main__.py
2022-06-24 17:14:37 +02:00

411 lines
11 KiB
Python

###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Crossbar.io Technologies GmbH
#
# 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.
#
###############################################################################
# this module is available as the 'wamp' command-line tool or as
# 'python -m autobahn'
import os
import sys
import argparse
import json
from copy import copy
try:
from autobahn.twisted.component import Component
except ImportError:
print("The 'wamp' command-line tool requires Twisted.")
print(" pip install autobahn[twisted]")
sys.exit(1)
from twisted.internet.defer import Deferred, inlineCallbacks
from twisted.internet.task import react
from twisted.internet.protocol import ProcessProtocol
from autobahn.wamp.exception import ApplicationError
from autobahn.wamp.types import PublishOptions
from autobahn.wamp.types import SubscribeOptions
import txaio
txaio.use_twisted()
# XXX other ideas to get 'connection config':
# - if there .crossbar/ here, load that config and accept a --name or
# so to idicate which transport to use
# wamp [options] {call,publish,subscribe,register} wamp-uri [args] [kwargs]
#
# kwargs are spec'd with a 2-value-consuming --keyword option:
# --keyword name value
top = argparse.ArgumentParser(prog="wamp")
top.add_argument(
'--url',
action='store',
help='A WAMP URL to connect to, like ws://127.0.0.1:8080/ws or rs://localhost:1234',
required=True,
)
top.add_argument(
'--realm', '-r',
action='store',
help='The realm to join',
default='default',
)
top.add_argument(
'--private-key', '-k',
action='store',
help='Hex-encoded private key (via WAMP_PRIVATE_KEY if not provided here)',
default=os.environ.get('WAMP_PRIVATE_KEY', None),
)
top.add_argument(
'--authid',
action='store',
help='The authid to use, if authenticating',
default=None,
)
top.add_argument(
'--authrole',
action='store',
help='The role to use, if authenticating',
default=None,
)
top.add_argument(
'--max-failures', '-m',
action='store',
type=int,
help='Failures before giving up (0 forever)',
default=0,
)
sub = top.add_subparsers(
title="subcommands",
dest="subcommand_name",
)
call = sub.add_parser(
'call',
help='Do a WAMP call() and print any results',
)
call.add_argument(
'uri',
type=str,
help="A WAMP URI to call"
)
call.add_argument(
'call_args',
nargs='*',
help="All additional arguments are positional args",
)
call.add_argument(
'--keyword',
nargs=2,
action='append',
help="Specify a keyword argument to send: name value",
)
publish = sub.add_parser(
'publish',
help='Do a WAMP publish() with the given args, kwargs',
)
publish.add_argument(
'uri',
type=str,
help="A WAMP URI to publish"
)
publish.add_argument(
'publish_args',
nargs='*',
help="All additional arguments are positional args",
)
publish.add_argument(
'--keyword',
nargs=2,
action='append',
help="Specify a keyword argument to send: name value",
)
register = sub.add_parser(
'register',
help='Do a WAMP register() and run a command when called',
)
register.add_argument(
'uri',
type=str,
help="A WAMP URI to call"
)
register.add_argument(
'--times',
type=int,
default=0,
help="Listen for this number of events, then exit. Default: forever",
)
register.add_argument(
'command',
type=str,
nargs='*',
help=(
"Takes one or more args: the executable to call, and any positional "
"arguments. As well, the following environment variables are set: "
"WAMP_ARGS, WAMP_KWARGS and _JSON variants."
)
)
subscribe = sub.add_parser(
'subscribe',
help='Do a WAMP subscribe() and print one line of JSON per event',
)
subscribe.add_argument(
'uri',
type=str,
help="A WAMP URI to call"
)
subscribe.add_argument(
'--times',
type=int,
default=0,
help="Listen for this number of events, then exit. Default: forever",
)
subscribe.add_argument(
'--match',
type=str,
default='exact',
choices=['exact', 'prefix'],
help="Massed in the SubscribeOptions, how to match the URI",
)
def _create_component(options):
"""
Configure and return a Component instance according to the given
`options`
"""
if options.url.startswith('ws://'):
kind = 'websocket'
elif options.url.startswith('rs://'):
kind = 'rawsocket'
else:
raise ValueError(
"URL should start with ws:// or rs://"
)
authentication = dict()
if options.private_key:
if not options.authid:
raise ValueError(
"Require --authid and --authrole if --private-key (or WAMP_PRIVATE_KEY) is provided"
)
authentication["cryptosign"] = {
"authid": options.authid,
"authrole": options.authrole,
"privkey": options.private_key,
}
return Component(
transports=[{
"type": kind,
"url": options.url,
}],
authentication=authentication if authentication else None,
realm=options.realm,
)
@inlineCallbacks
def do_call(reactor, session, options):
call_args = list(options.call_args)
call_kwargs = dict()
if options.keyword is not None:
call_kwargs = {
k: v
for k, v in options.keyword
}
results = yield session.call(options.uri, *call_args, **call_kwargs)
print("result: {}".format(results))
@inlineCallbacks
def do_publish(reactor, session, options):
publish_args = list(options.publish_args)
publish_kwargs = {} if options.keyword is None else {
k: v
for k, v in options.keyword
}
yield session.publish(
options.uri,
*publish_args,
options=PublishOptions(acknowledge=True),
**publish_kwargs
)
@inlineCallbacks
def do_register(reactor, session, options):
"""
run a command-line upon an RPC call
"""
all_done = Deferred()
countdown = [options.times]
@inlineCallbacks
def called(*args, **kw):
print("called: args={}, kwargs={}".format(args, kw), file=sys.stderr)
env = copy(os.environ)
env['WAMP_ARGS'] = ' '.join(args)
env['WAMP_ARGS_JSON'] = json.dumps(args)
env['WAMP_KWARGS'] = ' '.join('{}={}'.format(k, v) for k, v in kw.items())
env['WAMP_KWARGS_JSON'] = json.dumps(kw)
exe = os.path.abspath(options.command[0])
args = options.command
done = Deferred()
class DumpOutput(ProcessProtocol):
def outReceived(self, data):
sys.stdout.write(data.decode('utf8'))
def errReceived(self, data):
sys.stderr.write(data.decode('utf8'))
def processExited(self, reason):
done.callback(reason.value.exitCode)
proto = DumpOutput()
reactor.spawnProcess(
proto, exe, args, env=env, path="."
)
code = yield done
if code != 0:
print("Failed with exit-code {}".format(code))
if countdown[0]:
countdown[0] -= 1
if countdown[0] <= 0:
reactor.callLater(0, all_done.callback, None)
yield session.register(called, options.uri)
yield all_done
@inlineCallbacks
def do_subscribe(reactor, session, options):
"""
print events (one line of JSON per event)
"""
all_done = Deferred()
countdown = [options.times]
@inlineCallbacks
def published(*args, **kw):
print(
json.dumps({
"args": args,
"kwargs": kw,
})
)
if countdown[0]:
countdown[0] -= 1
if countdown[0] <= 0:
reactor.callLater(0, all_done.callback, None)
yield session.subscribe(published, options.uri, options=SubscribeOptions(match=options.match))
yield all_done
def _main():
"""
This is a magic name for `python -m autobahn`, and specified as
our entry_point in setup.py
"""
react(_real_main)
@inlineCallbacks
def _real_main(reactor):
"""
Sanity check options, create a connection and run our subcommand
"""
options = top.parse_args()
component = _create_component(options)
if options.subcommand_name is None:
print("Must select a subcommand")
sys.exit(1)
if options.subcommand_name == "register":
exe = options.command[0]
if not os.path.isabs(exe):
print("Full path to the executable required. Found: {}".format(exe), file=sys.stderr)
sys.exit(1)
if not os.path.exists(exe):
print("Executable not found: {}".format(exe), file=sys.stderr)
sys.exit(1)
subcommands = {
"call": do_call,
"register": do_register,
"subscribe": do_subscribe,
"publish": do_publish,
}
command_fn = subcommands[options.subcommand_name]
exit_code = [0]
@component.on_join
@inlineCallbacks
def _(session, details):
print("connected: authrole={} authmethod={}".format(details.authrole, details.authmethod), file=sys.stderr)
try:
yield command_fn(reactor, session, options)
except ApplicationError as e:
print("\n{}: {}\n".format(e.error, ''.join(e.args)))
exit_code[0] = 5
yield session.leave()
failures = []
@component.on_connectfailure
def _(comp, fail):
print("connect failure: {}".format(fail))
failures.append(fail)
if options.max_failures > 0 and len(failures) > options.max_failures:
print("Too many failures ({}). Exiting".format(len(failures)))
reactor.stop()
yield component.start(reactor)
# sys.exit(exit_code[0])
if __name__ == "__main__":
_main()