def make_storage(config, today, quiet=False, verbose=False, logfile=os.devnull): arclist = list() logdate = datetime.date.strftime(today, r'%y%m%d') dskpath = config['Path']['dskdir'] tarpath = os.path.join(config['Path']['logdir'], 'tarfile') if not os.path.isdir(dskpath): return arclist if not os.path.isdir(tarpath): return arclist text = f'Storing ancient archives at external hard disk {under}{dskpath}{reset}' print_info(text, logfile, redirect=quiet) days = calendar.monthrange(year=today.year, month=today.month)[1] ctime = datetime.datetime.fromtimestamp(os.stat(tarpath).st_birthtime) if (today - ctime) > datetime.timedelta(days=days): glob_list = glob.glob(os.path.join(tarpath, '*/*.tar.xz')) if glob_list: with tempfile.TemporaryDirectory() as tmppath: arcdate = datetime.date.strftime(ctime, r'%y%m%d') tarname = os.path.join(tmppath, f'{arcdate}-{logdate}.tar.bz') if verbose: print_scpt(f'tar -cjf {tarname} {tarpath}', logfile, redirect=quiet) else: print_scpt(f'tar -cjvf {tarname} {tarpath}', logfile, redirect=verbose) with tarfile.open(tarname, 'w:bz2') as bz: for absname in glob_list: arcname = pathlib.Path(absname).relative_to(tarpath) bz.add(absname, arcname) arclist.append(absname) print_text(absname, logfile, redirect=verbose) arcfile = os.path.join(config['Path']['arcdir'], 'archive.zip') if verbose: print_scpt(f'tar -cZf {arcfile} {tarname}', logfile, redirect=quiet) else: print_scpt(f'tar -cZvf {arcfile} {tarname}', logfile, redirect=verbose) with zipfile.ZipFile(arcfile, 'a', zipfile.ZIP_DEFLATED) as zf: arcname = os.path.split(tarname)[1] zf.write(tarname, arcname) print_text(tarname, logfile, redirect=verbose) shutil.rmtree(tarpath) return arclist
def launch_askpass(password=None, quiet=False, verbose=False, logfile=os.devnull, *args, **kwargs): # pylint: disable=unused-argument,keyword-arg-before-vararg text = 'Launching MacDaily SSH-AskPass program' print_info(text, logfile, quiet) path = f'Macintosh HD{ROOT.replace(os.path.sep, ":")}:img:askpass.icns' ASKPASS = ['#!/usr/bin/env osascript', '', '-- script based on https://github.com/theseal/ssh-askpass', '', 'on run argv', ' set args to argv as text', ' if args starts with "--help" or args starts with "-h" then', ' return "macdaily-askpass [-h|--help] [prompt]"', ' end if', f' display dialog args with icon file ("{path}") default button "OK" default answer "" with hidden answer', # pylint: disable=line-too-long " return result's text returned", 'end run', ''] askpass = os.path.join(ROOT, 'res', 'askpass.applescript') text = f'Making executable {askpass!r}' print_misc(text, logfile, verbose) user = owner = getpass.getuser() if os.path.isfile(askpass): owner = pwd.getpwuid(os.stat(askpass).st_uid).pw_name if user != owner: run_script(['chown', user, askpass], quiet, verbose, sudo=True, password=password, logfile=logfile) else: try: pathlib.Path(askpass).touch() except PermissionError: owner = 'root' run_script(['touch', askpass], quiet, verbose, sudo=True, password=password, logfile=logfile) run_script(['chown', user, askpass], quiet, verbose, sudo=True, password=password, logfile=logfile) with open(askpass, 'w') as file: file.write(os.linesep.join(ASKPASS)) if user != owner: run_script(['chmod', '+x', askpass], quiet, verbose, sudo=True, password=password, logfile=logfile) run_script(['chown', owner, askpass], quiet, verbose, sudo=True, password=password, logfile=logfile) else: run_script(['chmod', '+x', askpass], quiet, verbose, logfile=logfile) return askpass
def launch_confirm(password=None, quiet=False, verbose=False, logfile=os.devnull, *args, **kwargs): # pylint: disable=unused-argument,keyword-arg-before-vararg text = 'Launching MacDaily Confirmation program' print_info(text, logfile, quiet) path = f'Macintosh HD{ROOT.replace(os.path.sep, ":")}:img:confirm.icns' ASKPASS = ['#!/usr/bin/env osascript', '', 'on run argv', ' set args to argv as text', ' if args starts with "--help" or args starts with "-h" then', ' return "macdaily-confirm [-h|--help] [prompt]"', ' end if', f' display dialog args with icon file ("{path}") default button "Cancel"', " return result's button returned", 'end run', ''] confirm = os.path.join(ROOT, 'res', 'confirm.applescript') text = f'Making executable {confirm!r}' print_misc(text, logfile, verbose) user = owner = getpass.getuser() if os.path.isfile(confirm): owner = pwd.getpwuid(os.stat(confirm).st_uid).pw_name if user != owner: run_script(['chown', user, confirm], quiet, verbose, sudo=True, password=password, logfile=logfile) else: try: pathlib.Path(confirm).touch() except PermissionError: owner = 'root' run_script(['touch', confirm], quiet, verbose, sudo=True, password=password, logfile=logfile) run_script(['chown', user, confirm], quiet, verbose, sudo=True, password=password, logfile=logfile) with open(confirm, 'w') as file: file.write(os.linesep.join(ASKPASS)) if user != owner: run_script(['chmod', '+x', confirm], quiet, verbose, sudo=True, password=password, logfile=logfile) run_script(['chown', owner, confirm], quiet, verbose, sudo=True, password=password, logfile=logfile) else: run_script(['chmod', '+x', confirm], quiet, verbose, logfile=logfile) return confirm
def _move(root, stem): arch = os.path.join(self._disk_dir, stem) pathlib.Path(arch).mkdir(parents=True, exist_ok=True) file_list = list() for name in os.listdir(root): path = os.path.join(root, name) if os.path.isdir(path): if name != 'Cask': file_list.extend( _move(path, os.path.join(stem, name))) elif os.path.splitext(name)[ 1] != '.incomplete' and path not in cask_list: # pylint: disable=cell-var-from-loop try: shutil.move(path, os.path.join(arch, name)) except (shutil.Error, FileExistsError): os.remove(path) # with contextlib.suppress(shutil.Error, FileExistsError): # shutil.move(path, os.path.join(arch, name)) file_list.append(path) return file_list
def __init__( self, namespace, filename, timeout, confirm, # pylint: disable=super-init-not-called askpass, password, disk_dir, brew_renew=None): self._qflag = namespace.get('quiet', False) self._vflag = self._qflag or (not namespace.get('verbose', False)) text = f'Running {self.cmd} command for {self.mode}' print_info(text, filename, redirect=self._qflag) # assign members self._file = filename self._timeout = timeout self._confirm = confirm self._askpass = askpass self._password = password self._disk_dir = disk_dir self._brew_renew = brew_renew self._logroot = str(pathlib.Path(filename).resolve().parents[1]) # exit if no executable found if self._check_exec(): # mainloop process self._pkg_args(namespace) self._loc_exec() self._run_proc() # remove temp vars [ delattr(self, attr) for attr in filter(lambda s: s.startswith('_var_'), dir(self)) ] # pylint: disable=expression-not-assigned
def install(argv=None): # parse args & set context redirection flags args = parse_args(argv) quiet = args.quiet verbose = (args.quiet or not args.verbose) # parse config & change environ config = parse_config(quiet, verbose) os.environ['SUDO_ASKPASS'] = config['Miscellaneous']['askpass'] # fetch current time today = datetime.datetime.today() logdate = datetime.date.strftime(today, r'%y%m%d') logtime = datetime.date.strftime(today, r'%H%M%S') # mkdir for logs logpath = pathlib.Path( os.path.join(config['Path']['logdir'], 'install', logdate)) logpath.mkdir(parents=True, exist_ok=True) # prepare command paras filename = os.path.join(logpath, f'{logtime}-{uuid.uuid4()!s}.log') os.environ['MACDAILY_LOGFILE'] = filename confirm = config['Miscellaneous']['confirm'] askpass = config['Miscellaneous']['askpass'] timeout = config['Miscellaneous']['limit'] disk_dir = config['Path']['arcdir'] brew_renew = None # record program status text = f'{bold}{green}|🚨|{reset} {bold}Running MacDaily version {__version__}{reset}' print_term(text, filename, redirect=quiet) record(filename, args, today, config, redirect=verbose) # ask for password text = f'{bold}{purple}|🔑|{reset} {bold}Your {under}sudo{reset}{bold} password may be necessary{reset}' print_term(text, filename, redirect=quiet) password = get_pass(askpass) cmd_list = list() for mode in {'apm', 'brew', 'cask', 'gem', 'mas', 'npm', 'pip', 'system'}: # skip disabled commands if not config['Mode'].get(mode, False): text = f'macdaily-install: {yellow}{mode}{reset}: command disabled' print_term(text, filename, redirect=verbose) continue # skip commands with no package spec packages = getattr(args, f'{mode}_pkgs', list()) namespace = getattr(args, mode, None) if not (packages or namespace): text = f'macdaily-install: {yellow}{mode}{reset}: nothing to install' print_term(text, filename, redirect=verbose) continue # update package specifications if namespace is None: namespace = dict(vars(args), packages=list()) namespace['packages'].extend(packages) # check master controlling flags if args.yes: namespace['yes'] = True if args.quiet: namespace['quiet'] = True if args.verbose: namespace['verbose'] = True if args.no_cleanup: namespace['no_cleanup'] = True # run command cmd_cls = globals()[f'{mode.capitalize()}Install'] command = cmd_cls(make_namespace(namespace), filename, timeout, confirm, askpass, password, disk_dir, brew_renew) # record command cmd_list.append(command) brew_renew = command.time archive = None if not args.no_cleanup: archive = make_archive(config, 'install', today, quiet=quiet, verbose=verbose, logfile=filename) text = f'{bold}{green}|📖|{reset} {bold}MacDaily report of install command{reset}' print_term(text, filename, redirect=quiet) for command in cmd_list: desc = make_description(command) pkgs = f'{reset}{bold}, {green}'.join(command.packages) fail = f'{reset}{bold}, {red}'.join(command.failed) if pkgs: flag = (len(pkgs) == 1) text = f'Installed following {under}{desc(flag)}{reset}{bold}: {green}{pkgs}{reset}' print_misc(text, filename, redirect=quiet) else: text = f'No {under}{desc(False)}{reset}{bold} installed' print_misc(text, filename, redirect=quiet) if fail: flag = (len(fail) == 1) text = f'Installation of following {under}{desc(flag)}{reset}{bold} failed: {red}{fail}{reset}' print_misc(text, filename, redirect=quiet) else: verb, noun = ('s', '') if len(fail) == 1 else ('', 's') text = f'All {under}{desc(False)}{reset}{bold} installation{noun} succeed{verb}' print_misc(text, filename, redirect=verbose) if archive: formatted_list = f'{reset}{bold}, {under}'.join(archive) text = ( f'Archived following ancient logs: {under}{formatted_list}{reset}') print_misc(text, filename, redirect=quiet) if len(cmd_list) == 0: # pylint: disable=len-as-condition text = f'macdaily: {purple}install{reset}: no packages installed' print_term(text, filename, redirect=quiet) if args.show_log: try: subprocess.check_call([ 'open', '-a', '/Applications/Utilities/Console.app', filename ]) except subprocess.CalledProcessError: print_text(traceback.format_exc(), filename, redirect=verbose) print( f'macdaily: {red}install{reset}: cannot show log file {filename!r}', file=sys.stderr) mode_lst = [command.mode for command in cmd_list] mode_str = ', '.join(mode_lst) if mode_lst else 'none' text = ( f'{bold}{green}|🍺|{reset} {bold}MacDaily successfully performed install process ' f'for {mode_str} package managers{reset}') print_term(text, filename, redirect=quiet)
def _proc_cleanup(self): if self._no_cleanup: return text = 'Pruning caches and archives' print_info(text, self._file, redirect=self._qflag) if not os.path.isdir(self._disk_dir): text = ( f'macdaily-{self.cmd}: {yellow}cask{reset}: ' f'archive directory {bold}{self._disk_dir}{reset} not found') print_term(text, self._file, redirect=self._vflag) return argv = ['brew', 'cask', 'cleanup'] if self._verbose: argv.append('--verbose') if self._quiet: argv.append('--quiet') print_scpt(' '.join(argv), self._file, redirect=self._qflag) path_cask = os.path.join(self._disk_dir, 'Homebrew', 'Cask') path_down = os.path.join(self._disk_dir, 'Homebrew', 'download') pathlib.Path(path_cask).mkdir(parents=True, exist_ok=True) pathlib.Path(path_down).mkdir(parents=True, exist_ok=True) for path in self._exec: argv = [path, '--cache'] args = ' '.join(argv) print_scpt(args, self._file, redirect=self._vflag) with open(self._file, 'a') as file: file.write(f'Script started on {date()}\n') file.write(f'command: {args!r}\n') fail = False try: proc = subprocess.check_output(argv, stderr=make_stderr(self._vflag)) except subprocess.CalledProcessError: print_text(traceback.format_exc(), self._file, redirect=self._vflag) fail = True else: context = proc.decode() print_text(context, self._file, redirect=self._vflag) finally: with open(self._file, 'a') as file: file.write(f'Script done on {date()}\n') if fail: continue cache = context.strip() if os.path.isdir(cache): argv = [path, 'cask', 'caches', 'archive'] if self._verbose: argv.append('--verbose') if self._quiet: argv.append('--quiet') print_scpt(' '.join(argv), self._file, redirect=self._qflag) file_list = list() link_list = glob.glob(os.path.join(cache, 'Cask/*')) cask_list = [os.path.realpath(name) for name in link_list] for link in link_list: file_list.append(link) try: shutil.move(link, path_cask) except (shutil.Error, FileExistsError): os.remove(link) for cask in filter( lambda p: os.path.splitext(p)[1] != '.incomplete', cask_list): try: shutil.move(cask, path_down) except (shutil.Error, FileExistsError): os.remove(cask) file_list.append(cask) print_text(os.linesep.join(sorted(file_list)), self._file, redirect=self._vflag)
def launch_daemons(config, password, quiet=False, verbose=False, logfile=os.devnull): text = 'Launching MacDaily LaunchAgent program' print_info(text, logfile, quiet) def make_daemon(mode, argv): DAEMON = ['#!/usr/bin/env osascript', '', '-- show notification', f'display notification "Running scheduled {mode} scripts..." with title "MacDaily"', '', '-- run script', f'do shell script "{PYTHON} -m macdaily {mode} {argv}"', ''] return os.linesep.join(DAEMON) # Property List osascript = shutil.which('osascript') PLIST_BASE = collections.OrderedDict( Label=str(), UserName=getpass.getuser(), Program=osascript, ProgramArguments=[osascript, ''], # RunAtLoad=True, RootDirectory=str(pathlib.Path.home()), EnvironmentVariables=dict(os.environ), StartCalendarInterval=list(), StandardOutPath=str(), StandardErrorPath=str(), ) library = os.path.expanduser('~/Library/LaunchAgents/') os.makedirs(library, exist_ok=True) root = pathlib.Path(config['Path']['logdir']) for mode, time in config['Daemon'].items(): (root / mode).mkdir(parents=True, exist_ok=True) name = f'com.macdaily.{mode}' path = os.path.join(ROOT, 'res', f'daemon-{mode}.applescript') pout = str(root / mode / 'stdout.log') perr = str(root / mode / 'stderr.log') argv = ' '.join(config['Command'].get(mode, ['--help'])) text = f'Adding {under}{mode}{reset}{bold} command LaunchAgent {name!r}' print_misc(text, logfile, quiet) user = owner = getpass.getuser() if os.path.isfile(path): owner = pwd.getpwuid(os.stat(path).st_uid).pw_name if user != owner: run_script(['chown', user, path], quiet, verbose, sudo=True, password=password, logfile=logfile) else: try: pathlib.Path(path).touch() except PermissionError: owner = 'root' run_script(['touch', path], quiet, verbose, sudo=True, password=password, logfile=logfile) run_script(['chown', user, path], quiet, verbose, sudo=True, password=password, logfile=logfile) with open(path, 'w') as file: file.write(make_daemon(mode, argv)) run_script(['chmod', '+x', path], quiet, verbose, sudo=True, password=password, logfile=logfile) if user != owner: run_script(['chown', owner, path], quiet, verbose, sudo=True, password=password, logfile=logfile) PLIST = copy.copy(PLIST_BASE) PLIST['Label'] = name PLIST['ProgramArguments'][1] = path PLIST['StartCalendarInterval'] = time PLIST['StandardOutPath'] = pout PLIST['StandardErrorPath'] = perr plist = os.path.join(library, f'{name}.plist') text = f'Adding Launch Agent {name!r}' print_misc(text, logfile, verbose) if os.path.exists(plist): run_script(['launchctl', 'unload', '-w', plist], quiet, verbose, logfile=logfile) with open(plist, 'wb') as file: plistlib.dump(PLIST, file, sort_keys=False) run_script(['launchctl', 'load', '-w', plist], quiet, verbose, logfile=logfile) return library
def make_archive(config, mode, today, zipfile=True, quiet=False, verbose=False, logfile=os.devnull): # pylint: disable=redefined-outer-name logdir = config['Path']['logdir'] logdate = datetime.date.strftime(today, r'%y%m%d') logpath = pathlib.Path(os.path.join(logdir, mode)) arcpath = pathlib.Path(os.path.join(logdir, 'arcfile', mode)) tarpath = pathlib.Path(os.path.join(logdir, 'tarfile', mode)) logpath.mkdir(parents=True, exist_ok=True) arcpath.mkdir(parents=True, exist_ok=True) tarpath.mkdir(parents=True, exist_ok=True) text = f'Moving ancient logs into {under}GNU Zip Archives{reset}' print_info(text, logfile, redirect=quiet) filelist = list() for subdir in filter( lambda subdir: os.path.isdir(os.path.join(logpath, subdir)), os.listdir(logpath)): if subdir == logdate: continue absdir = os.path.abspath(os.path.join(logpath, subdir)) glob_list = glob.glob(os.path.join(absdir, '*.log')) if glob_list: tarname = os.path.join(arcpath, f'{subdir}.tar.gz') if verbose: print_scpt(f'tar -czf {tarname} {absdir}', logfile, redirect=quiet) else: print_scpt(f'tar -czvf {tarname} {absdir}', logfile, redirect=verbose) with tarfile.open(tarname, 'w:gz') as gz: for absname in glob_list: arcname = os.path.split(absname)[1] gz.add(absname, arcname) filelist.append(absname) print_text(absname, logfile, redirect=verbose) shutil.rmtree(absdir) misc_path = os.path.join(get_logdir(), 'misc') if os.path.isdir(misc_path): this_version = distutils.version.LooseVersion(VERSION) # pylint: disable=no-member for entry in os.scandir(misc_path): if not entry.is_dir: continue try: that_version = distutils.version.LooseVersion(entry.name) # pylint: disable=no-member if this_version <= that_version: continue except ValueError: pass glob_list = glob.glob(os.path.join(entry.path, '*.log')) if glob_list: tarname = os.path.join(arcpath, f'{entry.name}.tar.gz') with tarfile.open(tarname, 'w:gz') as gz: for absname in glob_list: arcname = os.path.split(absname)[1] gz.add(absname, arcname) shutil.rmtree(entry.path) text = f'Moving ancient archives into {under}XZ Compressed Archives{reset}' print_info(text, logfile, redirect=quiet) ctime = datetime.datetime.fromtimestamp(os.stat(arcpath).st_birthtime) if (today - ctime) > datetime.timedelta(days=7): glob_list = glob.glob(os.path.join(arcpath, '*.tar.gz')) if glob_list: arcdate = datetime.date.strftime(ctime, r'%y%m%d') tarname = os.path.join(tarpath, f'{arcdate}-{logdate}.tar.xz') if verbose: print_scpt(f'tar -cJf {tarname} {arcpath}', logfile, redirect=quiet) else: print_scpt(f'tar -cJvf {tarname} {arcpath}', logfile, redirect=verbose) with tarfile.open(tarname, 'w:xz') as xz: for absname in glob_list: arcname = os.path.split(absname)[1] xz.add(absname, arcname) filelist.append(absname) print_text(absname, logfile, redirect=verbose) shutil.rmtree(arcpath) if zipfile: # pylint:disable=redefined-outer-name filelist.extend(make_storage(config, today, quiet, verbose, logfile)) return filelist
def launch(argv=None): # parse args & set context redirection flags args = parse_args(argv) quiet = args.quiet verbose = (args.quiet or not args.verbose) # parse config & change environ config = parse_config(quiet, verbose) os.environ['SUDO_ASKPASS'] = config['Miscellaneous']['askpass'] # fetch current time today = datetime.datetime.today() logdate = datetime.date.strftime(today, r'%y%m%d') logtime = datetime.date.strftime(today, r'%H%M%S') # mkdir for logs logpath = pathlib.Path(os.path.join(config['Path']['logdir'], 'launch', logdate)) logpath.mkdir(parents=True, exist_ok=True) # prepare command paras filename = os.path.join(logpath, f'{logtime}-{uuid.uuid4()!s}.log') os.environ['MACDAILY_LOGFILE'] = filename askpass = config['Miscellaneous']['askpass'] # record program status text = f'{bold}{green}|🚨|{reset} {bold}Running MacDaily version {__version__}{reset}' print_term(text, filename, redirect=quiet) record(filename, args, today, config, redirect=verbose) # ask for password text = f'{bold}{purple}|🔑|{reset} {bold}Your {under}sudo{reset}{bold} password may be necessary{reset}' print_term(text, filename, redirect=quiet) password = get_pass(askpass) # update program list if args.all: args.program.extend(('askpass', 'confirm', 'daemons')) prog_dict = dict() for program in set(args.program): if re.match(r'^(askpass|confirm|daemons)$', program, re.IGNORECASE) is None: parser = get_launch_parser() parser.error(f"argument PROG: invalid choice: {program!r} (choose from 'askpass', 'confirm', 'daemons')") # launch program launch_func = globals()[f'launch_{program.lower()}'] path = launch_func(quiet=quiet, verbose=verbose, config=config, password=password, logfile=filename) # record program prog_dict[program] = path archive = None if not args.no_cleanup: archive = make_archive(config, 'launch', today, quiet=quiet, verbose=verbose, logfile=filename) text = f'{bold}{green}|📖|{reset} {bold}MacDaily report of launch command{reset}' print_term(text, filename, redirect=quiet) for prog, path in prog_dict.items(): text = f'Launched helper program {under}{prog}{reset}{bold} at {under}{path}{reset}' print_misc(text, filename, redirect=quiet) if archive: formatted_list = f'{reset}{bold}, {under}'.join(archive) text = (f'Archived following ancient logs: {under}{formatted_list}{reset}') print_misc(text, filename, redirect=quiet) if len(prog_dict) == 0: text = f'macdaily: {purple}launch{reset}: no program launched' print_term(text, filename, redirect=quiet) if args.show_log: try: subprocess.check_call(['open', '-a', '/Applications/Utilities/Console.app', filename]) except subprocess.CalledProcessError: print_text(traceback.format_exc(), filename, redirect=verbose) print(f'macdaily: {red}launch{reset}: cannot show log file {filename!r}', file=sys.stderr) mode_str = ', '.join(prog_dict) if prog_dict else 'none' text = (f'{bold}{green}|🍺|{reset} {bold}MacDaily successfully performed launch process ' f'for {mode_str} helper programs{reset}') print_term(text, filename, redirect=quiet)
os.getenv('MACDAILY_DEVMODE', 'false').strip().lower(), False) ########################################################### # Miscellaneous Constants ########################################################### # ansi escape pattern # from http://www.umich.edu/~archive/apple2/misc/programmers/vt100.codes.txt ANSI = r'([\x1B\x9B][\[\]\(\)#;?]*(?:(?:(?:[a-zA-Z0-9]*(?:;[-a-zA-Z0-9\\/#&.:=?%@~_]*)*)?\x07)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-PR-TZcf-ntqry=><~])))' # pylint: disable=line-too-long # script utilities SCRIPT = shutil.which('script') UNBUFFER = shutil.which('unbuffer') # environment macros ROOT = str(pathlib.Path(__file__).resolve().parents[2]) SHELL = os.getenv('SHELL', shutil.which('sh')) # user information USR = getpass.getuser() PWD = pwd.getpwnam(USR) USER = PWD.pw_gecos PASS = os.getenv('SUDO_PASSWORD', PWD.pw_passwd) # emoji mappings ORIG_BEER = '�' RESP_BEER = 'ð\x9f\x8dº' # homebrew aliases NODE = {'node', 'node.js', 'node@11', 'nodejs', 'npm'} PYTHON2 = {'python@2', 'python2'}
def logging(argv=None): # parse args & set context redirection flags args = parse_args(argv) quiet = args.quiet verbose = (args.quiet or not args.verbose) # parse config & change environ config = parse_config(quiet, verbose) os.environ['SUDO_ASKPASS'] = config['Miscellaneous']['askpass'] # fetch current time today = datetime.datetime.today() logdate = datetime.date.strftime(today, r'%y%m%d') logtime = datetime.date.strftime(today, r'%H%M%S') # prepare command paras confirm = config['Miscellaneous']['confirm'] askpass = config['Miscellaneous']['askpass'] timeout = config['Miscellaneous']['limit'] disk_dir = config['Path']['arcdir'] brew_renew = None # record program status text_record = f'{bold}{green}|🚨|{reset} {bold}Running MacDaily version {__version__}{reset}' print_term(text_record, get_logfile(), redirect=quiet) record(get_logfile(), args, today, config, redirect=verbose) # ask for password text_askpass = f'{bold}{purple}|🔑|{reset} {bold}Your {under}sudo{reset}{bold} password may be necessary{reset}' print_term(text_askpass, get_logfile(), redirect=quiet) password = get_pass(askpass) cmd_list = list() log_list = list() file_list = list() for mode in { 'apm', 'app', 'brew', 'cask', 'gem', 'mas', 'npm', 'pip', 'tap' }: # mkdir for logs logpath = pathlib.Path( os.path.join(config['Path']['logdir'], 'logging', mode, logdate)) logpath.mkdir(parents=True, exist_ok=True) filename = os.path.join(logpath, f'{logtime}-{uuid.uuid4()!s}.log') os.environ['MACDAILY_LOGFILE'] = filename # redo program status records print_term(text_record, filename, redirect=True) record(filename, args, today, config, redirect=True) print_term(text_askpass, filename, redirect=True) # skip disabled commands if (not config['Mode'].get(mode, False)) or getattr( args, f'no_{mode}', False): text = f'macdaily-logging: {yellow}{mode}{reset}: command disabled' print_term(text, filename, redirect=verbose) continue # update logging specifications namespace = getattr(args, mode, vars(args)) # check master controlling flags if args.quiet: namespace['quiet'] = True if args.verbose: namespace['verbose'] = True if args.show_log: namespace['show_log'] = True if args.no_cleanup: namespace['no_cleanup'] = True # run command cmd_cls = globals()[f'{mode.capitalize()}Logging'] command = cmd_cls(make_namespace(namespace), filename, timeout, confirm, askpass, password, disk_dir, brew_renew) # record command cmd_list.append(command) log_list.append(filename) brew_renew = command.time if not namespace['no_cleanup']: archive = make_archive(config, f'logging/{mode}', today, zipfile=False, quiet=quiet, verbose=verbose, logfile=filename) file_list.extend(archive) if namespace.get('show_log', False): try: subprocess.check_call([ 'open', '-a', '/Applications/Utilities/Console.app', filename ]) except subprocess.CalledProcessError: print_text(traceback.format_exc(), filename, redirect=verbose) print( f'macdaily: {red}logging{reset}: cannot show log file {filename!r}', file=sys.stderr) if not args.no_cleanup: storage = make_storage(config, today, quiet=quiet, verbose=verbose, logfile=filename) file_list.extend(storage) text = f'{bold}{green}|📖|{reset} {bold}MacDaily report of logging command{reset}' print_term(text, get_logfile(), redirect=quiet) for file in log_list: print_term(text, file, redirect=True) for command in cmd_list: text = f'Recorded existing {under}{command.desc[1]}{reset}{bold} at {under}{command.sample}{reset}' print_misc(text, get_logfile(), redirect=quiet) for file in log_list: print_misc(text, file, redirect=True) if file_list: formatted_list = f'{reset}{bold}, {under}'.join(file_list) text = ( f'Archived following ancient logs: {under}{formatted_list}{reset}') print_misc(text, filename, redirect=quiet) if len(cmd_list) == 0: # pylint: disable=len-as-condition text = f'macdaily: {purple}logging{reset}: no packages recorded' print_term(text, get_logfile(), redirect=quiet) for file in log_list: print_term(text, file, redirect=True) mode_lst = [command.mode for command in cmd_list] mode_str = ', '.join(mode_lst) if mode_lst else 'none' text = ( f'{bold}{green}|🍺|{reset} {bold}MacDaily successfully performed logging process ' f'for {mode_str} package managers{reset}') print_term(text, get_logfile(), redirect=quiet) for file in log_list: print_term(text, file, redirect=True)
def archive(argv=None): # parse args & set context redirection flags args = parse_args(argv) quiet = args.quiet verbose = (args.quiet or not args.verbose) # parse config & change environ config = parse_config(quiet, verbose) os.environ['SUDO_ASKPASS'] = config['Miscellaneous']['askpass'] # fetch current time today = datetime.datetime.today() logdate = datetime.date.strftime(today, r'%y%m%d') logtime = datetime.date.strftime(today, r'%H%M%S') # mkdir for logs logpath = pathlib.Path( os.path.join(config['Path']['logdir'], 'archive', logdate)) logpath.mkdir(parents=True, exist_ok=True) # prepare command paras filename = os.path.join(logpath, f'{logtime}-{uuid.uuid4()!s}.log') os.environ['MACDAILY_LOGFILE'] = filename disk_dir = config['Path']['arcdir'] # record program status text = f'{bold}{green}|🚨|{reset} {bold}Running MacDaily version {__version__}{reset}' print_term(text, filename, redirect=quiet) record(filename, args, today, config, redirect=verbose) log_pth = { 'logging/apm', 'logging/app', 'logging/brew', 'logging/cask', 'logging/gem', 'logging/mas', 'logging/npm', 'logging/pip', 'logging/tap' } all_pth = log_pth | { 'archive', 'cleanup', 'dependency', 'postinstall', 'reinstall', 'tarfile', 'uninstall', 'update' } # update path list if args.all: args.path.extend(all_pth) if 'logging' in args.path: args.path.remove('logging') args.path.extend(log_pth) file_dict = dict() for mode in set(args.path): if mode not in all_pth: tmp_pth = all_pth.add('logging') with open(filename, 'a') as file: file.write(f'macdaily: archive: invalid option: {mode!r}') parser = get_archive_parser() parser.error( f"argument CMD: invalid choice: {mode!r} (choose from {', '.join(sorted(tmp_pth))})" ) # make archives file_list = make_archive(config, mode, today, zipfile=False, quiet=quiet, verbose=verbose) # record file list file_dict[mode] = copy.copy(file_list) storage = None if not args.no_storage: storage = make_storage(config, today, quiet=quiet, verbose=verbose) text = f'{bold}{green}|📖|{reset} {bold}MacDaily report of archive command{reset}' print_term(text, filename, redirect=quiet) for mode, file_list in file_dict.items(): if file_list: formatted_list = f'{reset}{bold}, {under}'.join(file_list) text = f'Archived following ancient logs of {pink}{mode}{reset}{bold}: {under}{formatted_list}{reset}' else: text = f'No ancient logs of {under}{mode}{reset}{bold} archived' print_misc(text, filename, redirect=quiet) if len(file_dict) == 0: # pylint: disable=len-as-condition text = f'macdaily: {purple}archive{reset}: no logs archived' print_term(text, filename, redirect=quiet) if storage: formatted_list = f'{reset}{bold}, {under}'.join(storage) text = ( f'Stored following ancient archived into {pink}external hard disk{reset} ' f'{bold} at {under}{disk_dir}{reset}{bold}: {under}{formatted_list}{reset}' ) print_misc(text, filename, redirect=quiet) if args.show_log: try: subprocess.check_call([ 'open', '-a', '/Applications/Utilities/Console.app', filename ]) except subprocess.CalledProcessError: print_text(traceback.format_exc(), filename, redirect=verbose) print( f'macdaily: {red}archive{reset}: cannot show log file {filename!r}', file=sys.stderr) mode_str = ', '.join(file_dict) if file_dict else 'none' text = ( f'{bold}{green}|🍺|{reset} {bold}MacDaily successfully performed archive process ' f'for {mode_str} helper programs{reset}') print_term(text, filename, redirect=quiet)
def postinstall(argv=None): # parse args & set context redirection flags args = parse_args(argv) quiet = args.quiet verbose = (args.quiet or not args.verbose) # parse config & change environ config = parse_config(quiet, verbose) os.environ['SUDO_ASKPASS'] = config['Miscellaneous']['askpass'] # fetch current time today = datetime.datetime.today() logdate = datetime.date.strftime(today, r'%y%m%d') logtime = datetime.date.strftime(today, r'%H%M%S') # mkdir for logs logpath = pathlib.Path( os.path.join(config['Path']['logdir'], 'postinstall', logdate)) logpath.mkdir(parents=True, exist_ok=True) # prepare command paras filename = os.path.join(logpath, f'{logtime}-{uuid.uuid4()!s}.log') os.environ['MACDAILY_LOGFILE'] = filename confirm = config['Miscellaneous']['confirm'] askpass = config['Miscellaneous']['askpass'] timeout = config['Miscellaneous']['limit'] disk_dir = config['Path']['arcdir'] brew_renew = None # record program status text = f'{bold}{green}|🚨|{reset} {bold}Running MacDaily version {__version__}{reset}' print_term(text, filename, redirect=quiet) record(filename, args, today, config, redirect=verbose) # ask for password text = f'{bold}{purple}|🔑|{reset} {bold}Your {under}sudo{reset}{bold} password may be necessary{reset}' print_term(text, filename, redirect=quiet) password = get_pass(askpass) # check if disabled enabled = (not config['Mode'].get('brew', False)) # run command if enabled: command = PostinstallCommand(make_namespace(args), filename, timeout, confirm, askpass, password, disk_dir, brew_renew) else: text = f'macdaily-postinstall: {yellow}brew{reset}: command disabled' print_term(text, filename, redirect=verbose) archive = None if not args.no_cleanup: archive = make_archive(config, 'postinstall', today, quiet=quiet, verbose=verbose, logfile=filename) text = f'{bold}{green}|📖|{reset} {bold}MacDaily report of postinstall command{reset}' print_term(text, filename, redirect=quiet) if enabled: desc = make_description(command) pkgs = f'{reset}{bold}, {green}'.join(command.packages) miss = f'{reset}{bold}, {yellow}'.join(command.notfound) ilst = f'{reset}{bold}, {pink}'.join(command.ignored) fail = f'{reset}{bold}, {red}'.join(command.failed) if pkgs: flag = (len(pkgs) == 1) text = f'Postinstalled following {under}{desc(flag)}{reset}{bold}: {green}{pkgs}{reset}' print_misc(text, filename, redirect=quiet) else: text = f'No {under}{desc(False)}{reset}{bold} postinstalled' print_misc(text, filename, redirect=quiet) if fail: flag = (len(fail) == 1) text = f'Postinstallation of following {under}{desc(flag)}{reset}{bold} failed: {red}{fail}{reset}' print_misc(text, filename, redirect=quiet) else: verb, noun = ('s', '') if len(fail) == 1 else ('', 's') text = f'All {under}{desc(False)}{reset}{bold} postinstallation{noun} succeed{verb}' print_misc(text, filename, redirect=verbose) if ilst: flag = (len(ilst) == 1) text = f'Ignored postinstallation of following {under}{desc(flag)}{reset}{bold}: {pink}{ilst}{reset}' print_misc(text, filename, redirect=quiet) else: text = f'No {under}{desc(False)}{reset}{bold} ignored' print_misc(text, filename, redirect=verbose) if miss: flag = (len(miss) == 1) text = f'Following {under}{desc(flag)}{reset}{bold} not found: {yellow}{miss}{reset}' print_misc(text, filename, redirect=quiet) else: text = f'Hit all {under}{desc(False)}{reset}{bold} specifications' print_misc(text, filename, redirect=verbose) if archive: formatted_list = f'{reset}{bold}, {under}'.join(archive) text = ( f'Archived following ancient logs: {under}{formatted_list}{reset}') print_misc(text, filename, redirect=quiet) if not enabled: text = f'macdaily: {purple}postinstall{reset}: no Homebrew formulae postinstalled' print_term(text, filename, redirect=quiet) if args.show_log: try: subprocess.check_call([ 'open', '-a', '/Applications/Utilities/Console.app', filename ]) except subprocess.CalledProcessError: print_text(traceback.format_exc(), filename, redirect=verbose) print( f'macdaily: {red}postinstall{reset}: cannot show log file {filename!r}', file=sys.stderr) text = ( f'{bold}{green}|🍺|{reset} {bold}MacDaily successfully performed postinstall process{reset}' ) print_term(text, filename, redirect=quiet)