mirror of
https://github.com/nchevsky/systemrescue-zfs.git
synced 2026-03-19 02:14:39 +01:00
- Implement a new style configuring autorun scripts ("autorun.exec"), more suited to a YAML config file than the old style (#287)
- The old style autorun scripts are still fully supported, they are loaded into keys from "1000-autorun" to "1026-autorunF"
- change the default for ar_nowait to true
- add "shell" option for new autorun exec scripts: let bash interpret the command instead of directly forking it from python
- allow to wait for keypress with a countdown timer, all keys are accepted now (instead of just enter as in the past)
- fix is_elf_binary
- improve output and logging (#253)
- use curl instead of wget for downloading scripts from URLs
- bind-mount /run/archios/bootmnt in case of copytoram to create a stable path for the new-style scripts
- deprecate storing autorun scripts in the root of the boot disk (#252)
- don't check /var/autorun/cdrom for autorun scripts anymore, it was not documented and there are more than enough better alternatives
758 lines
28 KiB
Python
Executable file
758 lines
28 KiB
Python
Executable file
#!/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
|
|
#
|
|
# For documentation see
|
|
# https://www.system-rescue.org/manual/Run_your_own_scripts_with_autorun/
|
|
#
|
|
|
|
import sys, os, re, subprocess, logging, time, json, traceback, tty, termios, select, tempfile, stat
|
|
|
|
# ------------------------ 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','/root','/usr/share/sys.autorun']
|
|
effectivecfg="/etc/sysrescue/sysrescue-effective-config.json"
|
|
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] == 0x7f and content[1] == ord('E') and \
|
|
content[2] == ord('L') and content[3] == ord('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)-1
|
|
remain=totallen-startpos-len(title)-2
|
|
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, 0o755)
|
|
return 0
|
|
else:
|
|
return -1
|
|
|
|
def copyfilefct_http(src, 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:
|
|
writemsg(f"Successfully downloaded {src}")
|
|
os.chmod(dest, 0o755)
|
|
return 0
|
|
else:
|
|
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, 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[ext]=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 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:
|
|
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
|
|
if printit:
|
|
writemsg(f"config['{name}']={val}")
|
|
return val
|
|
|
|
def wait_for_keypress(timeout=None):
|
|
try:
|
|
# disable line buffering for stdin
|
|
old_settings = termios.tcgetattr(sys.stdin)
|
|
tty.setcbreak(sys.stdin.fileno())
|
|
|
|
pollObj = select.poll()
|
|
pollObj.register(sys.stdin.fileno(), select.POLLIN | select.POLLHUP | select.POLLERR)
|
|
|
|
# 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)
|
|
|
|
if timeout is None:
|
|
print('Press any key to continue')
|
|
else:
|
|
# timeout in seconds
|
|
countdown = int(timeout)
|
|
|
|
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
|
|
|
|
# key pressed or countdown reached 0
|
|
print()
|
|
return
|
|
|
|
except KeyboardInterrupt:
|
|
# ctrl+c counts as just any other key
|
|
print()
|
|
return
|
|
|
|
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 success:
|
|
os.remove(targetfilename)
|
|
except:
|
|
# ignore it if the tmpfile is already deleted
|
|
pass
|
|
|
|
return False, ""
|
|
|
|
def exec_script_errorwrapper(entryname, data):
|
|
""" provides errorhandling around exec_script()
|
|
|
|
does waiting for keypress and similar, depending on result
|
|
returns: 0 = ok, 1 = error but continue, 2 = error don't continue
|
|
"""
|
|
|
|
global ar_ignorefail
|
|
|
|
# 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"
|
|
|
|
if "waitmode" in data and \
|
|
( data["waitmode"] == "key" or is_float(data["waitmode"]) ):
|
|
waitmode = data["waitmode"]
|
|
else:
|
|
waitmode = 30
|
|
|
|
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)
|
|
|
|
# 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)
|