anaconda/anaconda-40.22.3.13/pyanaconda/core/process_watchers.py
2024-11-14 21:39:56 -08:00

181 lines
6.8 KiB
Python

# Classes to watch for an external application.
#
# Copyright (C) 2018 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details. You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
import os
import signal
from pyanaconda.core.glib import child_watch_add, source_remove
from pyanaconda.errors import ExitError
__all__ = ["PidWatcher", "WatchProcesses"]
class PidWatcher(object):
"""Watch for process and call callback when the process ends."""
def __init__(self):
self._id = 0
def watch_process(self, pid, callback, *args, **kwargs):
"""Watch for process with given pid to exit then call `callback`.
:param pid: Process ID.
:type pid: int
:param callback: Callback to call when process ends.
:type callback: A function.
:param args: Arguments passed to the callback.
:param kwargs: Keyword arguments passed to the callback.
"""
self._id = child_watch_add(pid, callback, *args, **kwargs)
def cancel(self):
"""Cancel watching."""
source_remove(self._id)
self._id = 0
class WatchProcesses(object):
"""Static class for watching external processes."""
# Dictionary of processes to watch in the form {pid: [name, GLib event source id], ...}
_forever_pids = {}
# Set to True if process watching is handled by GLib
_watch_process_glib = False
_watch_process_handler_set = False
@classmethod
def _raise_exit_error(cls, statuses):
"""Raise an error on process exit. The argument is a list of tuples
of the form [(name, status), ...] with statuses in the subprocess
format (>=0 is return codes, <0 is signal)
"""
exn_message = []
for proc_name, status in statuses:
if status >= 0:
status_str = "with status %s" % status
else:
status_str = "on signal %s" % -status
exn_message.append("%s exited %s" % (proc_name, status_str))
raise ExitError(", ".join(exn_message))
@classmethod
def _sigchld_handler(cls, num=None, frame=None):
"""Signal handler used with watchProcess"""
# Check whether anything in the list of processes being watched has
# exited. We don't want to call waitpid(-1), since that would break
# anything else using wait/waitpid (like the subprocess module).
exited_pids = []
exit_statuses = []
for child_pid in cls._forever_pids:
try:
pid_result, status = os.waitpid(child_pid, os.WNOHANG)
except ChildProcessError:
continue
if pid_result:
proc_name = cls._forever_pids[child_pid][0]
exited_pids.append(child_pid)
# Convert the wait-encoded status to the format used by subprocess
if os.WIFEXITED(status):
sub_status = os.WEXITSTATUS(status)
else:
# subprocess uses negative return codes to indicate signal exit
sub_status = -os.WTERMSIG(status)
exit_statuses.append((proc_name, sub_status))
for child_pid in exited_pids:
if cls._forever_pids[child_pid][1]:
source_remove(cls._forever_pids[child_pid][1])
del cls._forever_pids[child_pid]
if exit_statuses:
cls._raise_exit_error(exit_statuses)
@classmethod
def _watch_process_cb(cls, pid, status, proc_name):
"""GLib callback used with watchProcess."""
# Convert the wait-encoded status to the format used by subprocess
if os.WIFEXITED(status):
sub_status = os.WEXITSTATUS(status)
else:
# subprocess uses negative return codes to indicate signal exit
sub_status = -os.WTERMSIG(status)
cls._raise_exit_error([(proc_name, sub_status)])
@classmethod
def watch_process(cls, proc, name):
"""Watch for a process exit, and raise a ExitError when it does.
This method installs a SIGCHLD signal handler and thus interferes
the child_watch_add methods in GLib. Use watchProcessGLib to convert
to GLib mode if using a GLib main loop.
Since the SIGCHLD handler calls wait() on the watched process, this call
cannot be combined with Popen.wait() or Popen.communicate, and also
doing so wouldn't make a whole lot of sense.
:param proc: The Popen object for the process
:param name: The name of the process
"""
if not cls._watch_process_glib and not cls._watch_process_handler_set:
signal.signal(signal.SIGCHLD, cls._sigchld_handler)
cls._watch_process_handler_set = True
# Add the PID to the dictionary
# The second item in the list is for the GLib event source id and will be
# replaced with the id once we have one.
cls._forever_pids[proc.pid] = [name, None]
# If GLib is watching processes, add a watcher. child_watch_add checks if
# the process has already exited.
if cls._watch_process_glib:
cls._forever_pids[proc.id][1] = child_watch_add(proc.pid, cls._watch_process_cb, name)
else:
# Check that the process didn't already exit
if proc.poll() is not None:
del cls._forever_pids[proc.pid]
cls._raise_exit_error([(name, proc.returncode)])
@classmethod
def unwatch_process(cls, proc):
"""Unwatch a process watched by watchProcess.
:param proc: The Popen object for the process.
"""
if cls._forever_pids[proc.pid][1]:
source_remove(cls._forever_pids[proc.pid][1])
del cls._forever_pids[proc.pid]
@classmethod
def unwatch_all_processes(cls):
"""Clear the watched process list."""
for child_pid in cls._forever_pids:
if cls._forever_pids[child_pid][1]:
source_remove(cls._forever_pids[child_pid][1])
cls._forever_pids = {}