#!/usr/bin/env python3 # Distributed under the terms of the GNU General Public License v2 # The original bash version of autorun was developed by Pierre Dorgueil in 2003 # The current python implementation has been developed by Francois Dupoux in 2008 # # ----------------------- changeslog: ----------------------------------------- # 2003-10-01: Pierre Dorgueil --> original bash version of autorun for sysrescue # 2008-01-26: Francois Dupoux --> rewrote autorun in python to support http # 2008-01-27: Francois Dupoux --> added 'ar_ignorefail', 'ar_nodel', 'ar_disable' # 2017-05-30: Gernot Fink --> ported the script from python2 to python3 # 2021-07-07: Alexander Mahr --> added 'ar_attempts' # 2022-01-09: Francois Dupoux --> added support for yaml configuration # 2022-01-09: Francois Dupoux --> option 'autoruns=' renamed 'ar_suffixes=' # 2022-01-23: Francois Dupoux --> use the generated effective configuration file # # ----------------------- autorun exec rules: --------------------------------- # - pass 'ar_source=/dev/fd#' to request floppy device test # - CD is tested if no floppy requested or no autorun found on floppy # - if a file named 'autorun' is found on any media, it is always run, except if # option 'ar_disable' is used # - if a file named 'autorun[0-9A-F]' is found on any media, it is run if either # - 'ar_suffixes=...' arg did specify its suffix (ex. ar_suffixes=1,3,5), or # - no 'ar_suffixes=...' arg was passed # - pass ar_suffixes=no to prevent running any 'autorun[0-9A-F]' file # - defaults to allow all 'autorun[0-9A-F]' files # - if many autorun files are to be run, # - always in alphab order: autorun, then autorun0, then autorun1 etc... # - first non-zero exit code stops all (except if ar_ignorefail is used) # - if option 'ar_nodel' is used, the temp copy of the script will not be deleted # - if option 'ar_ignorefail' is used, do not stop autorun if a script failed # - if option 'ar_disable' is used, absolutely no autorun script will be run import sys, os, re, subprocess, logging, time, glob, json # ------------------------ initialize internal variables ----------------------- pidfile='/run/autorun.pid' basedir='/var/autorun' autorunlog=basedir+'/log' autorunmnt=basedir+'/mnt' autoruntmp=basedir+'/tmp' defaultsrc=['/run/archiso/bootmnt/autorun','/run/archiso/bootmnt','/run/archiso/copytoram/autorun','/run/archiso/copytoram','/var/autorun/cdrom','/root','/usr/share/sys.autorun'] effectivecfg="/etc/sysrescue/sysrescue-effective-config.json" autorunfiles=[] config = {} # ----------------------- functions definitions -------------------------------- def writemsg(message): print(message) logging.info(message) # remove all '\r' in that file def processdostextfiles(curfile): txt=open(curfile,'rb').read() origlen=len(txt) txt=txt.replace(b'\r',b'') if len(txt) != origlen: writemsg(f'WARNING: \\r line endings removed from {curfile}.') writemsg('Relying on automatic line ending sanitizing is deprecated and it will be removed from a future release.') txtfile=open(curfile, 'wb') txtfile.write(txt) txtfile.close() def is_elf_binary(filename): with open(filename,'rb') as f: content = f.read(4) if len(content) == 4 and \ content[0] == '\x7f' and content[1] == 'E' and \ content[2] == 'L' and content[3] == 'F': return True else: return False def ensure_shebang(filename): # does the file have a shebang? with open(filename,'r+') as f: content = f.read() if len(content) > 2 and content[0] == '#' and content[1] == '!': # we have a shebang, nothing to do return # no shebang, we have to add one writemsg(f'WARNING: no shebang in {filename}.') writemsg('This is deprecated and a shebang will be required in future releases.') f.seek(0, 0) f.write("#!/bin/sh\n" + content) def format_title(title, padding): totallen=80 startpos=int(totallen/2)-int(len(title)/2) remain=totallen-startpos-len(title) text=(padding*startpos)+title+(padding*remain) return text def copyfilefct_basic(src, dest): if os.path.isfile(src): dstfile=open(dest, 'wb') dstfile.write(open(src,'rb').read()) dstfile.close() os.chmod(dest, 755) return 0 else: return -1 def copyfilefct_http(src, dest): logging.debug(f"Attempting to download {src} ...") cmd=('wget','-q',src,'-O',dest) p = subprocess.Popen(cmd) p.wait() if p.returncode == 0: logging.info(f"Successfully downloaded {src}") os.chmod(dest, 755) return 0 else: logging.warning(f"Failed to download {src}") os.unlink(dest) return -1 def search_autoruns(dirname, suffixes, copyfilefct): found=0 for ext in suffixes: curpath=os.path.join(dirname, f'autorun{ext}') newpath=os.path.join(autoruntmp, f'autorun{ext}') if copyfilefct(curpath, newpath)==0: autorunfiles.append(newpath) found+=1 return found def strtobool (val): """Convert a string representation of truth to true (1) or false (0). True values are 'y', 'yes', 't', 'true', 'on', '1', '1.0'; false values are 'n', 'no', 'f', 'false', 'off', '0', '0.0'. Raises ValueError if 'val' is anything else. Function adapted from Pythons distutils.util.py because it will be deprecated soon Copyright (c) Python Software Foundation; All Rights Reserved """ val = str(val).lower() if val in ('y', 'yes', 't', 'true', 'on', '1', '1.0'): return True elif val in ('n', 'no', 'f', 'false', 'off', '0', '0.0'): return False else: raise ValueError("invalid truth value %r" % (val,)) def read_cfg_value(name, defaultval): if name in config: chkval = config[name] try: if isinstance(chkval, list) or isinstance(chkval, dict): raise TypeError(f"must be a {type(defaultval)}, not a {type(chkval)}") elif isinstance(defaultval, bool) and not isinstance(chkval, bool): val = strtobool(chkval) else: val = type(defaultval)(chkval) except (TypeError, ValueError) as e: writemsg(f"config['{name}'] with {chkval} is not the same type as defaultval: {e}") val = defaultval else: val = defaultval print(f"config['{name}']={val}") return val def main(): global config errcnt=0 # in case no autorun executed logging.basicConfig(filename='/var/log/sysrescue-autorun.log', format='%(asctime)s %(message)s', level=logging.DEBUG) writemsg('Initializing autorun ...') # ---- read the effective configuration file if os.path.exists(effectivecfg) == False: print (f"Failed to find effective configuration file in {effectivecfg}") sys.exit(1) with open(effectivecfg) as file: fullcfg = json.load(file) if 'autorun' in fullcfg: config = fullcfg['autorun'] else: config = { } # ---- parse legacy options passed on the boot command line for curopt in open("/proc/cmdline","r").read().split(): if re.match('^autoruns=', curopt): # "autoruns" is the legacy name for 'ar_suffixes' config['ar_suffixes'] = curopt.replace('autoruns=','') # ---- load the config into local variables, allow the keys not to exist # ---- also print the effective configuration logging.info(f"Showing the effective autorun configuration ...") ar_suffixes = read_cfg_value('ar_suffixes', "") ar_disable = read_cfg_value('ar_disable', False) ar_attempts = read_cfg_value('ar_attempts', 1) ar_source = read_cfg_value('ar_source', "") ar_ignorefail = read_cfg_value('ar_ignorefail', False) ar_nodel = read_cfg_value('ar_nodel', False) ar_nowait = read_cfg_value('ar_nowait', False) # ---- determine the effective script files suffixes if ar_suffixes in (None, 'no', ''): suffixes=[''] else: suffixes=[''] + str(ar_suffixes).split(',') logging.info(f"suffixes={suffixes}") # ---- exit here is there is nothing to do if ar_disable == True: writemsg(f"Autorun has been disabled using ar_disable, exiting now") sys.exit(0) # ---- parse autorun sources ---- if re.match('^https?://', ar_source): while ar_attempts > 0 and not autorunfiles: time.sleep(1) ar_attempts -= 1 search_autoruns(ar_source, suffixes, copyfilefct_http) elif re.match('^/dev/', ar_source): # mount a partition/device mnt1=('mount',ar_source,autorunmnt) mnt2=('umount',autorunmnt) p = subprocess.Popen(mnt1) p.wait() if p.returncode != 0: writemsg('fatal error: cannot mount', mnt1) sys.exit(1) search_autoruns(autorunmnt, suffixes, copyfilefct_basic) subprocess.Popen(mnt2) elif re.match('^nfs://', ar_source): # mount an nfs share source=ar_source.replace('nfs://','') mnt1=('mount','-t','nfs','-o','nolock',source,autorunmnt) mnt2=('umount',autorunmnt) p = subprocess.Popen(mnt1) p.wait() if p.returncode != 0: writemsg ('fatal error: cannot mount', mnt1) sys.exit(1) search_autoruns(autorunmnt, suffixes, copyfilefct_basic) subprocess.Popen(mnt2) elif re.match('^smb://', ar_source): # mount a samba share source=ar_source.replace('smb://','') mnt1=('mount','-t','cifs','//%s'%source,autorunmnt) mnt2=('umount',autorunmnt) p = subprocess.Popen(mnt1) p.wait() if p.returncode != 0: writemsg ('fatal error: cannot mount',mnt1) sys.exit(1) search_autoruns(autorunmnt, suffixes, copyfilefct_basic) subprocess.Popen(mnt2) else: # search in all default directories writemsg ('Cannot find a valid ar_source, searching scripts in the default directories') found=0 for curdir in defaultsrc: if found == 0: found += search_autoruns(curdir, suffixes, copyfilefct_basic) # ---- remove user setable ar_nowait flag if set ---- if os.path.isfile('/etc/ar_nowait'): os.unlink('/etc/ar_nowait') # ---- execute the autorun scripts found ---- for curfile in autorunfiles: try: if not is_elf_binary(curfile): processdostextfiles(curfile) # compatibility with old autorun: add #!/bin/sh if no shebang ensure_shebang(curfile) except: pass filebase=os.path.basename(curfile) writemsg("\n") writemsg(format_title(f'executing {filebase}', '=')) redir=os.path.join(autorunlog, filebase) logoutput=open(redir,'wt') try: # directly (=without extra shell) execute the script # stdin=None means the stdin of sysrescue-autorun will be passed through # this allows the autorun script to take input from the terminal proc = subprocess.Popen(curfile, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False,universal_newlines=True) # pipe through stdout&stderr live, write it to the autorunlog too # we do not expect too much data here, so reading byte-by-byte is ok # but it allows us to show for example progress indicators live on the console while not proc.stdout.closed and proc.stdout.readable() and proc.poll() is None: output = proc.stdout.read(1) sys.stdout.write(output) sys.stdout.flush() logoutput.write(output) # the program has ended. read the rest of data that is in the buffer if not proc.stdout.closed and proc.stdout.readable(): output = proc.stdout.read(-1) sys.stdout.write(output) sys.stdout.flush() logoutput.write(output) logoutput.close() returncode = proc.returncode except OSError as e: # for example the program wasn't found or is not executable writemsg (f'Execution of {filebase} failed: {e.strerror}') returncode = e.errno fileres=open(redir+'.return','wt') fileres.write(str(returncode)+'\n') fileres.close() writemsg('='*80) writemsg (f'Execution of {filebase} returned {returncode}') if returncode != 0: errcnt += 1 if ar_ignorefail == False: writemsg (f'Now aborting autorun as {filebase} has failed') break; # Stop on the first script failure # ---- delete the copies of the scripts ---- if ar_nodel == False: for curfile in autorunfiles: writemsg (f'removing {curfile}') os.unlink(curfile) # ---- wait a keypress feature ----- if os.path.isfile('/etc/ar_nowait'): ar_nowait = True if (ar_nowait != True) and (len(autorunfiles) > 0): writemsg(f'Autorun scripts completed with {errcnt} errors, press to continue') sys.stdin.read(1) return errcnt # ----------------------- autorun main ---------------------------------------- for curdir in (basedir, autorunlog, autorunmnt, autoruntmp): if not os.path.isdir(curdir): os.mkdir(curdir) # Exit if already running if os.path.isfile(pidfile): sys.exit(0) # create lockfile lockfile = open(pidfile, 'wt') lockfile.write(str(os.getpid())) try: res = main() sys.exit(res) finally: os.unlink(pidfile)