diff --git a/CHANGELOG.md b/CHANGELOG.md index 272a1dc71..007bd0a09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ at anytime. * Added `is_mine` field to `channel_list` results * Added `claim_renew` command * Added user configurable `auto_renew_claim_height_delta` setting, defaults to 0 (off) + * Added `lbrynet-console`, a tool to run or connect to lbrynet-daemon and launch an interactive python console with the api functions built in. ### Removed * Removed claim related filter arguments `name`, `claim_id`, and `outpoint` from `file_list`, `file_delete`, `file_set_status`, and `file_reflect` diff --git a/build/build.ps1 b/build/build.ps1 index 86b034a02..1baa55a51 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -25,6 +25,7 @@ pip install ..\. pyinstaller -y daemon.onefile.spec pyinstaller -y cli.onefile.spec +pyinstaller -y console.onefile.spec nuget install secure-file -ExcludeVersion secure-file\tools\secure-file -decrypt .\lbry2.pfx.enc -secret "$env:pfx_key" diff --git a/build/build.sh b/build/build.sh index d9094edb6..f23c098f7 100755 --- a/build/build.sh +++ b/build/build.sh @@ -42,6 +42,7 @@ cp "$ROOT/requirements.txt" "$BUILD_DIR/requirements_base.txt" cd "$BUILD_DIR" pyinstaller -y daemon.onefile.spec pyinstaller -y cli.onefile.spec + pyinstaller -y console.onefile.spec ) python "$BUILD_DIR/zip_daemon.py" diff --git a/build/console.onefile.spec b/build/console.onefile.spec new file mode 100644 index 000000000..420bf5043 --- /dev/null +++ b/build/console.onefile.spec @@ -0,0 +1,50 @@ +# -*- mode: python -*- +import platform +import os + +import lbryum + +dir = 'build'; +cwd = os.getcwd() +if os.path.basename(cwd) != dir: + raise Exception('pyinstaller build needs to be run from the ' + dir + ' directory') +repo_base = os.path.abspath(os.path.join(cwd, '..')) + +execfile(os.path.join(cwd, "entrypoint.py")) # ghetto import + + +system = platform.system() +if system == 'Darwin': + icns = os.path.join(repo_base, 'build', 'icon.icns') +elif system == 'Linux': + icns = os.path.join(repo_base, 'build', 'icons', '256x256.png') +elif system == 'Windows': + icns = os.path.join(repo_base, 'build', 'icons', 'lbry256.ico') +else: + print 'Warning: System {} has no icons'.format(system) + icns = None + + +datas = [ + (os.path.join(os.path.dirname(lbryum.__file__), 'wordlist', language + '.txt'), 'lbryum/wordlist') + for language in ('chinese_simplified', 'japanese', 'spanish','english', 'portuguese') +] + + +a = Entrypoint('lbrynet', 'console_scripts', 'lbrynet-console', pathex=[cwd], datas=datas) + +pyz = PYZ(a.pure, a.zipped_data) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name='lbrynet-console', + debug=False, + strip=False, + upx=True, + console=True, + icon=icns +) diff --git a/build/zip_daemon.py b/build/zip_daemon.py index ef3f5e222..53c60085b 100644 --- a/build/zip_daemon.py +++ b/build/zip_daemon.py @@ -10,7 +10,7 @@ def main(): tag = subprocess.check_output(['git', 'describe']).strip() zipfilename = 'lbrynet-daemon-{}-{}.zip'.format(tag, get_system_label()) full_filename = os.path.join(this_dir, 'dist', zipfilename) - executables = ['lbrynet-daemon', 'lbrynet-cli'] + executables = ['lbrynet-daemon', 'lbrynet-cli', 'lbrynet-console'] ext = '.exe' if platform.system() == 'Windows' else '' with zipfile.ZipFile(full_filename, 'w') as myzip: for executable in executables: diff --git a/lbrynet/daemon/DaemonConsole.py b/lbrynet/daemon/DaemonConsole.py new file mode 100644 index 000000000..6210dfc0e --- /dev/null +++ b/lbrynet/daemon/DaemonConsole.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- + +import sys +import code +import argparse +import logging.handlers +from exceptions import SystemExit +from twisted.internet import defer, reactor, threads +from lbrynet import analytics +from lbrynet import conf +from lbrynet.core import utils +from lbrynet.core import log_support +from lbrynet.daemon.DaemonServer import DaemonServer +from lbrynet.daemon.auth.client import LBRYAPIClient +from lbrynet.daemon.Daemon import Daemon + +get_client = LBRYAPIClient.get_client + +log = logging.getLogger(__name__) + + +if sys.platform.startswith('darwin') or sys.platform.startswith('linux'): + def color(msg, c="white"): + _colors = { + "normal": (0, 37), + "underlined": (2, 37), + "red": (1, 31), + "green": (1, 32), + "yellow": (1, 33), + "blue": (1, 33), + "magenta": (1, 34), + "cyan": (1, 35), + "white": (1, 36), + "grey": (1, 37) + } + i, j = _colors[c] + return "\033[%i;%i;40m%s\033[0m" % (i, j, msg) + + + logo = """\ + ╓▄█▄ç + ,▄█▓▓▀▀▀▓▓▓▌▄, + ▄▄▓▓▓▀¬ ╙▀█▓▓▓▄▄ + ,▄█▓▓▀▀ ^▀▀▓▓▓▌▄, + ▄█▓▓█▀` ╙▀█▓▓▓▄▄ + ╓▄▓▓▓▀╙ ▀▀▓▓▓▌▄, + ▄█▓▓█▀ ╙▀▓▓ + ╓▄▓▓▓▀` ▄█▓▓▓▀ + ▓▓█▀ ,▄▓▓▓▀╙ + ▓▓m ╟▌▄, ▄█▓▓█▀ ,,╓µ + ▓▓m ^▀█▓▓▓▄▄ ╓▄▓▓▓▀╙ █▓▓▓▓▓▀ + ▓▓Q '▀▀▓▓▓▌▄, ,▄█▓▓█▀ ▄█▓▓▓▓▓▀ + ▀▓▓▓▌▄, ╙▀█▓▓█▄╗ ╓▄▓▓▓▀ ╓▄▓▓▓▀▀ ▀▀ + ╙▀█▓▓█▄╗ ^▀▀▓▓▓▌▄▄█▓▓▀▀ ▄█▓▓█▀` + '▀▀▓▓▓▌▄, ╙▀██▀` ╓▄▓▓▓▀╙ + ╙▀█▓▓█▄╥ ,▄█▓▓▀▀ + └▀▀▓▓▓▌▄ ▄▒▓▓▓▀╙ + ╙▀█▓▓█▓▓▓▀▀ + ╙▀▀` + +""" +else: + def color(msg, c=None): + return msg + + logo = """\ + '. + ++++. + +++,;+++, + :+++ :+++: + +++ ,+++; + '++; .+++' + `+++ `++++ + +++. `++++ + ;+++ ++++ + +++ +++ + +++: '+ + ,+++ +++ + +++` +++: + `+' ,+++ + `+ + +++ + `+ +++ '++' :'+++: + `+ ++++ `+++ ++++ + `+ ++++ +++. :+++' + `+, ++++ ;+++ +++++ + `+++, ++++ +++ +++; + + ,+++, ++++ +++: .+++ + ,+++: '++++++ +++` + ,+++: '++ '++' + ,+++: `+++ + .+++; +++, + .+++; ;+++ + .+++; +++ + `+++++: + `++ + +""" + +welcometext = """\ +For a list of available commands: + >>>help() + +To see the documentation for a given command: + >>>help("resolve") + +To exit: + >>>exit() +""" + +welcome = "{:*^60}\n".format(" Welcome to the lbrynet interactive console! ") +welcome += "\n".join(["{:<60}".format(w) for w in welcometext.splitlines()]) +welcome += "\n%s" % ("*" * 60) +welcome = color(welcome, "grey") +banner = color(logo, "green") + color(welcome, "grey") + + +def get_methods(daemon): + locs = {} + + def wrapped(name, fn): + client = get_client() + _fn = getattr(client, name) + _fn.__doc__ = fn.__doc__ + return {name: _fn} + + for method_name, method in daemon.callable_methods.iteritems(): + locs.update(wrapped(method_name, method)) + return locs + + +def run_terminal(callable_methods, started_daemon, quiet=False): + locs = {} + locs.update(callable_methods) + + def help(method_name=None): + if not method_name: + print "Available api functions: " + for name in callable_methods: + print "\t%s" % name + return + if method_name not in callable_methods: + print "\"%s\" is not a recognized api function" + return + print callable_methods[method_name].__doc__ + return + + locs.update({'help': help}) + + if started_daemon: + def exit(status=None): + if not quiet: + print "Stopping lbrynet-daemon..." + callable_methods['daemon_stop']() + return sys.exit(status) + + locs.update({'exit': exit}) + else: + def exit(status=None): + try: + reactor.callLater(0, reactor.stop) + except Exception as err: + print "error stopping reactor: ", err + return sys.exit(status) + + locs.update({'exit': exit}) + + code.interact(banner if not quiet else "", local=locs) + + +@defer.inlineCallbacks +def start_server_and_listen(use_auth, analytics_manager, quiet): + log_support.configure_console() + logging.getLogger("lbrynet").setLevel(logging.CRITICAL) + logging.getLogger("lbryum").setLevel(logging.CRITICAL) + logging.getLogger("requests").setLevel(logging.CRITICAL) + + analytics_manager.send_server_startup() + daemon_server = DaemonServer(analytics_manager) + try: + yield daemon_server.start(use_auth) + analytics_manager.send_server_startup_success() + if not quiet: + print "Started lbrynet-daemon!" + defer.returnValue(True) + except Exception as e: + log.exception('Failed to start lbrynet-daemon') + analytics_manager.send_server_startup_error(str(e)) + daemon_server.stop() + raise + + +def threaded_terminal(started_daemon, quiet): + callable_methods = get_methods(Daemon) + d = threads.deferToThread(run_terminal, callable_methods, started_daemon, quiet) + d.addErrback(lambda err: err.trap(SystemExit)) + d.addErrback(log.exception) + + +def start_lbrynet_console(quiet, use_existing_daemon, useauth): + if not utils.check_connection(): + print "Not connected to internet, unable to start" + raise Exception("Not connected to internet, unable to start") + if not quiet: + print "Starting lbrynet-console..." + try: + get_client().status() + d = defer.succeed(False) + if not quiet: + print "lbrynet-daemon is already running, connecting to it..." + except: + if not use_existing_daemon: + if not quiet: + print "Starting lbrynet-daemon..." + analytics_manager = analytics.Manager.new_instance() + d = start_server_and_listen(useauth, analytics_manager, quiet) + else: + raise Exception("cannot connect to an existing daemon instance, " + "and set to not start a new one") + d.addCallback(threaded_terminal, quiet) + d.addErrback(log.exception) + + +def main(): + conf.initialize_settings() + parser = argparse.ArgumentParser(description="Launch lbrynet-daemon") + parser.add_argument( + "--use_existing_daemon", + help="Start lbrynet-daemon if it isn't already running", + action="store_true", + default=False, + dest="use_existing_daemon" + ) + parser.add_argument( + "--quiet", dest="quiet", action="store_true", default=False + ) + parser.add_argument( + "--http-auth", dest="useauth", action="store_true", default=conf.settings['use_auth_http'] + ) + args = parser.parse_args() + start_lbrynet_console(args.quiet, args.use_existing_daemon, args.useauth) + reactor.run() + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index 98eb075dd..211c2bc0a 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,8 @@ requires = [ console_scripts = [ 'lbrynet-daemon = lbrynet.daemon.DaemonControl:start', - 'lbrynet-cli = lbrynet.daemon.DaemonCLI:main' + 'lbrynet-cli = lbrynet.daemon.DaemonCLI:main', + 'lbrynet-console = lbrynet.daemon.DaemonConsole:main' ]