-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathinstance.py
More file actions
78 lines (68 loc) · 3.19 KB
/
Copy pathinstance.py
File metadata and controls
78 lines (68 loc) · 3.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
"""Socket-as-single-instance-lock for the control endpoint (headless-core H2).
The control socket doubles as the instance lock: if a process is already
serving the endpoint (answers ``hello``), a second launch should defer to
it rather than fight over the HID device. On POSIX a crashed previous run
can leave a stale socket file that would block ``Listener`` from binding;
``clear_stale_endpoint`` removes it only once we've confirmed nothing is
listening.
"""
import os
import stat
import sys
from multiprocessing.connection import AuthenticationError, Client
from polyhost.server import protocol
# probe_existing outcomes. Only STALE means "nothing is really there, safe to
# unlink the socket and bind"; the other three all mean a real process owns the
# endpoint and a second host must defer.
LIVE = "live" # a compatible control server answered hello
INCOMPATIBLE = "incompatible" # a process answered but with a bad/absent hello
AUTH_MISMATCH = "auth" # a process is listening but rejected our authkey
STALE = "stale" # nothing listening (refused / not found / EOF pre-hello)
def probe_existing(address=None, authkey=None, timeout=0.5) -> str:
"""Classify the control endpoint: LIVE, INCOMPATIBLE, AUTH_MISMATCH or STALE.
Connects, reads the server's hello, and verifies the control-protocol
version. Distinguishing the failure modes matters: a live-but-incompatible
or auth-mismatched endpoint must NOT be treated as stale and unlinked
(that would let a second host start and fight over the HID device)."""
address = address or protocol.endpoint_address()
authkey = authkey or protocol.load_or_create_authkey()
try:
conn = Client(address, authkey=authkey)
except AuthenticationError:
return AUTH_MISMATCH
except (FileNotFoundError, ConnectionError, OSError):
# Nothing accepted the connection — no listener / stale socket node.
return STALE
try:
# Past Client(): something accepted, so the endpoint is in use. Any
# failure from here on is INCOMPATIBLE, never STALE — we must not unlink
# a socket a real process is bound to.
if not conn.poll(timeout):
return INCOMPATIBLE
msg = protocol.recv_message(conn)
if msg.get("method") != protocol.HELLO:
return INCOMPATIBLE
ok, _ = protocol.check_hello(msg.get("params") or {})
return LIVE if ok else INCOMPATIBLE
except (EOFError, OSError):
return INCOMPATIBLE
finally:
try:
conn.close()
except OSError:
pass
def clear_stale_endpoint(address=None) -> None:
"""Remove a stale POSIX socket file so a fresh Listener can bind.
Only call when :func:`probe_existing` returned ``STALE``. No-op on Windows
(named pipes don't persist as files). Guarded so it only ever unlinks an
actual socket node — never a regular file that happens to share the path."""
if sys.platform == "win32":
return
address = address or protocol.endpoint_address()
try:
if stat.S_ISSOCK(os.lstat(address).st_mode):
os.unlink(address)
except FileNotFoundError:
pass
except OSError:
pass