class EssexPapertrail(ColorApp): """Print a sample Papertrail log_files.yml""" interactive = Flag( ['i', 'interactive'], help="interactively ask the user for host and port" ) def main(self, host='{{ PAPERTRAIL_HOST }}', port='{{ PAPERTRAIL_PORT }}'): if self.interactive: host = input("Papertrail host: ") port = input("Papertrail port: ") entries = '\n'.join( f" - tag: {svc.name}\n" f" path: {self.parent.logs_dir / svc.name / 'current'}" for svc in self.parent.svcs ) print( f"files:", f"{entries}", f"destination:", f" host: {host}", f" port: {port}", f" protocol: tls", sep='\n' )
class EssexStatus(ColorApp): """View the current states of (all or specified) services""" enabled_only = Flag( ['e', 'enabled'], help="only list enabled services (configured to be running)" ) def main(self, *svc_names): self.parent.fail_if_unsupervised() s6_svscanctl('-a', self.parent.svcs_dir) cols = ( 'up', 'wantedup', 'normallyup', 'ready', 'paused', 'pid', 'exitcode', 'signal', 'signum', 'updownsince', 'readysince', 'updownfor', 'readyfor' ) errors = False for svc in self.parent.svc_map(svc_names or self.parent.svcs): if 'run' in svc: if self.enabled_only and 'down' in svc: continue stats = { col: False if val == 'false' else val for col, val in zip( cols, s6_svstat('-o', ','.join(cols), svc).split() ) } statline = f"{svc.name:<20} {'up' if stats['up'] else 'down':<5} {stats['updownfor'] + 's':<10} {stats['pid'] if stats['pid'] != '-1' else stats['exitcode']:<6} {'autorestarts' if stats['wantedup'] else '':<13} {'autostarts' if stats['normallyup'] else '':<11}" print(statline | (green if stats['up'] else red)) else: warn(f"{svc} doesn't exist") errors = True if errors: fail(1)
class EssexTree(ColorApp): """View the process tree from the supervision root""" quiet = Flag( ['q', 'quiet'], help=( "don't print childless supervisors, s6-log processes, or s6-log supervisors; " "has no effect when pstree is provided by busybox" ) ) def main(self): self.parent.fail_if_unsupervised() try: readlink(pstree) except: # real pstree tree = pstree['-apT', self.parent.root_pid]() if self.quiet: tl = tree.splitlines() whitelist = set(range(len(tl))) for i, line in enumerate(tl): if re.match(r'^ +(\||`)-s6-supervise,', line): # supervisor if i + 1 == len(tl) or re.match(r'^ +(\||`)-s6-supervise,', tl[i + 1]): whitelist.discard(i) elif re.match(r'^ +\| +`-s6-log,', line): # logger whitelist.discard(i) whitelist.discard(i - 1) tree = '\n'.join(tl[i] for i in sorted(whitelist)) else: # busybox pstree tree = pstree['-p', self.parent.root_pid]() print(tree)
class TemplateRenderer(Application): """ Use json or yaml var_files to render template_file to an adjacent file with the same name but the extension stripped """ VERSION = __version__ overwrite = Flag( ['f', 'force'], help="Overwrite any existing destination file" ) def main(self, template_file: ExistingFile, *var_files: ExistingFile): warn_shebangs(template_file) data = {} # {var: value} var_mentions = defaultdict(list) # {var: [files]} for vfile in var_files: loader = jloads if vfile.suffix.lower() == '.json' else yloads new_data = loader(vfile.read()) for v in new_data: var_mentions[v].append(vfile) data.update(new_data) warn_overrides(var_mentions) rstr = Template(filename=str(template_file), data=data)() dest = template_file.with_suffix('') if not self.overwrite: try: NonexistentPath(dest) except ValueError as e: err(f"{type(e)}: {e}") err("Use -f/--force to overwrite") sys.exit(1) dest.write(rstr)
class EssexLog(ColorApp): """View (all or specified) services' current log files""" lines = SwitchAttr( ['n', 'lines'], argname='LINES', help=( "print only the last LINES lines from the service's current log file, " "or prepend a '+' to start at line LINES" ), default='+1' ) follow = Flag( ['f', 'follow'], help="continue printing new lines as they are added to the log file" ) debug = Flag( ['d', 'debug'], help="view the s6-svscan log file" ) def main(self, *svc_names): logs = [ self.parent.logs_dir / svc.name / 'current' for svc in self.parent.svc_map(svc_names or self.parent.svcs) ] if self.debug: logs.append(self.parent.logs_dir / '.s6-svscan' / 'current') if self.follow: with suppress(KeyboardInterrupt): try: mtail = local.get('lnav', 'multitail') except CommandNotFound: tail[['-n', self.lines, '-F'] + logs].run_fg() else: mtail[logs].run_fg() else: for log in logs: if log.is_file(): tail['-vn', self.lines, log].run_fg() print('\n')
class VWriter(Application): """ Use json or yaml vars files to render each template file found recursively under the working folder, to an adjacent file with the same name but the extension stripped, overriding any vars in ./vars.json in the following order: <root_path>/vars.json, <root_path>/vars.yml, <root_path>/vars.yaml, <template.parent>/vars.json, <template.parent>/vars.yml, <template.parent>/vars.yaml Later entries override earlier ones. """ VERSION = __version__ overwrite = Flag( ['f', 'force'], help="Overwrite any existing destination file" ) template_ext = SwitchAttr( ['t', 'template-ext'], help="Filename extension for templates", default='t' ) vars_name = SwitchAttr( ['n', 'vars-name'], help="Filename (excluding extension) for each vars file", default='vars' ) ext_precedence = ('json', 'yml', 'yaml') def get_vars_files(self, folder: ExistingDirectory): return [ folder / f"{self.vars_name}.{ext}" for ext in self.ext_precedence if (folder / f"{self.vars_name}.{ext}").is_file() ] def main(self, root_path: ExistingDirectory=local.cwd): """root_path defaults to the process's working directory""" root_vfiles = self.get_vars_files(root_path) for tmplt in root_path.walk(lambda f: f.suffix == f".{self.template_ext}"): if tmplt.up() == root_path: vfiles = root_vfiles else: vfiles = (*root_vfiles, *self.get_vars_files(tmplt.up())) TemplateRenderer.invoke(tmplt, *vfiles, overwrite=self.overwrite)
class EssexList(ColorApp): """List all known services""" enabled_only = Flag( ['e', 'enabled'], help="only list enabled services (configured to be running)" ) def main(self): if self.parent.svcs_dir.is_dir(): if self.enabled_only: print(*(s for s in self.parent.svcs if 'down' not in s), sep='\n') else: print(*self.parent.svcs, sep='\n')
class Rawr(Application): VERSION = '0.0.1' adult = Flag(['a', 'adult'], help="Search (only) in the 'adult' category") def main(self, *search_terms): try: results = search(search_terms, adult=self.adult) except KeyboardInterrupt: results = None if not results: raise NoResults with suppress(CommandNotFound): print( f"{local['df']('-h', '-P', '.').splitlines()[-1].split()[3]} available" | yellow) uri = choose_result(results) if not uri: return try: clip(uri) except CommandNotFound: print(uri | blue) else: print("Magnet URI copied to clipboard" | green) show_connection() try: aria2c = local['aria2c'] except CommandNotFound: print( "If I'd found the 'aria2c' command, I'd have offered to launch it for you." | yellow) else: if ask("Begin download with aria2" | magenta, True): # try: aria2c['--seed-time=0', uri] & FG
class EssexNew(ColorApp): """Create a new service""" working_dir = SwitchAttr( ['d', 'working-dir'], local.path, argname='WORKING_DIRECTORY', help=( "run the process from inside this folder; " "the default is SERVICES_DIRECTORY/svc_name" ) ) as_user = SwitchAttr( ['u', 'as-user'], argname='USERNAME', help="non-root user to run the new service as (only works for root)" ) enabled = Flag( ['e', 'enable'], help="enable the new service after creation" ) on_finish = SwitchAttr( ['f', 'finish'], argname='FINISH_CMD', help=( "command to run whenever the supervised process dies " "(must complete in under 5 seconds)" ) ) rotate_at = SwitchAttr( ['r', 'rotate-at'], Range(1, 256), argname='MEBIBYTES', help="archive each log file when it reaches MEBIBYTES mebibytes", default=4 ) prune_at = SwitchAttr( ['p', 'prune-at'], Range(0, 1024), argname='MEBIBYTES', help=( "keep up to MEBIBYTES mebibytes of logs before deleting the oldest; " "0 means never prune" ), default=40 ) on_rotate = SwitchAttr( ['o', 'on-rotate'], argname='PROCESSOR_CMD', help=( "processor command to run when rotating logs; " "receives log via stdin; " "its stdout is archived; " "PROCESSOR_CMD will be double-quoted" ) ) store = SwitchAttr( ['s', 'store'], argname='VARNAME=CMD', help=("run CMD and store its output in env var VARNAME before main cmd is run"), list=True ) # TODO: use skabus-dyntee for socket-logging? maybe def main(self, svc_name, cmd): self.svc = self.parent.svcs_dir / svc_name if self.svc.exists(): fail(1, f"{self.svc} already exists!") self.cmd = cmd if self.as_user and ':' in self.as_user: user, group = self.as_user.split(':', 1) if not user.isnumeric(): user = uid('-u', user).strip() if not group.isnumeric(): group = getent('group', group).split(':')[2] self.as_user = f"{user}:{group}" self.mk_runfile() self.mk_logger() if not self.enabled: (self.svc / 'down').touch() def mk_runfile(self): self.svc.mkdir() runfile = self.svc / 'run' shebang = ('#!/bin/execlineb -P', '') cmd = (self.cmd, "Do the thing") err_to_out = ('fdmove -c 2 1', "Send stderr to stdout") hash_run = ( 'foreground { redirfd -w 1 run.md5 md5sum run }', "Generate hashfile, to detect changes since launch" ) set_user = ( f's6-setuidgid {self.as_user}', "Run as this user" ) if self.as_user else None working_dir = ( f'cd {self.working_dir}', "Enter working directory" ) if self.working_dir else None store_vars = [] for store_var in self.store: var, store_cmd = store_var.split('=', 1) store_vars.append((f'backtick -n {var} {{ {store_cmd} }} importas -u {var} {var}', "Store command output")) runfile.write(columnize_comments(*filter(None, ( shebang, err_to_out, hash_run, set_user, working_dir, *store_vars, cmd )))) runfile.chmod(0o755) if self.on_finish: runfile = self.svc / 'finish' shebang = ('#!/bin/execlineb', '') cmd = (self.on_finish, "Do the thing") runfile.write(columnize_comments(*filter(None, ( shebang, err_to_out, set_user, cmd )))) runfile.chmod(0o755) def mk_logger(self): logger = self.svc / 'log' logger.mkdir() runfile = logger / 'run' shebang = ('#!/bin/execlineb -P', '') hash_run = ( 'foreground { redirfd -w 1 run.md5 md5sum run }', "Generate hashfile, to detect changes since launch" ) receive = ('s6-log', "Receive process output") timestamp = (' T', "Start each line with an ISO 8601 timestamp") rotate = ( f' s{self.rotate_at * 1024 ** 2}', "Archive log when it gets this big (bytes)" ) prune = ( f' S{self.prune_at * 1024 ** 2}', "Purge oldest archived logs when the archive gets this big (bytes)" ) process = ( f'!"{self.on_rotate}"', "Processor (log --stdin--> processor --stdout--> archive)" ) if self.on_rotate else None logfile = (f' {self.parent.logs_dir / self.svc.name}', "Store logs here") runfile.write(columnize_comments(*filter(None, ( shebang, hash_run, receive, timestamp, rotate, prune, process, logfile )))) runfile.chmod(0o755)
class EssexPrint(ColorApp): """View (all or specified) services' run, finish, and log commands""" no_color = Flag( ['n', 'no-color'], help="do not colorize the output (for piping)" ) run_only = Flag( ['r', 'run-only'], help="only print each service's runfile, ignoring any finish, crash, or logger scripts" ) enabled_only = Flag( ['e', 'enabled'], help="only print contents of enabled services (configured to be running)" ) def display(self, docpath): title_cat = tail['-vn', '+1', docpath] if self.no_color: title_cat.run_fg() else: try: ( title_cat | local['highlight'][ '--stdout', '-O', 'truecolor', '-s', 'moria', '-S', 'sh' ] ).run_fg() except CommandNotFound: try: ( title_cat | local['bat']['-p', '-l', 'sh'] ).run_fg() except CommandNotFound: title_cat.run_fg() print('\n') def main(self, *svc_names): errors = False for svc in self.parent.svc_map(svc_names or self.parent.svcs): if self.enabled_only and 'down' in svc: continue found = False for file in ('run',) if self.run_only else ('run', 'finish', 'crash'): # if (runfile := svc / file).is_file(): runfile = svc / file # if runfile.is_file(): # self.display(runfile) found = True # if not self.run_only and (logger := svc / 'log' / 'run').is_file(): logger = svc / 'log' / 'run' # if not self.run_only and logger.is_file(): # self.display(logger) found = True if not found: warn(f"{svc} doesn't exist") errors = True if errors: fail(1)