Verified Commit ba568643 authored by Karel Koci's avatar Karel Koci 🤘

Initial commit

Separate project from updater-ng repository and mark is as 1.0.
parents
Updater-ng supervisor
=====================
Supervising daemon, tool and Python3 library for Updater-ng. The idea is that
updater it self takes a long time to run and this should add simple enough layer
on top of it to run and manage updater from some front-end. It fulfills following
roles and features:
* Supervises user unsupervised `pkgupdate` execution and reports fatal failures
* Allows front-end to run updater in background
* Provides a way to check for updater lock status (to see if updater is running)
* Provides configuration wrapper for updater
* Implements delayed and approved updates (generally known as updater approvals)
* Periodic updater execution with possible random delay
This tool is Turris OS specific. You can use this as a code base to write your own
but you are going to have hard time in integrating it to other distributions.
Checking for updater status
----------------------------
Currently only checks if updater or opkg is running and check if there is instance
of updater running in supervisor are implemented.
To check if updater or opkg is running you can use function `opkg_lock()` which
returns boolean depending on opkg lock status.
To check if there is supervised updater running then you can call
`updater_supervised()`.
Running pkgupdate with supervisor
---------------------------------
To run updater you can either directly run `updater-supervisor` script or you can
use library and `run(wait_for_network, ensure_run, timeout, timeout_kill,
hooklist)` function.
Updater and updater-supervisor configuration
---------------------------------------------
Both updater-ng and updater-supervisor are using UCI for configuration. There are
three primary sections of configuration: `autorun`, `l10n` and `lists`.
All configuration is in UCI file `updater`.
### autorun
This is intended as a configuration for updater-supervisor. It configures
automatic execution and approvals.
`enabled()` and `set_enabled(enabled)` are getter and setter for
`updater.autorun.enabled` config. If this is not set to `True` updater-supervisor
won't start `pkgupdate`.
`approvals()` and `set_approvals(enabled)` are getter and setter for
`updater.autorun.approvals`. This informs updater-supervisor if `pkgupdate` should
be run so it immediately updates system or if it should rather only generate plan
that has to be approved.
`auto_approve_time()` and `set_auto_approve_time(approve_time)` are getter and
setter for `updater.autorun.auto_approve_time`. This is number of hours before
approval is automatically granted. This implements update delay.
### l10n
Updater in Turris OS support multiple languages. Supported languages are provided
by additional file provided by separate package but updater-supervisor serves as
a bridge between that file and updater configuration (`updater.l10n`)
To get current state and list of all supported languages at once you can call
function `l10n.languages()`. This returns dictionary where keys are language codes
and values are boolean specification if language should or should not be
installed. You can edit this table and pass it back to
`l10n.update_languages(langs)` to save settings. By that you are adding or
removing lists to/from `updater.l10n.langs`.
### lists
Updater in Turris OS supports additional sets of packages called package lists.
The definition is provided as file by package the same way as in case of l10n. The
approach to lists is same as in case of l10n with exception that more information
are provided by `lists.pkglists(lang)`.
Approvals
---------
This is a feature that simulates otherwise normal package manager execution with
user approving changes to system explicitly. This feature can also be configured
to serve as delayed updates to just delay update by some amount of time.
The implementation expects updater to be run as usual in periodic runs but
supervisor automatically configures updater to not install update unless it was
approved by it. This is done by providing hash of approved plan (or not providing
any). Updater automatically only plans actions and unless those actions were
approved (hash is same as provided by supervisor) it does not continue and instead
dumps plan information. Front-end can receive such plan with
`approvals.current()`. It later can either approve it by `approvals.approve(hash)`
or deny it `approvals.approve(hash)`. If plan is denied then it won't be
automatically approved later on if delayed updates are configured.
See also `autorun` configuration section for configuration options that are
considered in approvals.
Note that for correct functionality there is `files/hook_postupdate` script that
should be placed in `/etc/updater/hook_postupdate` to be run after `pkgupdate`
completion. It is supporting script to handle situation when supervisor is not the
only one running `pkgupdate`.
Periodic runs
-------------
Cron is used to run updater periodically to check for updates. In short there is a
cron file in `files` directory that can be used to run updater every four hours.
Updater supervisor is in such case run in background which is required because
otherwise it would hang cron execution. Another argument is `--rand-sleep`. This
delays real `pkgupdate` execution by random amount of time. This was introduced to
spread server load, it is highly suggested to use random delay for periodic
updater execution.
MAILTO=""
0 0-23/4 * * * root /usr/bin/updater-supervisor -d --rand-sleep
#!/bin/sh
# Updater it self is not aware of approvals. Those are completely handled in
# updater-supervisor. But when updater does something to system it's most probable
# that it invalidated current approval request. So this script just removes it
# every time update proceeds.
rm -f /usr/share/updater/approvals
#!/bin/sh /etc/rc.common
# This script handles updater-ng immediate reboot recovery. After an immediate
# reboot there is going to be a journal and we should continue execution from it
# as soon as possible.
START=85
status() {
return 0
}
start() {
# Recover updater's journal if it exists
if [ -e "/usr/share/updater/journal" ]; then
# Note: supervisor runs pkgupdate that recovers run from journal and later
# checks for update. This update is required because there could have been
# replan planned after reboot and this ensures that we do replan as soon
# as possible.
updater-supervisor
fi
}
stop() {
:
}
restart() {
:
}
reload() {
:
}
#!/usr/bin/env python3
# Copyright (c) 2018-2019, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from setuptools import setup
setup(
name='svupdater',
version='1.0',
description="Supervising application and library for updater-ng.",
url="https://gitlab.labs.nic.cz/turris/updater/supervisor",
author="CZ.NIC, z. s. p. o.",
author_email="karel.koci@nic.cz",
license="MIT",
packages=['svupdater'],
entry_points={
'console_scripts': [
'updater-supervisor=svupdater:main'
]
}
)
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from . import autorun, const
from .utils import check_exclusive_lock as _check_exclusive_lock
from .utils import daemonize as _daemonize
from ._pidlock import pid_locked as _pid_locked
from .exceptions import ExceptionUpdaterDisabled
from ._supervisor import run as _run
from .prerun import wait_for_network as _wait_for_network
def opkg_lock():
"""Returns True if opkg lock is taken. It can be taken by any other
process. It doesn't have to be updater.
"""
return _check_exclusive_lock(const.OPKG_LOCK, False)
def updater_supervised():
"""This returns True if there is running updater-supervisor instance.
(Running means as a running process not as a library in some other process)
"""
# This is in reality a wrapper on top of pidlock
return _pid_locked()
def run(wait_for_network=False, ensure_run=False, timeout=const.PKGUPDATE_TIMEOUT,
timeout_kill=const.PKGUPDATE_TIMEOUT_KILL, hooklist=None):
"""Run updater.
This call will spawn daemon process and returns. But be aware that at first
it checks if some other supervisor is not running and it takes file lock
because of that. If someone messed up that lock then it won't return
immediately. Calling this with timeout is advised for time sensitive
applications.
If there is already running daemon then it just sends signal to it and
exits.
You can pass hooks (single line shell scripts) to be run after updater.
"""
if not autorun.enabled():
raise ExceptionUpdaterDisabled(
"Can't run. Updater is configured to be disabled.")
# Fork to daemon
if _daemonize():
return
# Wait for network if configured
if wait_for_network:
if type(wait_for_network == bool):
wait_for_network = const.PING_TIMEOUT
_wait_for_network(wait_for_network)
# And run updater
_run(
ensure_run=ensure_run,
timeout=timeout,
timeout_kill=timeout_kill,
verbose=False,
hooklist=hooklist)
exit()
# Copyright (c) 2018-2019, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import sys
import argparse
from svupdater import autorun
from svupdater.prerun import random_sleep, wait_for_network
from svupdater._supervisor import run
from svupdater.const import PKGUPDATE_TIMEOUT, PKGUPDATE_TIMEOUT_KILL
from svupdater.const import PING_TIMEOUT
from svupdater.utils import daemonize
HELP_DESCRIPTION = """
Updater-ng supervisor used for system updating.
"""
def parse_arguments():
"Parse script arguments"
prs = argparse.ArgumentParser(description=HELP_DESCRIPTION)
prs.add_argument('--daemon', '-d', action='store_true',
help="""
Run supervisor in background (detach from terminal).
""")
prs.add_argument('--rand-sleep', const=7200, nargs='?', type=int,
help="""
Sleep random amount of the time with maximum of given
number of seconds. In default two hours are used.
""")
prs.add_argument('--wait-for-network', const=PING_TIMEOUT, type=int,
nargs='?', help="""
Check if Turris repository is accessible (even before
going to background). You can specify timeout in seconds
as an argument. 10 seconds is used if no argument is
specified.
""")
prs.add_argument('--ensure-run', action='store_true',
help="""
Make sure that updater runs at least once after current
time. This can be used to ensure that latest changes are
applied as soon as possible even if another instance of
updater is already running.
""")
prs.add_argument('--quiet', '-q', action='store_true',
help="""
Don't print pkgupdate's output to console. But still print
supervisor output.
""")
prs.add_argument('--timeout', default=PKGUPDATE_TIMEOUT,
help="""
Set time limit in seconds for updater execution. pkgupdate
is gracefully exited when this timeout runs out. This is
protection for pkgupdate stall. In defaut one hour is set
as timeout.
""")
prs.add_argument('--timeout-kill', default=PKGUPDATE_TIMEOUT_KILL,
help="""
Set time in seconds after which pkgupdate is killed. This
is time from timeout. In default one minute is used.
""")
return prs.parse_args()
def main():
"Main function for updater-supervisor run as executable"
if not autorun.enabled():
print('Updater autorun disabled.')
sys.exit(0)
args = parse_arguments()
if args.daemon and daemonize():
return
random_sleep(args.rand_sleep)
wait_for_network(args.wait_for_network)
sys.exit(run(
ensure_run=args.ensure_run,
timeout=args.timeout,
timeout_kill=args.timeout_kill,
verbose=not args.quiet))
if __name__ == '__main__':
main()
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""This implements updater-supervisor pid file lock.
This ensures that only one instance of updater-supervisor is running and that
any other just spawned instance can send signal to this instance.
Signals are used in updater-supervisor for simple comunication between instance
holding lock and any other spawned instance.
"""
import os
import fcntl
import errno
import signal
from .const import PID_FILE_PATH
from .utils import report, check_exclusive_lock
from .exceptions import ExceptionUpdaterPidLockFailure
def pid_locked():
"""Check if someone holds pid lock. It won't check if process holding the
lock is alive. But it can potentially also catch such situation as in that
case we would manage to get exclusive lock. But we can't ensure that
because some other instance can have shared lock at the same time because
it wants to read content.
"""
return check_exclusive_lock(PID_FILE_PATH, True)
def pid_lock_content():
"""Get content of our pid lock. That is, it returns pid (as an integer).
If there is no lock or its content is invalid then it returns None.
"""
file = None
try:
file = os.open(PID_FILE_PATH, os.O_RDONLY)
except IOError as excp:
# There is no such file
if excp.errno == errno.EACCES:
return None
raise
# TODO timeout
fcntl.flock(file, fcntl.LOCK_SH) # Lock for shared read
# Check if we are reading existing file (if it wasn't unlinked)
invalid = False
try:
if os.fstat(file).st_ino != os.stat(PID_FILE_PATH).st_ino:
invalid = True
except OSError as excp:
if excp.errno == errno.ENOENT:
invalid = True
raise
if invalid: # Otherwise try again
os.close(file)
return pid_lock_content()
val = None
with os.fdopen(file, 'r') as filed:
try:
val = int(filed.readline())
except ValueError:
pass # Failed to convert for us means that pid is invalid (None)
# Note: file is closed when we leave fdopen closure
return val
class PidLock():
"""Supervisor pid file to ensure that only one instance is running and that
that specific instance can receive SIGUSR1 to inform it that it should run
pkgupdate once again. This functionality is exported using sigusr property.
Note that there should be only once PidLock object used in single process
because it registers signal.
"""
def __init__(self):
self.file = None
self._sigusr_rec = False
signal.signal(signal.SIGUSR1, self._sigusr1)
def __del__(self):
if self.file is not None:
self.free()
def _sigusr1(self, *_):
self._sigusr_rec = True
@property
def sigusr1(self):
"If SIGUSR1 was receiver. Reading this sets it back to False"
val = self._sigusr_rec
self._sigusr_rec = False
return val
def _take(self, overtake):
"Take lock if possible"
flags = os.O_WRONLY | os.O_SYNC | os.O_CREAT | \
(os.O_EXCL if not overtake else 0)
while True:
try:
self.file = os.open(PID_FILE_PATH, flags)
fcntl.flock(self.file, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as excp:
# File exists or lock couldn't been acquired
if excp.errno == errno.EEXIST or \
excp.errno == errno.EWOULDBLOCK:
return False
raise
# There is possible race condition when file is removed before we
# lock it. This ensures that we have file that is on FS
invalid = False
try:
if os.fstat(self.file).st_ino == os.stat(PID_FILE_PATH).st_ino:
invalid = True
except OSError as excp:
if excp.errno == errno.ENOENT:
invalid = True
raise
if invalid:
os.ftruncate(self.file, 0)
os.write(self.file, str.encode(str(os.getpid())))
os.fsync(self.file)
return True
# File was removed before we were able to acquire lock. Try again.
os.close(self.file)
def acquire(self, send_signal):
"""Try to take supervisor pid lock. Returns boolean signaling if lock
was taken successfully.
"""
if self._take(False):
return True # We have lock so return
pid = pid_lock_content()
if pid is None:
report("Taking lock for PID file failed but no pid loaded. Trying to lock pid again.")
if self._take(True):
return True
pid = pid_lock_content() # Second attempt
if pid is None:
report("Second attempt failed too. Giving up.")
return False
# Here we have loaded pid
sig = signal.SIGUSR1 if send_signal else 0
try:
os.kill(pid, sig)
except OSError as excp:
if excp.errno != errno.ESRCH:
raise
# It doesn't runs
report("There is no running process with stored pid. Overtaking it.")
if self._take(True):
return True
report("Pid file overtake failed. Giving up.")
return False
# Signal sent successfully
if send_signal:
report("Another instance is already running. It was notified to run pkgupdate again.")
else:
report("Another instance of supervisor is already running.")
return False
def free(self):
"""Free pid lock if we have it at the moment
"""
if self.file is None:
raise ExceptionUpdaterPidLockFailure(
"Can't free not taken pidlock")
file = self.file
self.file = None
# TODO timeout
fcntl.flock(file, fcntl.LOCK_EX)
os.remove(PID_FILE_PATH)
os.close(file)
file = None
def block(self):
"""Block read access to pid lock.
"""
if self.file is None:
raise ExceptionUpdaterPidLockFailure(
"Can't block not taken pidlock")
# TODO timeout
fcntl.flock(self.file, fcntl.LOCK_EX)
def unblock(self):
"""Unblock previously blocked read access. Note that in default when
lock is acquired it is blocking read access.
"""
if self.file is None:
raise ExceptionUpdaterPidLockFailure(
"Can't block not taken pidlock")
# TODO timeout
fcntl.flock(self.file, fcntl.LOCK_SH)
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""This module is core of udpdater-supervisor. It runs and supervise updater
execution.
"""
from __future__ import print_function
import os
import sys
import subprocess
import atexit
import signal
import errno
from threading import Thread, Lock
from . import autorun
from . import approvals
from . import notify
from . import hook
from .utils import setup_alarm, report
from .const import PKGUPDATE_CMD, APPROVALS_ASK_FILE, PKGUPDATE_STATE
from ._pidlock import PidLock
class Supervisor:
"pkgupdate supervisor"
def __init__(self, verbose):
self.verbose = verbose
self.kill_timeout = 0
self.process = None
self.trace = None
self.trace_lock = Lock()
self._devnull = open(os.devnull, 'w')
self._stdout_thread = Thread(
target=self._stdout,
name="pkgupdate-stdout")
self._stderr_thread = Thread(
target=self._stderr,
name="pkgupdate-stderr")
atexit.register(self._at_exit)
def run(self):
"Run pkgupdate"
if self.process is not None:
raise Exception("Only one call to Supervisor.run is allowed.")
self.trace = ""
# Create state directory
try:
os.mkdir(PKGUPDATE_STATE)
except OSError as exc:
if exc.errno != errno.EEXIST:
raise
# Prepare command to be run
cmd = list(PKGUPDATE_CMD)
if autorun.approvals():
cmd.append('--ask-approval=' + APPROVALS_ASK_FILE)
approved = approvals._approved()
if approved is not None:
cmd.append('--approve=' + approved)
# Clear old dump files
notify.clear_logs()
# Open process
self.process = subprocess.Popen(
cmd,
stdin=self._devnull,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
self._stdout_thread.start()
self._stderr_thread.start()
def join(self, timeout, killtimeout):
"Join pkgupdate execution and return exit code."
self.kill_timeout = killtimeout
# Wait for pkgupdate to exit (with timeout)
setup_alarm(self._timeout, timeout)
exit_code = self.process.wait()
signal.alarm(0)
# Wait untill we process all output
self._stdout_thread.join()
self._stderr_thread.join()
# Dump process
self.process = None
# Return exit code
return exit_code
def _stdout(self):
while True:
line = self.process.stdout.readline().decode(sys.getdefaultencoding())
self.trace_lock.acquire()
self.trace += line
self.trace_lock.release()
if not line:
break
if self.verbose:
print(line.decode(sys.getdefaultencoding()), end='')
sys.stdout.flush()
def _stderr(self):
while True:
line = self.process.stderr.readline().decode(sys.getdefaultencoding())
self.trace_lock.acquire()
self.trace += line
self.trace_lock.release()
if not line:
break
if self.verbose:
print(line, end='', file=sys.stderr)
sys.stderr.flush()
def _at_exit(self):
if self.process is not None:
self.process.terminate()
def _timeout(self):
report("Timeout run out. Terminating pkgupdate.")
self.process.terminate()
setup_alarm(self._kill_timeout, self.kill_timeout)
self.process.wait()
signal.alarm(0)