Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions tests/test_reckless.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,18 @@ def test_basic_help():
assert r.search_stdout("options:") or r.search_stdout("optional arguments:")


def test_version():
'''Version should be reported without loading config and should advance
with lightningd'''
r = reckless(["-V", "-v", "--json"])
assert r.returncode == 0
import json
json_out = ''.join(r.stdout)
with open('.version', 'r') as f:
version = f.readlines()[0].strip()
assert json.loads(json_out)['result'][0] == version


def test_contextual_help(node_factory):
n = get_reckless_node(node_factory)
for subcmd in ['install', 'uninstall', 'search',
Expand Down Expand Up @@ -238,6 +250,18 @@ def test_install(node_factory):
assert os.path.exists(plugin_path)


def test_install_cleanup(node_factory):
"""test failed installation and post install cleanup"""
n = get_reckless_node(node_factory)
n.start()
r = reckless([f"--network={NETWORK}", "-v", "install", "testplugfail"], dir=n.lightning_dir)
assert r.returncode == 0
assert r.search_stdout('testplugfail failed to start')
r.check_stderr()
plugin_path = Path(n.lightning_dir) / 'reckless/testplugfail'
assert not os.path.exists(plugin_path)


@unittest.skipIf(VALGRIND, "virtual environment triggers memleak detection")
def test_poetry_install(node_factory):
"""test search, git clone, and installation to folder."""
Expand Down
121 changes: 106 additions & 15 deletions tools/reckless
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import argparse
import copy
import datetime
from enum import Enum
import io
import json
import logging
import os
Expand Down Expand Up @@ -276,14 +277,14 @@ class InstInfo:
pass
# If unable to search deeper, resort to matching directory name
elif recursion < 1:
if sub.name.lower() == self.name.lower():
if sub.name.lower().removesuffix('.git') == self.name.lower():
# Partial success (can't check for entrypoint)
self.name = sub.name
return sub
return None
sub.populate()

if sub.name.lower() == self.name.lower():
if sub.name.lower().removesuffix('.git') == self.name.lower():
# Directory matches the name we're trying to install, so check
# for entrypoint and dependencies.
for inst in INSTALLERS:
Expand All @@ -301,7 +302,7 @@ class InstInfo:
self.entry = found_entry.name
self.deps = found_dep.name
return sub
log.debug(f"missing dependency for {self}")
log.debug(f"{inst.name} installer: missing dependency for {self}")
found_entry = None
for file in sub.contents:
if isinstance(file, SourceDir):
Expand Down Expand Up @@ -403,7 +404,7 @@ class Source(Enum):
trailing = Path(source.lower().partition('github.com/')[2]).parts
if len(trailing) < 2:
return None, None
return trailing[0], trailing[1]
return trailing[0], trailing[1].removesuffix('.git')


class SourceDir():
Expand Down Expand Up @@ -450,7 +451,7 @@ class SourceDir():
for c in self.contents:
if ftype and not isinstance(c, ftype):
continue
if c.name.lower() == name.lower():
if c.name.lower().removesuffix('.git') == name.lower():
return c
return None

Expand Down Expand Up @@ -626,7 +627,7 @@ def populate_github_repo(url: str) -> list:
while '' in repo:
repo.remove('')
repo_name = None
parsed_url = urlparse(url)
parsed_url = urlparse(url.removesuffix('.git'))
if 'github.com' not in parsed_url.netloc:
return None
if len(parsed_url.path.split('/')) < 2:
Expand Down Expand Up @@ -1124,9 +1125,16 @@ INSTALLERS = [pythonuv, pythonuvlegacy, python3venv, poetryvenv,

def help_alias(targets: list):
if len(targets) == 0:
parser.print_help(sys.stdout)
if log.capture:
help_output = io.StringIO()
parser.print_help(help_output)
log.add_result(help_output.getvalue())
else:
parser.print_help(sys.stdout)
else:
log.info('try "reckless {} -h"'.format(' '.join(targets)))
if log.capture:
log.reply_json()
sys.exit(1)


Expand Down Expand Up @@ -1204,7 +1212,7 @@ def _git_update(github_source: InstInfo, local_copy: PosixPath):
if git.returncode != 0:
return False
default_branch = git.stdout.splitlines()[0]
if default_branch != 'origin/master':
if default_branch not in ['origin/master', 'origin/main']:
log.debug(f'UNUSUAL: fetched default branch {default_branch} for '
f'{github_source.source_loc}')

Expand Down Expand Up @@ -1476,10 +1484,22 @@ def _enable_installed(installed: InstInfo, plugin_name: str) -> Union[str, None]
if enable(installed.name):
return f"{installed.source_loc}"

log.error(('dynamic activation failed: '
f'{installed.name} not found in reckless directory'))
log.error('dynamic activation failed')
return None


def cleanup_plugin_installation(plugin_name):
"""Remove traces of an installation attempt."""
inst_path = Path(RECKLESS_CONFIG.reckless_dir) / plugin_name
if not inst_path.exists():
log.warning(f'asked to clean up {inst_path}, but nothing is present.')
return

log.info(f'Cleaning up partial installation of {plugin_name} at {inst_path}')
shutil.rmtree(inst_path)
return


def install(plugin_name: str) -> Union[str, None]:
"""Downloads plugin from source repos, installs and activates plugin.
Returns the location of the installed plugin or "None" in the case of
Expand All @@ -1496,7 +1516,7 @@ def install(plugin_name: str) -> Union[str, None]:
direct_location, name = location_from_name(name)
src = None
if direct_location:
logging.debug(f"install of {name} requested from {direct_location}")
log.debug(f"install of {name} requested from {direct_location}")
src = InstInfo(name, direct_location, name)
# Treating a local git repo as a directory allows testing
# uncommitted changes.
Expand All @@ -1521,8 +1541,17 @@ def install(plugin_name: str) -> Union[str, None]:
except FileExistsError as err:
log.error(f'File exists: {err.filename}')
return None
return _enable_installed(installed, plugin_name)
except InstallationFailure as err:
cleanup_plugin_installation(plugin_name)
if log.capture:
log.warning(err)
return None
raise err

result = _enable_installed(installed, plugin_name)
if not result:
cleanup_plugin_installation(plugin_name)
return result


def uninstall(plugin_name: str) -> str:
Expand Down Expand Up @@ -1555,7 +1584,7 @@ def search(plugin_name: str) -> Union[InstInfo, None]:
for src in RECKLESS_SOURCES:
# Search repos named after the plugin before collections
if Source.get_type(src) == Source.GITHUB_REPO:
if src.split('/')[-1].lower() == plugin_name.lower():
if src.split('/')[-1].lower().removesuffix('.git') == plugin_name.lower():
ordered_sources.remove(src)
ordered_sources.insert(0, src)
# Check locally before reaching out to remote repositories
Expand Down Expand Up @@ -1883,6 +1912,60 @@ def update_plugins(plugin_name: str):
return update_results


def extract_metadata(plugin_name: str) -> dict:
metadata_file = Path(RECKLESS_CONFIG.reckless_dir) / plugin_name / '.metadata'
if not metadata_file.exists():
return None

with open(metadata_file, 'r') as md:
lines = md.readlines()
metadata_headers = ['installation date',
'installation time',
'original source',
'requested commit',
'installed commit'
]
metadata = {}
current_key = None

for line in lines:
if line.strip() in metadata_headers:
current_key = line.strip()
continue

if current_key:
metadata.update({current_key: line.strip()})
current_key = None

return metadata


def listinstalled():
"""list all plugins currently managed by reckless"""
dir_contents = os.listdir(RECKLESS_CONFIG.reckless_dir)
plugins = {}
for plugin in dir_contents:
if (Path(RECKLESS_CONFIG.reckless_dir) / plugin).is_dir():
# skip hidden dirs such as reckless' .remote_sources
if plugin[0] == '.':
continue
plugins.update({plugin: None})
longest = {'name': 0,
'inst': 0}
# Format output in a simple table
name_len = 0
inst_len = 0
for plugin in plugins.keys():
md = extract_metadata(plugin)
name_len = max(name_len, len(plugin) + 1)
inst_len = max(inst_len, len(md['installed commit']) + 1)
for plugin in plugins.keys():
md = extract_metadata(plugin)
log.info(f"{plugin:<{name_len}} {md['installed commit']:<{inst_len}} {md['installation date']:<11} enabled")
plugins[plugin] = md
return plugins


def report_version() -> str:
"""return reckless version"""
log.info(__VERSION__)
Expand Down Expand Up @@ -1982,6 +2065,9 @@ if __name__ == '__main__':
update.add_argument('targets', type=str, nargs='*')
update.set_defaults(func=update_plugins)

list_cmd = cmd1.add_parser('list', help='list reckless-installed plugins')
list_cmd.set_defaults(func=listinstalled)

help_cmd = cmd1.add_parser('help', help='for contextual help, use '
'"reckless <cmd> -h"')
help_cmd.add_argument('targets', type=str, nargs='*')
Expand All @@ -1992,7 +2078,7 @@ if __name__ == '__main__':

all_parsers = [parser, install_cmd, uninstall_cmd, search_cmd, enable_cmd,
disable_cmd, list_parse, source_add, source_rem, help_cmd,
update]
update, list_cmd]
for p in all_parsers:
# This default depends on the .lightning directory
p.add_argument('-d', '--reckless-dir', action=StoreIdempotent,
Expand Down Expand Up @@ -2036,6 +2122,9 @@ if __name__ == '__main__':
'litecoin', 'signet', 'testnet', 'testnet4']
if args.version:
report_version()
if log.capture:
log.reply_json()
sys.exit(0)
elif args.cmd1 is None:
parser.print_help(sys.stdout)
sys.exit(1)
Expand Down Expand Up @@ -2075,7 +2164,9 @@ if __name__ == '__main__':

if 'targets' in args: # and len(args.targets) > 0:
if args.func.__name__ == 'help_alias':
args.func(args.targets)
log.add_result(args.func(args.targets))
if log.capture:
log.reply_json()
sys.exit(0)
# Catch a missing argument so that we can overload functions.
if len(args.targets) == 0:
Expand Down
Loading