Skip to content

Commit

Permalink
Merge pull request #119 from seebye/orphans
Browse files Browse the repository at this point in the history
fix process orphans
  • Loading branch information
seebye authored Oct 29, 2020
2 parents 7df7f18 + f28eea7 commit 5d5a99d
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 20 deletions.
22 changes: 10 additions & 12 deletions ueberzug/__main__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#!/usr/bin/env python3
"""Usage:
ueberzug MODULE [options]
ueberzug layer [options]
ueberzug library
ueberzug query_windows PIDS ...
Routines:
layer Display images
library Prints the path to the bash library
query_windows Orders ueberzug to search for windows.
Only for internal use.
Layer options:
-p, --parser <parser> one of json, simple, bash
Expand All @@ -26,28 +30,22 @@
This is free software, and you are welcome to redistribute it
under certain conditions.
"""
import sys

import docopt


def main():
options = docopt.docopt(__doc__)
module_name = options['MODULE']
module = None

if module_name == 'layer':
if options['layer']:
import ueberzug.layer as layer
module = layer
elif module_name == 'library':
elif options['library']:
import ueberzug.library as library
module = library

if module is None:
print("Unknown module '{}'"
.format(module_name),
file=sys.stderr)
return
elif options['query_windows']:
import ueberzug.query_windows as query_windows
module = query_windows

module.main(options)

Expand Down
4 changes: 2 additions & 2 deletions ueberzug/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def setup_tmux_hooks():
lock_directory_path = pathlib.PosixPath(tempfile.gettempdir()) / 'ueberzug'
lock_file_path = lock_directory_path / tmux_util.get_session_id()
own_pid = str(os.getpid())
command_template = 'kill -USR1 '
command_template = 'ueberzug query_windows '

try:
lock_directory_path.mkdir()
Expand Down Expand Up @@ -216,7 +216,7 @@ def main(options):
atexit.register(setup_tmux_hooks())
view.offset = tmux_util.get_offset()

with windows:
with windows, image_loader:
loop.set_default_executor(executor)

for sig in (signal.SIGINT, signal.SIGTERM):
Expand Down
23 changes: 17 additions & 6 deletions ueberzug/lib/v0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import json
import collections
import contextlib
import os
import signal

import attr

Expand Down Expand Up @@ -186,7 +188,8 @@ def start(self):
['ueberzug', 'layer'] + self.__start_options,
stdin=subprocess.PIPE,
bufsize=self.__BUFFER_SIZE_BYTES,
universal_newlines=True)
universal_newlines=True,
start_new_session=True)

def stop(self):
"""Sends SIGTERM to the running ueberzug process
Expand All @@ -195,17 +198,25 @@ def stop(self):
SIGKILL will also be send.
"""
if self.running:
timer_kill = threading.Timer(
self.__KILL_TIMEOUT_SECONDS,
self.__process.kill,
[])
timer_kill = None

try:
ueberzug_pgid = os.getpgid(self.__process.pid)
own_pgid = os.getpgid(0)
assert ueberzug_pgid != own_pgid
timer_kill = threading.Timer(
self.__KILL_TIMEOUT_SECONDS,
os.killpg,
[ueberzug_pgid, signal.SIGKILL])

self.__process.terminate()
timer_kill.start()
self.__process.communicate()
except ProcessLookupError:
pass
finally:
timer_kill.cancel()
if timer_kill is not None:
timer_kill.cancel()


class CommandTransmitter:
Expand Down
15 changes: 15 additions & 0 deletions ueberzug/loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,13 @@ def process_error(self, exception):
exception is not None):
self.error_handler(exception)

def __enter__(self):
pass

def __exit__(self, *_):
"""Finalises the image loader."""
pass


class SynchronousImageLoader(ImageLoader):
"""Implementation of ImageLoader
Expand Down Expand Up @@ -408,6 +415,10 @@ def __init__(self):
max_workers=threads_low_priority)
self.threads = threads + threads_low_priority

def __exit__(self, *_):
self.__executor_low_priority.shutdown()
self.__executor.shutdown()

def _schedule(self, function, priority):
executor = self.__executor
if priority == self.Priority.LOW:
Expand Down Expand Up @@ -437,6 +448,10 @@ def __init__(self):
.submit(id, id) \
.result()

def __exit__(self, *args):
super().__exit__(*args)
self.__executor_loader.shutdown()

@staticmethod
def _load_image_extern(path, upper_bound_size, post_load_processor):
"""This function is a wrapper for the image loading function
Expand Down
99 changes: 99 additions & 0 deletions ueberzug/query_windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import os
import signal
import errno


def get_command(pid):
"""Figures out the associated command name
of a process with the given pid.
Args:
pid (int): the pid of the process of interest
Returns:
str: the associated command name
"""
with open('/proc/{}/comm'.format(pid), 'r') as commfile:
return '\n'.join(commfile.readlines())


def is_same_command(pid0, pid1):
"""Checks whether the associated command name
of the processes of the given pids equals to each other.
Args:
pid0 (int): the pid of the process of interest
pid1 (int): the pid of another process of interest
Returns:
bool: True if both processes have
the same associated command name
"""
return get_command(pid0) == get_command(pid1)


def send_signal_safe(own_pid, target_pid):
"""Sends SIGUSR1 to a process if both
processes have the same associated command name.
(Race condition free)
Requires:
- Python 3.9+
- Linux 5.1+
Args:
own_pid (int): the pid of this process
target_pid (int):
the pid of the process to send the signal to
"""
pidfile = None
try:
pidfile = os.pidfile_open(target_pid)
if is_same_command(own_pid, target_pid):
signal.pidfd_send_signal(pidfile, signal.SIGUSR1)
except FileNotFoundError:
pass
except OSError as error:
# not sure if errno is really set..
# at least the documentation of the used functions says so..
# see e.g.: https://github.com/python/cpython/commit/7483451577916e693af6d20cf520b2cc7e2174d2#diff-99fb04b208835118fdca0d54b76a00c450da3eaff09d2b53e8a03d63bbe88e30R1279-R1281
# and https://docs.python.org/3/c-api/exceptions.html#c.PyErr_SetFromErrno

# caused by either pidfile_open or pidfd_send_signal
if error.errno != errno.ESRCH:
raise
# else: the process is death
finally:
if pidfile is not None:
os.close(pidfile)


def send_signal_unsafe(own_pid, target_pid):
"""Sends SIGUSR1 to a process if both
processes have the same associated command name.
(Race condition if process dies)
Args:
own_pid (int): the pid of this process
target_pid (int):
the pid of the process to send the signal to
"""
try:
if is_same_command(own_pid, target_pid):
os.kill(target_pid, signal.SIGUSR1)
except (FileNotFoundError, ProcessLookupError):
pass


def main(options):
# assumption:
# started by calling the programs name
# ueberzug layer and
# ueberzug query_windows
own_pid = os.getpid()

for pid in options['PIDS']:
try:
send_signal_safe(own_pid, int(pid))
except AttributeError:
send_signal_unsafe(own_pid, int(pid))

0 comments on commit 5d5a99d

Please sign in to comment.