Repository navigation
Expand file tree
/
Copy pathtest-linux-service.py
More file actions
199 lines (174 loc) · 7.99 KB
/
Copy pathtest-linux-service.py
File metadata and controls
199 lines (174 loc) · 7.99 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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#!/usr/bin/env python3
"""Display-free single-instance regression. Never uses personal settings or a microphone.
One Hex process owns a data root. A second launch forwards to it and exits;
`hex` commands forward over the same socket and fail fast without it.
"""
import json
import os
from pathlib import Path
import socket
import stat
import struct
import subprocess
import sys
import tempfile
import time
binary = str(Path(sys.argv[1]).resolve())
def wait_for(check, timeout=8):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
result = check()
if result:
return result
time.sleep(0.025)
raise AssertionError("timed out waiting for Hex")
with tempfile.TemporaryDirectory(prefix="hex-instance-", dir=os.environ.get("TMPDIR")) as directory:
root = Path(directory)
support = root / "data"
support.mkdir(mode=0o700)
# Never listen: no microphone, no shortcuts portal registration.
(support / "settings.json").write_text(json.dumps({"enable_on_launch": False}))
alsa = root / "alsa.conf"
alsa.write_text("# No physical audio devices in this test.\n")
# Custom data roots must never start, stop, or restart the user's real unit.
fakebin = root / "bin"
fakebin.mkdir()
marker = root / "unexpected-systemctl"
fake = fakebin / "systemctl"
fake.write_text(f'#!/bin/sh\ntouch "{marker}"\necho loaded\nexit 0\n')
fake.chmod(0o755)
env = os.environ | {
"HEX_APPLICATION_SUPPORT_DIR": str(support),
# Hex tidies its launcher and autostart entries at startup.
"XDG_DATA_HOME": str(root / "xdg-data"),
"XDG_CONFIG_HOME": str(root / "xdg-config"),
"ALSA_CONFIG_PATH": str(alsa),
"PATH": f'{fakebin}:{os.environ["PATH"]}',
}
# No display: a window request fails the window, never Hex.
for key in ("DISPLAY", "WAYLAND_DISPLAY", "HEX_SYSTEMD_UNIT", "NOTIFY_SOCKET"):
env.pop(key, None)
path = support / "service.sock"
children = []
def spawn(*arguments):
child = subprocess.Popen([binary, *arguments], env=env, stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
children.append(child)
return child
def run(*arguments, timeout=10):
return subprocess.run([binary, *arguments], env=env, capture_output=True, text=True,
timeout=timeout)
def connection():
client = socket.socket(socket.AF_UNIX)
client.settimeout(4)
client.connect(str(path))
return client
def exact(client, size):
data = b""
while len(data) < size:
chunk = client.recv(size - len(data))
assert chunk, "Hex disconnected"
data += chunk
return data
def call(client, request="Status", version=2):
body = json.dumps({"version": version, "request": request}).encode()
client.sendall(struct.pack(">I", len(body)) + body)
size, = struct.unpack(">I", exact(client, 4))
return json.loads(exact(client, size))
def status():
try:
with connection() as client:
return call(client)["status"]
except (OSError, AssertionError):
return None
try:
# Without an instance every forwarded command fails fast.
for command in (["status"], ["dictation", "start"], ["paste-last"]):
started = time.monotonic()
missing = run(*command)
assert missing.returncode != 0 and "not running" in missing.stderr, missing.stderr
assert time.monotonic() - started < 2
assert run("quit").returncode == 3, "quitting nothing says Hex isn't running"
# `hex app --hidden` with no unit becomes the instance in this process.
instance = spawn("app", "--hidden")
state = wait_for(status)
assert state["pid"] == instance.pid
assert state["window_open"] is False
assert state["desktop"]["listener"]["status"] == "Disabled"
assert stat.S_IMODE(path.stat().st_mode) == 0o600
assert stat.S_IMODE(support.stat().st_mode) == 0o700
# A second launch forwards and exits 0, leaving the same instance;
# `hex start` finds it running.
for command in (["start"], ["app", "--hidden"], ["service"]):
forwarded = run(*command)
assert forwarded.returncode == 0, (command, forwarded.stderr)
assert status()["pid"] == instance.pid
# Activate reaches the window seam; without a display only the window fails.
token = run("app")
assert token.returncode == 0, token.stderr
time.sleep(0.5)
assert instance.poll() is None
assert status()["pid"] == instance.pid
with connection() as client:
reply = call(client, {"Activate": {
"activation": {"token": "test-token", "startup_id": None},
"show": False,
"session": state["session"],
}})
assert reply["error"] is None and reply["status"]["pid"] == instance.pid
printed = run("status", "--json")
assert printed.returncode == 0, printed.stderr
printed = json.loads(printed.stdout)
assert printed["status"] == "status" and printed["pid"] == instance.pid
assert printed["listener"]["running"] is False
# Control commands ride the same socket.
assert run("settings", "set", "sound_effects", "off").returncode == 0
assert run("settings", "get", "sound_effects").stdout.strip() == "false"
refused = run("window", "story", "HUD")
assert refused.returncode == 1 and "macOS-only" in refused.stderr, refused.stderr
# Keybinding commands need a listening Hex; they fail fast without one,
# with one line rather than an error report.
refused = run("dictation", "toggle")
assert refused.returncode != 0 and "not listening" in refused.stderr, refused.stderr
assert refused.stderr.startswith("hex: ") and refused.stderr.count("\n") == 1, refused.stderr
# Version 1 (service/Settings) clients get a version error, not a hang.
with connection() as client:
reply = call(client, "Snapshot", version=1)
assert reply["version"] == 2 and reply["error"]
with connection() as client:
client.sendall(struct.pack(">I", 128 * 1024 + 1))
assert client.recv(1) == b""
# A stalled request cannot stall other clients.
with connection() as stalled:
stalled.sendall(b"\x00")
assert status()["pid"] == instance.pid
assert stalled.recv(1) == b""
# A crash leaves a socket, not a permanent lock.
instance.kill()
instance.wait(timeout=5)
assert path.exists()
missing = run("paste-last")
assert missing.returncode != 0 and "not running" in missing.stderr, missing.stderr
assert missing.stderr.count("\n") == 1, missing.stderr
# Two racing launches produce one instance; the other forwards and exits 0.
first, second = spawn("app", "--hidden"), spawn("app", "--hidden")
state = wait_for(status)
owners = {first.pid, second.pid}
assert state["pid"] in owners
loser = second if state["pid"] == first.pid else first
assert loser.wait(timeout=8) == 0
instance = first if loser is second else second
# Quit drains and exits 0, removing the socket and keeping settings.
assert run("quit").returncode == 0
assert instance.wait(timeout=8) == 0
assert not path.exists()
assert (support / "settings.json").exists()
for command in (["quit"], ["dictation", "start"], ["paste-last"]):
assert run(*command).returncode == 3, command
assert not marker.exists(), "a custom data root ran systemctl"
finally:
for child in children:
if child.poll() is None:
child.kill()
child.wait(timeout=5)
print("Linux single-instance checks passed.")