Merge branch 'autorun-yaml' into 'main'

Implement new autorun configuration ( #287)

See merge request systemrescue/systemrescue-sources!233
This commit is contained in:
Gerd v. Egidy 2022-10-11 19:29:13 +00:00
commit 28274716e8
3 changed files with 617 additions and 196 deletions

View file

@ -1,36 +1,17 @@
#!/usr/bin/env python3
#
# Distributed under the terms of the GNU General Public License v2
# SPDX-License-Identifier: GPL-2.0-only
#
# 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
# For more detailed history and contributors see git log
#
# ----------------------- 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
# For documentation see
# https://www.system-rescue.org/manual/Run_your_own_scripts_with_autorun/
#
# ----------------------- 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
import sys, os, re, subprocess, logging, time, json, traceback, tty, termios, select, tempfile, stat
# ------------------------ initialize internal variables -----------------------
pidfile='/run/autorun.pid'
@ -38,9 +19,8 @@ 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']
defaultsrc=['/run/archiso/bootmnt/autorun','/run/archiso/bootmnt','/root','/usr/share/sys.autorun']
effectivecfg="/etc/sysrescue/sysrescue-effective-config.json"
autorunfiles=[]
config = {}
# ----------------------- functions definitions --------------------------------
@ -64,8 +44,8 @@ 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':
content[0] == 0x7f and content[1] == ord('E') and \
content[2] == ord('L') and content[3] == ord('F'):
return True
else:
return False
@ -85,9 +65,9 @@ def ensure_shebang(filename):
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)
startpos=int(totallen/2)-int(len(title)/2)-1
remain=totallen-startpos-len(title)-2
text=(padding*startpos)+" "+title+" "+(padding*remain)
return text
def copyfilefct_basic(src, dest):
@ -95,32 +75,36 @@ def copyfilefct_basic(src, dest):
dstfile=open(dest, 'wb')
dstfile.write(open(src,'rb').read())
dstfile.close()
os.chmod(dest, 755)
os.chmod(dest, 0o755)
return 0
else:
return -1
def copyfilefct_http(src, dest):
logging.debug(f"Attempting to download {src} ...")
cmd=('wget','-q',src,'-O',dest)
writemsg(f"Attempting to download {src} ...")
cmd=("curl","--connect-timeout","30","--silent","--show-error","--fail","--output",dest,src)
p = subprocess.Popen(cmd)
p.wait()
if p.returncode == 0:
logging.info(f"Successfully downloaded {src}")
os.chmod(dest, 755)
writemsg(f"Successfully downloaded {src}")
os.chmod(dest, 0o755)
return 0
else:
logging.warning(f"Failed to download {src}")
os.unlink(dest)
writemsg(f"Failed to download {src}")
# delete anything we retrieved, but don't care if there is no file
try:
os.unlink(dest)
except:
pass
return -1
def search_autoruns(dirname, suffixes, copyfilefct):
def search_autoruns(dirname, suffixes, autorunfiles, 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)
autorunfiles[ext]=newpath
found+=1
return found
@ -142,7 +126,14 @@ def strtobool (val):
else:
raise ValueError("invalid truth value %r" % (val,))
def read_cfg_value(name, defaultval):
def is_float(str_to_test):
try:
float(str_to_test)
return True
except ValueError:
return False
def read_cfg_value(name, defaultval, printit=True):
if name in config:
chkval = config[name]
try:
@ -157,178 +148,599 @@ def read_cfg_value(name, defaultval):
val = defaultval
else:
val = defaultval
print(f"config['{name}']={val}")
if printit:
writemsg(f"config['{name}']={val}")
return val
def main():
global config
def wait_for_keypress(timeout=None):
try:
# disable line buffering for stdin
old_settings = termios.tcgetattr(sys.stdin)
tty.setcbreak(sys.stdin.fileno())
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 ...')
pollObj = select.poll()
pollObj.register(sys.stdin.fileno(), select.POLLIN | select.POLLHUP | select.POLLERR)
# ---- 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 = { }
# is there input data available? clear it. we just want *new* keypresses, not old input
while len(pollObj.poll(0)) > 0:
os.read(sys.stdin.fileno(), 4096)
# ---- 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=['']
if timeout is None:
print('Press any key to continue')
else:
suffixes=[''] + str(ar_suffixes).split(',')
logging.info(f"suffixes={suffixes}")
# timeout in seconds
countdown = int(timeout)
# ---- 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)
while timeout is None or countdown > 0:
if timeout is not None:
# ESC M is reverse linefeed
# If it can't be interpreted (for example on a dumb serial console) we just get the regular newlines
print(f'\n\033MWait {countdown} seconds or press any key to continue ', end="")
# 1 second wait for event
if len(pollObj.poll(1000)) > 0:
# we got some input, read as much as we can to not leave the next script with half-processed input
os.read(sys.stdin.fileno(), 4096)
# we don't really care for the data, any key is ok to abort
break
if timeout is not None:
countdown -= 1
# ---- 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)
# key pressed or countdown reached 0
print()
return
# ---- remove user setable ar_nowait flag if set ----
if os.path.isfile('/etc/ar_nowait'):
os.unlink('/etc/ar_nowait')
except KeyboardInterrupt:
# ctrl+c counts as just any other key
print()
return
# ---- execute the autorun scripts found ----
for curfile in autorunfiles:
except termios.error:
# probably some strange stdin pipe, so no keypress to wait for -> just sleep
if timeout is None:
timeout = 30
print(f'Wait for {timeout} seconds')
try:
time.sleep(float(timeout))
except KeyboardInterrupt:
# clean abort with ctrl+C
print()
return
pass
finally:
# always reset the stdin state
try:
# restore stdio terminal settings
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
except:
# ignore termios errors, probably some strange stdin, so no keypress anyway
pass
def build_config():
"""Build the effective configuration for autorun.
Searches for autorun-scripts in several paths and uses the old-style ar_suffixes= option to
merge the scripts found into the new-style "exec" structure which is then executed.
The scripts are copied to autoruntmp, regardless where they were found.
"""
global config
global ar_ignorefail, ar_nodel, ar_nowait, ar_attempts
# ---- read the effective configuration file, build basic config structure
if not os.path.exists(effectivecfg):
writemsg(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 = { }
if not 'exec' in config or not isinstance(config['exec'], dict):
config["exec"] = { }
# ---- 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'
writemsg('WARNING: deprecated option `autoruns=` used, it will be removed soon. Use `ar_suffixes` instead')
config['ar_suffixes'] = curopt.replace('autoruns=','')
# ---- load the config into local variables, allow the keys not to exist
# ---- also print the effective configuration
writemsg("Showing the effective autorun configuration ...")
ar_suffixes = read_cfg_value('ar_suffixes', "", printit=False)
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', True)
# ---- determine the effective script files suffixes
if ar_suffixes in (None, 'no', ''):
suffixes=['']
else:
suffixes=[''] + str(ar_suffixes).split(',')
# validate suffixes, remove everything that is not on the whitelist
to_remove = []
for suf in suffixes:
if suf not in ( "", "0", "1", "2", "3", "4", "5", "6" ,"7", "8" ,"9", "A" ,"B" ,"C" ,"D", "E" , "F" ):
writemsg(f"Illegal autorun suffix {suf}: removed")
to_remove.append(suf)
for suf in to_remove:
suffixes.remove(suf)
writemsg(f"ar_suffixes={suffixes}")
# print+log at least the root exec entry names for debugging
if 'exec' in config and isinstance(config['exec'], dict):
exectext = str(list(config['exec'].keys()))
else:
exectext = "[]"
writemsg(f"exec={exectext}")
# ---- exit here is there is nothing to do
if ar_disable:
writemsg("Autorun has been disabled using ar_disable, exiting now")
sys.exit(0)
# ---- parse autorun sources ----
autorunfiles= {}
if re.match('^https?://', ar_source):
retries = int(ar_attempts)
while retries > 0 and not autorunfiles:
time.sleep(1)
retries -= 1
search_autoruns(ar_source, suffixes, autorunfiles, 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(f'error: cannot mount {ar_source}')
return
search_autoruns(autorunmnt, suffixes, autorunfiles, copyfilefct_basic)
subprocess.Popen(mnt2)
elif re.match('^nfs://', ar_source): # mount an nfs share
source=ar_source.replace('nfs://','')
# retry=1 means retry it for 1 minute, but there also is a 3 minute tcp timeout -> 3 minutes timeout
mnt1=('mount','-t','nfs','-o','nolock,retry=1',source,autorunmnt)
mnt2=('umount',autorunmnt)
p = subprocess.Popen(mnt1)
p.wait()
if p.returncode != 0:
writemsg (f'error: cannot mount {source}')
return
search_autoruns(autorunmnt, suffixes, autorunfiles, copyfilefct_basic)
subprocess.Popen(mnt2)
elif re.match('^smb://', ar_source): # mount a samba share
source=ar_source.replace('smb://','')
# use -o guest to prevent mount from asking for a password
mnt1=('mount','-t','cifs','-o','guest','//%s'%source,autorunmnt)
mnt2=('umount',autorunmnt)
p = subprocess.Popen(mnt1)
p.wait()
if p.returncode != 0:
writemsg (f'error: cannot mount {source}')
return
search_autoruns(autorunmnt, suffixes, autorunfiles, copyfilefct_basic)
subprocess.Popen(mnt2)
else: # search in all default directories
writemsg ('Cannot find a valid ar_source, searching scripts in the default directories')
for curdir in defaultsrc:
if search_autoruns(curdir, suffixes, autorunfiles, copyfilefct_basic) > 0:
writemsg (f'Using autorun scripts from {curdir}')
if curdir == '/run/archiso/bootmnt':
writemsg('WARNING: Storing autorun scripts in the root of the boot device is deprecated!')
writemsg('These scripts will be ignored in a future release. Use the autorun directory instead.')
writemsg("")
break
# build exec structure
for ext, curfile in sorted(autorunfiles.items()):
# build entry name for the exec dict
if ext == '':
entryno = 1000
elif ext[0].isdigit():
entryno = 1010 + int(ext[0])
elif ord(ext[0]) >= ord('A') and ord(ext[0]) <= ord('F'):
entryno = 1020 + ord(ext[0]) - ord('A')
else:
writemsg (f'fatal error: illegal autorun extension {ext}')
sys.exit(1)
filebase=os.path.basename(curfile)
entryname = f'{entryno}-{filebase}'
# overwrite entry if it already exists
config["exec"][entryname] = { }
config["exec"][entryname]['path'] = curfile
# leave everything else at default values
def download_script(url, execname):
global ar_attempts
retries = int(ar_attempts)
while retries > 0:
writemsg(f'Downloading {execname} from {url} ...')
if retries != int(ar_attempts):
time.sleep(3)
retries -= 1
try:
success = False
targetfile=tempfile.NamedTemporaryFile(dir=autoruntmp, delete=False)
targetfilename=targetfile.name
targetfile.close()
if re.match('^https?://', url):
if copyfilefct_http(url,targetfilename) == 0:
success = True
return success, targetfilename
elif re.match('^/dev/', url):
# mount a partition/device, something like /dev/disk/by-label/some-disklabel/dir/file
# we have to split the path to the device from the path below first
dircomponents = url.split('/')
path = "/"
for dircomp in dircomponents:
try:
path = os.path.join(path, dircomp)
mode = os.stat(path).st_mode
if stat.S_ISBLK(mode):
# we found our block device to mount
break
elif stat.S_ISDIR(mode):
# continue to descent the path
continue
else:
writemsg(f'Error: unexpected file type at {path}')
return False, ""
except FileNotFoundError:
writemsg(f'Error: can\'t find file {path}')
return False, ""
mountbase = path
srcfile = url.removeprefix(path)
srcfile = srcfile.removeprefix("/")
mnt1=('mount',mountbase,autorunmnt)
mnt2=('umount',autorunmnt)
p = subprocess.Popen(mnt1)
p.wait()
if p.returncode != 0:
writemsg(f'Error: cannot mount {mountbase}')
return False, ""
ret = copyfilefct_basic(os.path.join(autorunmnt,srcfile),targetfilename)
subprocess.Popen(mnt2)
if ret == 0:
success = True
return success, targetfilename
else:
writemsg(f'Error: cannot find file {srcfile} in mountpoint {mountbase}')
return False, ""
elif re.match('^nfs://', url):
# mount an nfs share, nfs://servername:/dir/script
source=url.replace('nfs://','')
sourcesplit = source.split(":", maxsplit=1)
if len(sourcesplit) != 2 or len(sourcesplit[1]) == 0:
writemsg(f'Illegal nfs url, not in the form nfs://servername:/dir/script')
return False, ""
srcdir = os.path.dirname(sourcesplit[1])
srcfile = os.path.basename(sourcesplit[1])
if len(srcfile) == 0:
writemsg(f'Illegal nfs url, not in the form nfs://servername:/dir/script')
return False, ""
mountbase=sourcesplit[0]+":"+srcdir
# retry=1 means retry it for 1 minute, but there also is a 3 minute tcp timeout -> 3 minutes timeout
mnt1=('mount','-t','nfs','-o','nolock,retry=1',mountbase,autorunmnt)
mnt2=('umount',autorunmnt)
p = subprocess.Popen(mnt1)
p.wait()
if p.returncode != 0:
writemsg(f'Error: cannot mount {mountbase}')
# retry it, it is a network operation after all
continue
ret = copyfilefct_basic(os.path.join(autorunmnt,srcfile),targetfilename)
subprocess.Popen(mnt2)
if ret == 0:
success = True
return success, targetfilename
else:
writemsg(f'Error: cannot find file {srcfile} in mountpoint {mountbase}')
return False, ""
elif re.match('^smb://', url):
# mount a samba share, smb://host/share/dir/script
source=url.replace('smb://','')
srchostdir = os.path.dirname(source)
srcfile = os.path.basename(source)
if len(srchostdir) == 0 or len(srchostdir.split("/")) < 2 or len(srcfile) == 0:
writemsg(f'Illegal smb url, not in the form smb://host/share/dir/script')
return False, ""
mountbase="//"+srchostdir
# use -o guest to prevent mount from asking for a password
mnt1=('mount','-t','cifs','-o','guest',mountbase,autorunmnt)
mnt2=('umount',autorunmnt)
p = subprocess.Popen(mnt1)
p.wait()
if p.returncode != 0:
writemsg(f'Error: cannot mount {mountbase}')
# retry it, it is a network operation after all
continue
ret = copyfilefct_basic(os.path.join(autorunmnt,srcfile),targetfilename)
subprocess.Popen(mnt2)
if ret == 0:
success = True
return success, targetfilename
else:
writemsg(f'Error: cannot find file {srcfile} in mountpoint {mountbase}')
return False, ""
finally:
try:
if not is_elf_binary(curfile):
processdostextfiles(curfile)
# compatibility with old autorun: add #!/bin/sh if no shebang
ensure_shebang(curfile)
if not success:
os.remove(targetfilename)
except:
# ignore it if the tmpfile is already deleted
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)
return False, ""
# 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()
def exec_script_errorwrapper(entryname, data):
""" provides errorhandling around exec_script()
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
does waiting for keypress and similar, depending on result
returns: 0 = ok, 1 = error but continue, 2 = error don't continue
"""
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
global ar_ignorefail
# ---- delete the copies of the scripts ----
if ar_nodel == False:
for curfile in autorunfiles:
writemsg (f'removing {curfile}')
os.unlink(curfile)
# determine what to do in case of an error
if "wait" in data and \
( data["wait"] == "always" or data["wait"] == "on_error" or data["wait"] == "never" ):
waitwhen = data["wait"]
else:
waitwhen = "on_error"
# ---- 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 <Enter> to continue')
sys.stdin.read(1)
if "waitmode" in data and \
( data["waitmode"] == "key" or is_float(data["waitmode"]) ):
waitmode = data["waitmode"]
else:
waitmode = 30
return errcnt
if "on_error" in data and \
( data["on_error"] == "break" or data["on_error"] == "continue" ):
errmode = data["on_error"]
else:
if not ar_ignorefail:
errmode = "break"
else:
errmode = "continue"
# execute the script
try:
errcount = exec_script(entryname, data)
except Exception as e:
# some exception we did not expect, deal with it like any other script error
writemsg('='*80)
writemsg (f'Exception {e.__class__.__name__} occured when executing {entryname}: {e}')
writemsg(traceback.format_exc())
errcount = 1
# waiting for timeout / keypress wanted?
if waitwhen == "always" or \
( waitwhen == "on_error" and errcount > 0 ):
# we need to wait
if waitmode == "key":
wait_for_keypress()
else:
wait_for_keypress(int(waitmode))
if errcount > 0:
if errmode == "break":
# don't continue executing other scripts
return 2
else:
# error, but continue
return 1
else:
# everything ok
return 0
def exec_script(entryname, data):
global config
if 'path' in data and 'url' in data:
writemsg (f'illegal configuration for {entryname}: both "path" and "url" given')
return 1
if not 'path' in data and not 'url' in data:
writemsg (f'illegal configuration for {entryname}: neither "path" nor "url" given')
return 1
if 'url' in data:
# resolve the URL, download to a tmp file, store tmp filename in path
success, path = download_script(str(data["url"]), entryname)
if not success:
return 1
# store temporary filename in config structure so we can delete it later
config["exec"][entryname]["path"] = path
else:
path = str(data["path"])
# we now have something in path, work it
try:
# only modify files in our tmp dir
if os.path.dirname(path) == autoruntmp and not is_elf_binary(path):
processdostextfiles(path)
# compatibility with old autorun: add #!/bin/sh if no shebang
ensure_shebang(path)
except:
pass
# sanitize parameters
if "parameters" in data and isinstance(data['parameters'], (str, int, float, bool)):
# we always want a list, even if we just have one parameter
data['parameters'] = [ data['parameters'] ]
if "parameters" in data and isinstance(data['parameters'], list):
# ensure all parameters are strings
new_param = [ ]
for p in data['parameters']:
new_param.append(str(p))
data['parameters'] = new_param
if "shell" in data and strtobool(data['shell']) is True:
# execution with shell
shell = True
# path_and_param must be just a string that is then interpreted by the shell
# since the user can't always control "path" (think urls), we append all the
# parameters as they are, the user has to ensure quoting etc.
path_and_param = path
if "parameters" in data and isinstance(data['parameters'], list):
for p in data['parameters']:
path_and_param += " " + p
else:
# execution without shell (default, recommended when not needing shell features)
shell = False
path_and_param = [ path ]
if "parameters" in data and isinstance(data['parameters'], list):
path_and_param += data['parameters']
writemsg("")
writemsg(format_title(f'executing {entryname}', '='))
# open logfile, sanitize filename
redir=os.path.join(autorunlog, re.sub('[^-a-zA-Z0-9_.]+', '_', entryname))
logoutput=open(redir,'wt')
exception = False
linelog = ""
try:
# 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(path_and_param, stdin=None, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, shell=shell,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)
# also write to the main autorun log. add manual line buffering because of timestamps added
if output == '\n':
# don't log newlines themselves
logging.info(linelog)
linelog = ""
else:
linelog += 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()
# also write to the main autorun log. add manual line buffering because of timestamps added
for char in output:
if char == '\n':
# don't log newlines themselves
logging.info(linelog)
linelog = ""
else:
linelog += char
returncode = proc.returncode
except OSError as e:
# for example the program wasn't found or is not executable
writemsg('='*80)
writemsg (f'Execution of {entryname} failed: {e.strerror}')
returncode = e.errno
exception = True
except KeyboardInterrupt:
print()
writemsg('='*80)
writemsg (f'Execution of {entryname} aborted')
returncode = 1
exception = True
fileres=open(redir+'.return','wt')
fileres.write(str(returncode)+'\n')
fileres.close()
if not exception:
writemsg('='*80)
writemsg (f'Execution of {entryname} returned {returncode}')
if exception or returncode != 0:
# we have an error
return 1
else:
return 0
def main():
global config
global ar_ignorefail, ar_nodel, ar_nowait, ar_attempts
logging.basicConfig(filename='/var/log/sysrescue-autorun.log', format='%(asctime)s %(message)s', level=logging.DEBUG)
writemsg("")
writemsg(format_title('Initializing autorun','#'))
build_config()
if len(config['exec']) == 0:
writemsg('No autorun scripts found, nothing to do.')
return 0
writemsg('Starting autorun execution ...')
# ---- 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 ----
errcnt=0
for entryname, data in sorted(config['exec'].items()):
err = exec_script_errorwrapper(entryname, data)
if err > 0:
errcnt += 1
if err == 2:
# Stop on the first script failure
writemsg (f'Now aborting autorun as {entryname} has failed')
break
writemsg("")
# ---- delete tmp copies of the scripts ----
if not ar_nodel:
for name, data in sorted(config['exec'].items()):
if "path" in data and os.path.dirname(data["path"]) == autoruntmp:
writemsg (f'removing {data["path"]}')
os.unlink(data["path"])
# ---- wait a keypress feature -----
if os.path.isfile('/etc/ar_nowait'):
ar_nowait = True
if not ar_nowait and len(config['exec'].items()) > 0:
writemsg(f'Autorun scripts completed with {errcnt} errors')
wait_for_keypress()
return errcnt
# ----------------------- autorun main ----------------------------------------
for curdir in (basedir, autorunlog, autorunmnt, autoruntmp):
if not os.path.isdir(curdir):
os.mkdir(curdir)
if not os.path.isdir(curdir):
os.mkdir(curdir)
# Exit if already running
if os.path.isfile(pidfile):
@ -339,7 +751,7 @@ lockfile = open(pidfile, 'wt')
lockfile.write(str(os.getpid()))
try:
res = main()
sys.exit(res)
res = main()
sys.exit(res)
finally:
os.unlink(pidfile)
os.unlink(pidfile)

View file

@ -25,3 +25,12 @@ diff -urN archiso-43-a/archiso/initcpio/hooks/archiso archiso-43-b/archiso/initc
[[ "${loadsrm}" == "y" ]] && _mnt_srm "/run/archiso/bootmnt/${archisobasedir}"
if [[ -f "/run/archiso/sfs/airootfs/airootfs.img" ]]; then
@@ -328,6 +328,8 @@
if [[ "${copytoram}" == "y" ]]; then
umount -d /run/archiso/bootmnt
+ # bind-mount bootmnt to create a stable path, for example for autorun scripts
+ mount --bind /run/archiso/copytoram /run/archiso/bootmnt
fi
}

View file

@ -10,7 +10,7 @@ global:
autorun:
ar_disable: false
ar_nowait: false
ar_nowait: true
ar_nodel: false
ar_attempts: 1
ar_ignorefail: false