okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petconnect to local kernels from erlang instead of keeping a python bridge per kernelce3e329

main
5.2 KiB157 linesraw
1"""Copy a detached kernel's frames between this process's stdio and its socket.
2
3    albedo_bridge.py start <run_dir> <modules-json>
4    albedo_bridge.py attach <run_dir>
5    albedo_bridge.py launch <run_dir> <modules-json>
6    albedo_bridge.py --frame
7
8`--frame` takes the same arguments from its first stdin frame instead,
9`{"bridge": {"argv": [...], "cwd": dir}}`, and starts in `cwd` (exit status 4
10when it is no directory): the remote form, so the daemon's ssh command line
11never carries a path or the module list.
12
13The daemon talks to this process as it once talked to the kernel itself, over
144-byte-length-framed stdio, and writes the attach frame first. `start` reads
15that frame to learn the kernel's token, starts the albedo_kernel.py beside this
16file in its own session (the token travels over the kernel's stdin, never
17argv), and waits for its socket; `attach` connects to a kernel that is already running. Either way this
18process announces the bundle it runs from, then copies bytes both ways until
19one side closes. Killing it never touches the kernel.
20
21`launch` only starts the kernel, its token read as one stdin line, and exits 0
22once the socket answers: a local daemon connects to that socket itself, so no
23bridge stays running beside a local kernel.
24
25Exit status 3 means no kernel is there to attach to, so the daemon can tell a
26kernel that is gone from a connection that merely dropped.
27"""
28
29from __future__ import annotations
30
31import errno
32import os
33import socket
34import subprocess
35import sys
36import threading
37import time
38from collections.abc import Callable
39
40import albedo_bundle
41import albedo_link
42
43GONE = 3
44NO_FOLDER = 4
45START_TIMEOUT = 10.0  # seconds a starting kernel has to bind its socket
46
47
48def connect(run_dir: str) -> socket.socket | None:
49    """The kernel's socket, or None when nothing listens there."""
50    connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
51    try:
52        os.chdir(run_dir)
53        connection.connect(albedo_link.SOCKET)
54        return connection
55    except OSError as error:
56        connection.close()
57        if error.errno in (errno.ENOENT, errno.ECONNREFUSED, errno.ENOTDIR):
58            return None
59        raise
60
61
62KERNEL = os.path.join(os.path.dirname(os.path.abspath(__file__)), "albedo_kernel.py")
63
64
65def start(run_dir: str, modules: str, token: str) -> socket.socket:
66    os.makedirs(run_dir, mode=0o700, exist_ok=True)
67    with open(os.path.join(run_dir, "kernel.log"), "ab") as log:
68        kernel = subprocess.Popen(
69            [sys.executable, "-u", KERNEL, "--run", run_dir, modules],
70            stdin=subprocess.PIPE,
71            stdout=subprocess.DEVNULL,
72            stderr=log,
73            start_new_session=True,
74            close_fds=True,
75        )
76    assert kernel.stdin is not None
77    kernel.stdin.write(token.encode() + b"\n")
78    kernel.stdin.close()
79    deadline = time.monotonic() + START_TIMEOUT
80    while time.monotonic() < deadline:
81        connection = connect(run_dir)
82        if connection is not None:
83            return connection
84        if kernel.poll() is not None:
85            break
86        time.sleep(0.02)
87    sys.stderr.write(
88        f"albedo_bridge: kernel did not start (see {run_dir}/kernel.log)\n"
89    )
90    sys.exit(GONE)
91
92
93def stdin(size: int) -> bytes:
94    return os.read(0, size)
95
96
97def stdout(data: bytes) -> int:
98    return os.write(1, data)
99
100
101def pump(source: Callable[[int], bytes], sink: Callable[[bytes], int | None]) -> None:
102    try:
103        while data := source(65536):
104            albedo_link.write_all(sink, data)
105    except OSError:
106        pass
107    os._exit(0)
108
109
110def framed() -> list[str]:
111    """The arguments a remote daemon sent as the first frame."""
112    first = albedo_link.read_frame(stdin)
113    spec = first.get("bridge") if isinstance(first, dict) else None
114    argv = spec.get("argv") if isinstance(spec, dict) else None
115    if not isinstance(argv, list) or not all(isinstance(a, str) for a in argv):
116        sys.exit(2)
117    cwd = spec.get("cwd") if isinstance(spec, dict) else None
118    if isinstance(cwd, str):
119        try:
120            os.chdir(cwd)
121        except OSError:
122            sys.exit(NO_FOLDER)
123    return [sys.argv[0], *argv]
124
125
126def main(argv: list[str]) -> None:
127    if argv[1:] == ["--frame"]:
128        argv = framed()
129    mode, run_dir = argv[1], argv[2]
130    if mode == "launch":
131        start(run_dir, argv[3], sys.stdin.readline().strip()).close()
132        return
133    if mode == "start":
134        attach = albedo_link.read_frame(stdin)
135        if not isinstance(attach, dict) or not isinstance(attach.get("attach"), dict):
136            sys.exit(2)
137        token = attach["attach"].get("token")
138        if not isinstance(token, str):
139            sys.exit(2)
140        connection = start(run_dir, argv[3], token)
141        first = albedo_link.encode(attach)
142    else:
143        found = connect(run_dir)
144        if found is None:
145            sys.exit(GONE)
146        connection, first = found, None
147    albedo_link.write_frame(
148        stdout, albedo_link.encode({"bridge": {"bundle": albedo_bundle.digest()}})
149    )
150    if first is not None:
151        albedo_link.write_frame(connection.send, first)
152    threading.Thread(target=pump, args=(stdin, connection.send), daemon=True).start()
153    pump(connection.recv, stdout)
154
155
156if __name__ == "__main__":
157    main(sys.argv)