mpv: blast: shut it down properly even when sandboxed
it only cost everything. also, blast doesnt reliably clean up its pseudo devices
This commit is contained in:
parent
01fa9919fd
commit
eabd113262
|
@ -2,7 +2,10 @@
|
|||
#!nix-shell -i python3 -p "python3.withPackages (ps: [ ])" -p blast-ugjka
|
||||
# vim: set filetype=python :
|
||||
|
||||
import ctypes
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
|
||||
|
@ -16,6 +19,48 @@ DEVICE_MAP = {
|
|||
"[LG] webOS TV OLED55C9PUA": [ "-usewav" ],
|
||||
}
|
||||
|
||||
def set_pdeathsig(sig=signal.SIGTERM):
|
||||
"""
|
||||
helper function to ensure once parent process exits, its children processes will automatically die.
|
||||
see: <https://stackoverflow.com/a/43152455>
|
||||
see: <https://www.man7.org/linux/man-pages/man2/prctl.2.html>
|
||||
"""
|
||||
libc = ctypes.CDLL("libc.so.6")
|
||||
return libc.prctl(1, sig)
|
||||
|
||||
MY_PID = None
|
||||
|
||||
def reap_children(*args, **kwargs):
|
||||
global MY_PID
|
||||
logger.info("killing all children (of pid %d)", MY_PID)
|
||||
os.killpg(MY_PID, signal.SIGTERM)
|
||||
|
||||
def reap_on_exit():
|
||||
"""
|
||||
catch when the parent exits, and map that to SIGTERM for this process.
|
||||
when this process receives SIGTERM, also terminate all descendent processes.
|
||||
|
||||
this is done because:
|
||||
1. mpv invokes this, but (potentially) via the sandbox wrapper.
|
||||
2. when mpv exits, it `SIGKILL`s that sandbox wrapper.
|
||||
3. bwrap does not pass SIGKILL or SIGTERM to its child.
|
||||
4. hence, we neither receive that signal NOR can we pass it on simply by killing our immediate children
|
||||
(since any bwrap'd children wouldn't pass that signal on...)
|
||||
really, the proper fix would be on mpv's side:
|
||||
- mpv should create a new process group when it launches a command, and kill that process group on exit.
|
||||
or fix this in the sandbox wrapper:
|
||||
- why *doesn't* bwrap forward the signals?
|
||||
- there's --die-with-parent, but i can't apply that *system wide* and expect reasonably behavior
|
||||
<https://github.com/containers/bubblewrap/issues/529>
|
||||
"""
|
||||
global MY_PID
|
||||
MY_PID = os.getpid()
|
||||
# create a new process group, pgid = gid
|
||||
os.setpgid(MY_PID, MY_PID)
|
||||
|
||||
set_pdeathsig(signal.SIGTERM)
|
||||
signal.signal(signal.SIGTERM, reap_children)
|
||||
|
||||
def get_ranked_ip_addrs():
|
||||
"""
|
||||
return the IP addresses most likely to be LAN addresses
|
||||
|
@ -37,13 +82,21 @@ class Status(Enum):
|
|||
Continue = "continue"
|
||||
Error = "error"
|
||||
RedoWithFlags = "redo_with_flags"
|
||||
Launched = "launched"
|
||||
|
||||
class BlastDriver:
|
||||
parsing: ParserState | None = None
|
||||
last_write: str | None = None
|
||||
def __init__(self, blast_flags: list[str] = []):
|
||||
self.ranked_ips = get_ranked_ip_addrs()
|
||||
self.blast = subprocess.Popen(["blast", "-source", "blast.monitor"] + blast_flags, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
self.blast = subprocess.Popen(
|
||||
["blast", "-source", "blast.monitor"] + blast_flags,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
# TODO: is this pdeathsig necessary? probably not.
|
||||
preexec_fn=set_pdeathsig
|
||||
)
|
||||
self.blast_flags = list(blast_flags)
|
||||
self.receiver_names = []
|
||||
self.ips = []
|
||||
|
@ -89,9 +142,9 @@ class BlastDriver:
|
|||
elif line == "Select the lan IP address for the stream:":
|
||||
for r in self.ranked_ips:
|
||||
if r in self.ips:
|
||||
return Status.Continue, str(self.ips.index(r))
|
||||
return Status.Launched, str(self.ips.index(r))
|
||||
# fallback: just guess the best IP
|
||||
return Status.Continue, "0"
|
||||
return Status.Launched, "0"
|
||||
elif self.parsing == ParserState.Receiver:
|
||||
id_, name = line.split(": ")
|
||||
assert id_ == str(len(self.receiver_names)), (id_, self.receiver_names)
|
||||
|
@ -123,7 +176,7 @@ class BlastDriver:
|
|||
self.writeline(reply)
|
||||
return status
|
||||
|
||||
def try_blast(*args, **kwargs):
|
||||
def try_blast(*args, **kwargs) -> BlastDriver | None:
|
||||
blast = BlastDriver(*args, **kwargs)
|
||||
status = Status.Continue
|
||||
while status == Status.Continue:
|
||||
|
@ -134,14 +187,28 @@ def try_blast(*args, **kwargs):
|
|||
blast_flags = DEVICE_MAP[dev]
|
||||
logger.info("re-exec blast for %s with flags: %r", dev, blast_flags)
|
||||
blast.blast.terminate()
|
||||
try_blast(blast_flags=blast_flags)
|
||||
return try_blast(blast_flags=blast_flags)
|
||||
elif status == Status.Error:
|
||||
logger.info("blast error => terminating")
|
||||
blast.blast.terminate()
|
||||
else:
|
||||
# successfully launched
|
||||
return blast
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig()
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
try_blast()
|
||||
reap_on_exit()
|
||||
|
||||
blast = try_blast()
|
||||
|
||||
if blast is not None:
|
||||
logger.info("waiting until blast exits")
|
||||
blast.blast.wait()
|
||||
|
||||
reap_children()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -39,6 +39,11 @@ in
|
|||
sandbox.method = "bwrap";
|
||||
sandbox.whitelistAudio = true;
|
||||
sandbox.net = "clearnet";
|
||||
sandbox.extraConfig = [
|
||||
# else it fails to reap its children (or, maybe, it fails to hook its parent's death signal?)
|
||||
# might be possible to remove this, but kinda hard to see a clean way.
|
||||
"--sane-sandbox-keep-namespace" "pid"
|
||||
];
|
||||
suggestedPrograms = [ "blast-ugjka" ];
|
||||
};
|
||||
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
function invoke_blast()
|
||||
function subprocess(in_terminal, args)
|
||||
if in_terminal then
|
||||
args = { "xdg-terminal-exec", table.unpack(args) }
|
||||
end
|
||||
mp.command_native({
|
||||
name = "subprocess",
|
||||
-- TODO: mpv shutdown hangs if not run w/o xdg-terminal-exec??
|
||||
args = { "xdg-terminal-exec", "blast-to-default" },
|
||||
args = args,
|
||||
detach = false,
|
||||
capture_stdout = false,
|
||||
capture_stderr = false,
|
||||
-- capture_size=0,
|
||||
passthrough_stdin = false,
|
||||
playback_only = false,
|
||||
})
|
||||
end
|
||||
|
||||
function invoke_go2tv(in_terminal, args)
|
||||
local full_args = { "go2tv", table.unpack(args) }
|
||||
if in_terminal then
|
||||
full_args = { "xdg-terminal-exec", table.unpack(full_args) }
|
||||
end
|
||||
mp.commandv("set", "pause", "yes")
|
||||
mp.command_native({ name = "subprocess", args = full_args })
|
||||
subprocess(in_terminal, { "go2tv", table.unpack(args) })
|
||||
end
|
||||
|
||||
function invoke_go2tv_on_open_file(mode)
|
||||
|
@ -20,7 +24,7 @@ function invoke_go2tv_on_open_file(mode)
|
|||
return invoke_go2tv(true, { mode, path })
|
||||
end
|
||||
|
||||
mp.add_key_binding(nil, "blast", invoke_blast)
|
||||
mp.add_key_binding(nil, "blast", function() subprocess(false, { "blast-to-default" }) end)
|
||||
mp.add_key_binding(nil, 'go2tv-gui', function() invoke_go2tv(false, {}) end)
|
||||
mp.add_key_binding(nil, 'go2tv-video', function() invoke_go2tv_on_open_file("-v") end)
|
||||
mp.add_key_binding(nil, 'go2tv-stream', function() invoke_go2tv_on_open_file("-s") end)
|
||||
|
|
Loading…
Reference in New Issue
Block a user