arrayview 0.26.2__py3-none-any.whl → 0.27.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arrayview/_analysis.py +19 -4
- arrayview/_launch_plan.py +285 -0
- arrayview/_launcher.py +133 -55
- arrayview/_platform.py +22 -9
- arrayview/_routes_analysis.py +442 -3
- arrayview/_server.py +1 -0
- arrayview/_stdio_server.py +2 -1
- arrayview/_viewer.html +3065 -576
- arrayview/_vscode_browser.py +16 -0
- arrayview/_vscode_extension.py +1 -1
- arrayview/_vscode_signal.py +31 -57
- arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.26.2.dist-info → arrayview-0.27.0.dist-info}/METADATA +7 -7
- {arrayview-0.26.2.dist-info → arrayview-0.27.0.dist-info}/RECORD +17 -16
- {arrayview-0.26.2.dist-info → arrayview-0.27.0.dist-info}/WHEEL +0 -0
- {arrayview-0.26.2.dist-info → arrayview-0.27.0.dist-info}/entry_points.txt +0 -0
- {arrayview-0.26.2.dist-info → arrayview-0.27.0.dist-info}/licenses/LICENSE +0 -0
arrayview/_analysis.py
CHANGED
|
@@ -67,7 +67,7 @@ def _safe_float(v) -> float | None:
|
|
|
67
67
|
return f if np.isfinite(f) else None
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
def _histogram_payload(finite: np.ndarray, bins: int) -> dict:
|
|
70
|
+
def _histogram_payload(finite: np.ndarray, bins: int, log_bins: bool = False) -> dict:
|
|
71
71
|
"""Build a histogram response from finite values."""
|
|
72
72
|
if finite.size == 0:
|
|
73
73
|
return {"counts": [], "edges": [], "vmin": 0.0, "vmax": 1.0}
|
|
@@ -81,7 +81,13 @@ def _histogram_payload(finite: np.ndarray, bins: int) -> dict:
|
|
|
81
81
|
"vmax": vmax,
|
|
82
82
|
}
|
|
83
83
|
bins = max(8, min(int(bins), 512))
|
|
84
|
-
|
|
84
|
+
if log_bins and vmin > 0:
|
|
85
|
+
log_data = np.log10(finite)
|
|
86
|
+
_, log_edges = np.histogram(log_data, bins=bins)
|
|
87
|
+
edges = np.power(10.0, log_edges)
|
|
88
|
+
counts, _ = np.histogram(finite, bins=edges)
|
|
89
|
+
else:
|
|
90
|
+
counts, edges = np.histogram(finite, bins=bins)
|
|
85
91
|
return {
|
|
86
92
|
"counts": counts.tolist(),
|
|
87
93
|
"edges": [float(e) for e in edges],
|
|
@@ -113,7 +119,8 @@ def _slice_histogram(
|
|
|
113
119
|
data = apply_complex_mode(raw, complex_mode)
|
|
114
120
|
finite = data.ravel()
|
|
115
121
|
finite = finite[np.isfinite(finite)]
|
|
116
|
-
|
|
122
|
+
log_bins = qmri_role in ("t1", "t2")
|
|
123
|
+
return _histogram_payload(finite, bins, log_bins=log_bins)
|
|
117
124
|
|
|
118
125
|
|
|
119
126
|
def _parse_fixed_indices(fixed_indices: str) -> dict[int, int]:
|
|
@@ -172,6 +179,7 @@ def _volume_histogram(
|
|
|
172
179
|
session, dim_x, dim_y, scroll_dim, scroll_dims
|
|
173
180
|
)
|
|
174
181
|
|
|
182
|
+
log_bins = qmri_role in ("t1", "t2")
|
|
175
183
|
cache_key = (
|
|
176
184
|
dim_x,
|
|
177
185
|
dim_y,
|
|
@@ -179,6 +187,8 @@ def _volume_histogram(
|
|
|
179
187
|
tuple(sorted(fixed.items())),
|
|
180
188
|
complex_mode,
|
|
181
189
|
qmri_role,
|
|
190
|
+
bins,
|
|
191
|
+
log_bins,
|
|
182
192
|
)
|
|
183
193
|
if not hasattr(session, "_volume_hist_cache"):
|
|
184
194
|
session._volume_hist_cache = {}
|
|
@@ -224,7 +234,7 @@ def _volume_histogram(
|
|
|
224
234
|
pixels.append(finite)
|
|
225
235
|
|
|
226
236
|
if pixels:
|
|
227
|
-
result = _histogram_payload(np.concatenate(pixels), bins)
|
|
237
|
+
result = _histogram_payload(np.concatenate(pixels), bins, log_bins=log_bins)
|
|
228
238
|
else:
|
|
229
239
|
result = {"counts": [], "edges": [], "vmin": 0.0, "vmax": 1.0}
|
|
230
240
|
session._volume_hist_cache[cache_key] = {
|
|
@@ -241,14 +251,19 @@ def _lebesgue_slice(
|
|
|
241
251
|
indices,
|
|
242
252
|
complex_mode: int = 0,
|
|
243
253
|
log_scale: bool = False,
|
|
254
|
+
qmri_role: str = "",
|
|
244
255
|
) -> np.ndarray:
|
|
245
256
|
"""Return float32 slice data for Lebesgue mode."""
|
|
257
|
+
from arrayview._synthetic_mri import normalize_relaxation_ms
|
|
258
|
+
|
|
246
259
|
idx_tuple = (
|
|
247
260
|
tuple(int(v) for v in indices.split(","))
|
|
248
261
|
if isinstance(indices, str)
|
|
249
262
|
else tuple(indices)
|
|
250
263
|
)
|
|
251
264
|
raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
|
|
265
|
+
if qmri_role:
|
|
266
|
+
raw = normalize_relaxation_ms(raw, qmri_role)
|
|
252
267
|
data = apply_complex_mode(raw, complex_mode).astype(np.float32)
|
|
253
268
|
if log_scale:
|
|
254
269
|
data = np.log10(np.abs(data) + 1).astype(np.float32)
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Data-only launch routing primitives and environment snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import socket
|
|
10
|
+
import sys
|
|
11
|
+
import urllib.request
|
|
12
|
+
|
|
13
|
+
_LOOPBACK_HOST = "localhost"
|
|
14
|
+
_PING_TIMEOUT_SECONDS = 0.2
|
|
15
|
+
|
|
16
|
+
_SNAPSHOT_ENV_KEYS = (
|
|
17
|
+
"ARRAYVIEW_WINDOW",
|
|
18
|
+
"ARRAYVIEW_WINDOW_ID",
|
|
19
|
+
"DISPLAY",
|
|
20
|
+
"TERM_PROGRAM",
|
|
21
|
+
"VSCODE_AGENT_FOLDER",
|
|
22
|
+
"VSCODE_INJECTION",
|
|
23
|
+
"VSCODE_IPC_HOOK_CLI",
|
|
24
|
+
"WAYLAND_DISPLAY",
|
|
25
|
+
"SSH_CLIENT",
|
|
26
|
+
"SSH_CONNECTION",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _StrEnum(str, Enum):
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return self.value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Invocation(_StrEnum):
|
|
36
|
+
CLI = "cli"
|
|
37
|
+
PYTHON = "python"
|
|
38
|
+
JUPYTER = "jupyter"
|
|
39
|
+
JULIA = "julia"
|
|
40
|
+
STDIO = "stdio"
|
|
41
|
+
CODEX = "codex"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Environment(_StrEnum):
|
|
45
|
+
TERMINAL = "terminal"
|
|
46
|
+
VSCODE_LOCAL = "vscode_local"
|
|
47
|
+
VSCODE_REMOTE = "vscode_remote"
|
|
48
|
+
SSH = "ssh"
|
|
49
|
+
JUPYTER = "jupyter"
|
|
50
|
+
JULIA = "julia"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Transport(_StrEnum):
|
|
54
|
+
HTTP = "http"
|
|
55
|
+
STDIO_FILE = "stdio_file"
|
|
56
|
+
STDIO_SHM = "stdio_shm"
|
|
57
|
+
NONE = "none"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ServerOwner(_StrEnum):
|
|
61
|
+
EXISTING = "existing"
|
|
62
|
+
SPAWNED_DAEMON = "spawned_daemon"
|
|
63
|
+
IN_PROCESS = "in_process"
|
|
64
|
+
PERSISTENT = "persistent"
|
|
65
|
+
EXTERNAL = "external"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Display(_StrEnum):
|
|
69
|
+
NATIVE = "native"
|
|
70
|
+
BROWSER = "browser"
|
|
71
|
+
VSCODE = "vscode"
|
|
72
|
+
INLINE = "inline"
|
|
73
|
+
NONE = "none"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Registration(_StrEnum):
|
|
77
|
+
HTTP_LOAD = "http_load"
|
|
78
|
+
DAEMON_STARTUP = "daemon_startup"
|
|
79
|
+
IN_PROCESS_SESSION = "in_process_session"
|
|
80
|
+
STDIO_REGISTER = "stdio_register"
|
|
81
|
+
RELAY = "relay"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class ServerSnapshot:
|
|
86
|
+
port: int
|
|
87
|
+
port_busy: bool
|
|
88
|
+
arrayview_server_alive: bool
|
|
89
|
+
server_pid: int | None = None
|
|
90
|
+
server_hostname: str | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class LaunchEnvironmentSnapshot:
|
|
95
|
+
invocation: Invocation
|
|
96
|
+
requested_window: str | None
|
|
97
|
+
environment: Environment
|
|
98
|
+
platform: str
|
|
99
|
+
env_vars: dict[str, str]
|
|
100
|
+
config_default: str | None
|
|
101
|
+
native_backend: str | None
|
|
102
|
+
server: ServerSnapshot
|
|
103
|
+
in_jupyter: bool
|
|
104
|
+
in_julia: bool
|
|
105
|
+
in_vscode_terminal: bool
|
|
106
|
+
is_vscode_remote: bool
|
|
107
|
+
in_vscode_tunnel: bool
|
|
108
|
+
ssh_connection: bool
|
|
109
|
+
ssh_client: bool
|
|
110
|
+
hostname: str
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> dict:
|
|
113
|
+
return asdict(self)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class LaunchRequest:
|
|
118
|
+
port: int
|
|
119
|
+
requested_window: str | None = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class LaunchPlan:
|
|
124
|
+
invocation: Invocation
|
|
125
|
+
environment: Environment
|
|
126
|
+
transport: Transport
|
|
127
|
+
server_owner: ServerOwner
|
|
128
|
+
display: Display
|
|
129
|
+
registration: Registration
|
|
130
|
+
fallback_display: Display | None = None
|
|
131
|
+
fallback_allowed: bool = False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def snapshot_launch_environment(
|
|
135
|
+
port: int,
|
|
136
|
+
invocation: Invocation | str,
|
|
137
|
+
requested_window: str | None = None,
|
|
138
|
+
) -> LaunchEnvironmentSnapshot:
|
|
139
|
+
"""Capture launch-relevant facts without importing the server stack."""
|
|
140
|
+
inv = _coerce_invocation(invocation)
|
|
141
|
+
env_vars = _snapshot_env_vars()
|
|
142
|
+
in_jupyter = _platform_bool("_in_jupyter")
|
|
143
|
+
in_julia = _platform_bool("_is_julia_env")
|
|
144
|
+
in_vscode_terminal = _platform_bool("_in_vscode_terminal")
|
|
145
|
+
is_vscode_remote = _platform_bool("_is_vscode_remote")
|
|
146
|
+
in_vscode_tunnel = _platform_bool("_in_vscode_tunnel")
|
|
147
|
+
ssh_connection = bool(os.environ.get("SSH_CONNECTION"))
|
|
148
|
+
ssh_client = bool(os.environ.get("SSH_CLIENT"))
|
|
149
|
+
environment = _classify_environment(
|
|
150
|
+
in_jupyter=in_jupyter,
|
|
151
|
+
in_julia=in_julia,
|
|
152
|
+
in_vscode_terminal=in_vscode_terminal,
|
|
153
|
+
is_vscode_remote=is_vscode_remote,
|
|
154
|
+
ssh_connection=ssh_connection,
|
|
155
|
+
ssh_client=ssh_client,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return LaunchEnvironmentSnapshot(
|
|
159
|
+
invocation=inv,
|
|
160
|
+
requested_window=requested_window,
|
|
161
|
+
environment=environment,
|
|
162
|
+
platform=sys.platform,
|
|
163
|
+
env_vars=env_vars,
|
|
164
|
+
config_default=_config_window_default(environment.value),
|
|
165
|
+
native_backend=_native_window_gui(),
|
|
166
|
+
server=_server_snapshot(port),
|
|
167
|
+
in_jupyter=in_jupyter,
|
|
168
|
+
in_julia=in_julia,
|
|
169
|
+
in_vscode_terminal=in_vscode_terminal,
|
|
170
|
+
is_vscode_remote=is_vscode_remote,
|
|
171
|
+
in_vscode_tunnel=in_vscode_tunnel,
|
|
172
|
+
ssh_connection=ssh_connection,
|
|
173
|
+
ssh_client=ssh_client,
|
|
174
|
+
hostname=socket.gethostname(),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _coerce_invocation(value: Invocation | str) -> Invocation:
|
|
179
|
+
if isinstance(value, Invocation):
|
|
180
|
+
return value
|
|
181
|
+
return Invocation(value)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _snapshot_env_vars() -> dict[str, str]:
|
|
185
|
+
return {key: os.environ[key] for key in _SNAPSHOT_ENV_KEYS if key in os.environ}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _platform_bool(name: str) -> bool:
|
|
189
|
+
try:
|
|
190
|
+
from arrayview import _platform
|
|
191
|
+
|
|
192
|
+
return bool(getattr(_platform, name)())
|
|
193
|
+
except Exception:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _native_window_gui() -> str | None:
|
|
198
|
+
try:
|
|
199
|
+
from arrayview._platform import _native_window_gui as native_window_gui
|
|
200
|
+
|
|
201
|
+
return native_window_gui()
|
|
202
|
+
except Exception:
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _config_window_default(environment: str) -> str | None:
|
|
207
|
+
try:
|
|
208
|
+
from arrayview._config import load_config
|
|
209
|
+
|
|
210
|
+
cfg = load_config()
|
|
211
|
+
except Exception:
|
|
212
|
+
return None
|
|
213
|
+
window_cfg = cfg.get("window", {})
|
|
214
|
+
if not isinstance(window_cfg, dict):
|
|
215
|
+
return None
|
|
216
|
+
env_key = "vscode" if environment in {"vscode_local", "vscode_remote"} else environment
|
|
217
|
+
value = window_cfg.get(env_key) or window_cfg.get("default")
|
|
218
|
+
if isinstance(value, str):
|
|
219
|
+
return value.strip().lower() or None
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _classify_environment(
|
|
224
|
+
*,
|
|
225
|
+
in_jupyter: bool,
|
|
226
|
+
in_julia: bool,
|
|
227
|
+
in_vscode_terminal: bool,
|
|
228
|
+
is_vscode_remote: bool,
|
|
229
|
+
ssh_connection: bool,
|
|
230
|
+
ssh_client: bool,
|
|
231
|
+
) -> Environment:
|
|
232
|
+
if in_jupyter:
|
|
233
|
+
return Environment.JUPYTER
|
|
234
|
+
if in_julia:
|
|
235
|
+
return Environment.JULIA
|
|
236
|
+
if is_vscode_remote:
|
|
237
|
+
return Environment.VSCODE_REMOTE
|
|
238
|
+
if in_vscode_terminal:
|
|
239
|
+
return Environment.VSCODE_LOCAL
|
|
240
|
+
if ssh_connection or ssh_client:
|
|
241
|
+
return Environment.SSH
|
|
242
|
+
return Environment.TERMINAL
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _server_snapshot(port: int) -> ServerSnapshot:
|
|
246
|
+
payload = _ping_arrayview_server(port)
|
|
247
|
+
return ServerSnapshot(
|
|
248
|
+
port=port,
|
|
249
|
+
port_busy=_port_busy(port),
|
|
250
|
+
arrayview_server_alive=payload is not None,
|
|
251
|
+
server_pid=_int_or_none(payload.get("pid")) if payload else None,
|
|
252
|
+
server_hostname=_str_or_none(payload.get("hostname")) if payload else None,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _port_busy(port: int) -> bool:
|
|
257
|
+
try:
|
|
258
|
+
with socket.create_connection(
|
|
259
|
+
(_LOOPBACK_HOST, port), timeout=_PING_TIMEOUT_SECONDS
|
|
260
|
+
):
|
|
261
|
+
return True
|
|
262
|
+
except OSError:
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _ping_arrayview_server(port: int) -> dict | None:
|
|
267
|
+
url = f"http://{_LOOPBACK_HOST}:{port}/ping"
|
|
268
|
+
try:
|
|
269
|
+
with urllib.request.urlopen(url, timeout=_PING_TIMEOUT_SECONDS) as resp:
|
|
270
|
+
if resp.status != 200:
|
|
271
|
+
return None
|
|
272
|
+
payload = json.loads(resp.read().decode("utf-8"))
|
|
273
|
+
except Exception:
|
|
274
|
+
return None
|
|
275
|
+
if payload.get("ok") is True and payload.get("service") == "arrayview":
|
|
276
|
+
return payload
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _int_or_none(value: object) -> int | None:
|
|
281
|
+
return value if isinstance(value, int) else None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _str_or_none(value: object) -> str | None:
|
|
285
|
+
return value if isinstance(value, str) else None
|
arrayview/_launcher.py
CHANGED
|
@@ -33,6 +33,7 @@ from arrayview._platform import (
|
|
|
33
33
|
_is_vscode_remote,
|
|
34
34
|
_in_vscode_tunnel,
|
|
35
35
|
_can_native_window,
|
|
36
|
+
_native_window_gui,
|
|
36
37
|
_find_vscode_ipc_hook,
|
|
37
38
|
_is_julia_env,
|
|
38
39
|
_in_julia_jupyter,
|
|
@@ -249,6 +250,7 @@ def _open_webview(
|
|
|
249
250
|
|
|
250
251
|
icon_path = _get_icon_png_path() or ""
|
|
251
252
|
inline_html_b64 = None
|
|
253
|
+
gui_name = _native_window_gui() or ""
|
|
252
254
|
|
|
253
255
|
if shell_port is not None:
|
|
254
256
|
shell_html = _build_inline_shell_html(url, shell_port)
|
|
@@ -258,14 +260,14 @@ def _open_webview(
|
|
|
258
260
|
if inline_html_b64:
|
|
259
261
|
script_lines = [
|
|
260
262
|
"import sys, base64, webview",
|
|
261
|
-
"u, w, h, icon, html_b64, ready_file = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5], sys.argv[6]",
|
|
263
|
+
"u, w, h, icon, html_b64, ready_file, gui = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5], sys.argv[6], sys.argv[7]",
|
|
262
264
|
"html = base64.b64decode(html_b64.encode()).decode()",
|
|
263
265
|
"class Api:",
|
|
264
266
|
" def set_title(self, title):",
|
|
265
267
|
" try: webview.windows[0].set_title(str(title)[:240])",
|
|
266
268
|
" except Exception: pass",
|
|
267
269
|
"win = webview.create_window('ArrayView', html=html, width=w, height=h, background_color='#0c0c0c', js_api=Api())",
|
|
268
|
-
"kw = {'gui':
|
|
270
|
+
"kw = {'gui': gui} if gui else {}",
|
|
269
271
|
"def _start_func():",
|
|
270
272
|
" if ready_file:",
|
|
271
273
|
" try:",
|
|
@@ -298,6 +300,7 @@ def _open_webview(
|
|
|
298
300
|
icon_path,
|
|
299
301
|
inline_html_b64,
|
|
300
302
|
ready_file or "",
|
|
303
|
+
gui_name,
|
|
301
304
|
],
|
|
302
305
|
stdout=subprocess.DEVNULL,
|
|
303
306
|
stderr=subprocess.PIPE if capture_stderr else subprocess.DEVNULL,
|
|
@@ -306,13 +309,13 @@ def _open_webview(
|
|
|
306
309
|
# URL mode — direct load (used when shell_port not provided)
|
|
307
310
|
script_lines = [
|
|
308
311
|
"import sys, webview",
|
|
309
|
-
"u, w, h, icon, ready_file = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5]",
|
|
312
|
+
"u, w, h, icon, ready_file, gui = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5], sys.argv[6]",
|
|
310
313
|
"class Api:",
|
|
311
314
|
" def set_title(self, title):",
|
|
312
315
|
" try: webview.windows[0].set_title(str(title)[:240])",
|
|
313
316
|
" except Exception: pass",
|
|
314
317
|
"win = webview.create_window('ArrayView', u, width=w, height=h, background_color='#0c0c0c', js_api=Api())",
|
|
315
|
-
"kw = {'gui':
|
|
318
|
+
"kw = {'gui': gui} if gui else {}",
|
|
316
319
|
"def _start_func():",
|
|
317
320
|
" if ready_file:",
|
|
318
321
|
" try:",
|
|
@@ -344,6 +347,7 @@ def _open_webview(
|
|
|
344
347
|
str(win_h),
|
|
345
348
|
icon_path,
|
|
346
349
|
ready_file or "",
|
|
350
|
+
gui_name,
|
|
347
351
|
],
|
|
348
352
|
stdout=subprocess.DEVNULL,
|
|
349
353
|
stderr=subprocess.PIPE if capture_stderr else subprocess.DEVNULL,
|
|
@@ -368,6 +372,7 @@ def _open_webview_with_fallback(
|
|
|
368
372
|
sockets_before = (
|
|
369
373
|
_session_mod.VIEWER_SOCKETS
|
|
370
374
|
) # capture count so we detect a NEW connection
|
|
375
|
+
shell_sockets_before = len(_session_mod.SHELL_SOCKETS)
|
|
371
376
|
|
|
372
377
|
def _read_stderr():
|
|
373
378
|
try:
|
|
@@ -390,14 +395,18 @@ def _open_webview_with_fallback(
|
|
|
390
395
|
_open_browser(url, floating=floating)
|
|
391
396
|
return
|
|
392
397
|
|
|
393
|
-
# Phase 2: process is alive — wait up to 8 s for a NEW viewer
|
|
394
|
-
#
|
|
395
|
-
#
|
|
398
|
+
# Phase 2: process is alive — wait up to 8 s for a NEW viewer or shell
|
|
399
|
+
# WebSocket. The native shell connection proves pywebview is usable;
|
|
400
|
+
# the embedded viewer iframe can legitimately connect later.
|
|
396
401
|
import arrayview._session as _sm
|
|
397
402
|
|
|
398
403
|
for _ in range(80):
|
|
399
404
|
time.sleep(0.1)
|
|
400
|
-
|
|
405
|
+
shell_connected = (
|
|
406
|
+
shell_port is not None
|
|
407
|
+
and len(_sm.SHELL_SOCKETS) > shell_sockets_before
|
|
408
|
+
)
|
|
409
|
+
if _sm.VIEWER_SOCKETS > sockets_before or shell_connected:
|
|
401
410
|
_vprint("[ArrayView] Native window connected successfully", flush=True)
|
|
402
411
|
if sys.platform == "darwin":
|
|
403
412
|
subprocess.Popen(
|
|
@@ -530,6 +539,21 @@ def _server_viewer_connections_seen(port: int, timeout: float = 0.5) -> int:
|
|
|
530
539
|
return 0
|
|
531
540
|
|
|
532
541
|
|
|
542
|
+
def _server_shell_sockets_open(port: int, timeout: float = 0.5) -> int:
|
|
543
|
+
"""Return the daemon's current native shell WebSocket count."""
|
|
544
|
+
url = f"http://{_LOOPBACK_HOST}:{port}/ping"
|
|
545
|
+
try:
|
|
546
|
+
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
|
547
|
+
if resp.status != 200:
|
|
548
|
+
return 0
|
|
549
|
+
payload = json.loads(resp.read().decode("utf-8"))
|
|
550
|
+
if payload.get("ok") is True and payload.get("service") == "arrayview":
|
|
551
|
+
return int(payload.get("shell_sockets") or 0)
|
|
552
|
+
except Exception:
|
|
553
|
+
pass
|
|
554
|
+
return 0
|
|
555
|
+
|
|
556
|
+
|
|
533
557
|
def _wait_for_viewer_connection(
|
|
534
558
|
port: int,
|
|
535
559
|
*,
|
|
@@ -545,6 +569,23 @@ def _wait_for_viewer_connection(
|
|
|
545
569
|
return False
|
|
546
570
|
|
|
547
571
|
|
|
572
|
+
def _wait_for_native_shell_or_viewer_connection(
|
|
573
|
+
port: int,
|
|
574
|
+
*,
|
|
575
|
+
viewer_before: int,
|
|
576
|
+
timeout: float = 8.0,
|
|
577
|
+
) -> bool:
|
|
578
|
+
"""Wait until pywebview proves it is alive via shell or viewer WebSocket."""
|
|
579
|
+
deadline = time.monotonic() + timeout
|
|
580
|
+
while time.monotonic() < deadline:
|
|
581
|
+
if _server_viewer_connections_seen(port, timeout=0.3) > viewer_before:
|
|
582
|
+
return True
|
|
583
|
+
if _server_shell_sockets_open(port, timeout=0.3) > 0:
|
|
584
|
+
return True
|
|
585
|
+
time.sleep(0.1)
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
|
|
548
589
|
def _terminate_native_process(proc: subprocess.Popen | None) -> None:
|
|
549
590
|
if proc is None or proc.poll() is not None:
|
|
550
591
|
return
|
|
@@ -554,6 +595,64 @@ def _terminate_native_process(proc: subprocess.Popen | None) -> None:
|
|
|
554
595
|
return
|
|
555
596
|
|
|
556
597
|
|
|
598
|
+
def _open_cli_native_shell_after_server(
|
|
599
|
+
*,
|
|
600
|
+
port: int,
|
|
601
|
+
sid: str,
|
|
602
|
+
name: str,
|
|
603
|
+
compare_sids: "_CompareSids | None",
|
|
604
|
+
win_w: int,
|
|
605
|
+
win_h: int,
|
|
606
|
+
) -> bool:
|
|
607
|
+
"""Open a CLI native shell and return whether it is usable."""
|
|
608
|
+
url_shell = _shell_url(port, sid, name, compare_sids=compare_sids)
|
|
609
|
+
viewer_before = _server_viewer_connections_seen(port)
|
|
610
|
+
opened, proc = _open_webview_cli_tracked(url_shell, win_w, win_h)
|
|
611
|
+
if opened and _wait_for_native_shell_or_viewer_connection(
|
|
612
|
+
port, viewer_before=viewer_before
|
|
613
|
+
):
|
|
614
|
+
return True
|
|
615
|
+
if opened:
|
|
616
|
+
_vprint(
|
|
617
|
+
"[ArrayView] Native window did not connect to the backend; falling back to browser",
|
|
618
|
+
flush=True,
|
|
619
|
+
)
|
|
620
|
+
_terminate_native_process(proc)
|
|
621
|
+
return False
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _activate_early_cli_native_shell(
|
|
625
|
+
*,
|
|
626
|
+
port: int,
|
|
627
|
+
sid: str,
|
|
628
|
+
name: str,
|
|
629
|
+
proc: subprocess.Popen | None,
|
|
630
|
+
) -> bool:
|
|
631
|
+
"""Attach an already-started preload shell to a spawned daemon session."""
|
|
632
|
+
viewer_before = _server_viewer_connections_seen(port)
|
|
633
|
+
try:
|
|
634
|
+
notify_result = _notify_existing_session(
|
|
635
|
+
port,
|
|
636
|
+
sid,
|
|
637
|
+
name,
|
|
638
|
+
url=_viewer_path(sid),
|
|
639
|
+
wait=True,
|
|
640
|
+
)
|
|
641
|
+
notified = bool(notify_result.get("notified"))
|
|
642
|
+
except Exception:
|
|
643
|
+
notified = False
|
|
644
|
+
if notified and _wait_for_native_shell_or_viewer_connection(
|
|
645
|
+
port, viewer_before=viewer_before
|
|
646
|
+
):
|
|
647
|
+
return True
|
|
648
|
+
_vprint(
|
|
649
|
+
"[ArrayView] Native window did not connect to the backend; falling back to browser",
|
|
650
|
+
flush=True,
|
|
651
|
+
)
|
|
652
|
+
_terminate_native_process(proc)
|
|
653
|
+
return False
|
|
654
|
+
|
|
655
|
+
|
|
557
656
|
# ── Server Port Utilities ─────────────────────────────────────────
|
|
558
657
|
|
|
559
658
|
|
|
@@ -862,23 +961,21 @@ def _open_cli_existing_server_view(
|
|
|
862
961
|
_vprint(f"Injected into existing window (port {port})")
|
|
863
962
|
return
|
|
864
963
|
if notify_webview and not notified:
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
_terminate_native_process(proc)
|
|
964
|
+
native_ready = _open_cli_native_shell_after_server(
|
|
965
|
+
port=port,
|
|
966
|
+
sid=sid,
|
|
967
|
+
name=name,
|
|
968
|
+
compare_sids=compare_sids,
|
|
969
|
+
win_w=1200,
|
|
970
|
+
win_h=800,
|
|
971
|
+
)
|
|
972
|
+
if not native_ready:
|
|
875
973
|
_vprint("[ArrayView] Falling back to browser", flush=True)
|
|
876
974
|
_print_viewer_location(url)
|
|
877
975
|
_open_browser(
|
|
878
976
|
url,
|
|
879
977
|
blocking=True,
|
|
880
978
|
title=f"ArrayView: {name}",
|
|
881
|
-
filepath=base_file,
|
|
882
979
|
floating=floating,
|
|
883
980
|
)
|
|
884
981
|
return
|
|
@@ -889,7 +986,6 @@ def _open_cli_existing_server_view(
|
|
|
889
986
|
blocking=True,
|
|
890
987
|
force_vscode=(window_mode == "vscode"),
|
|
891
988
|
title=f"ArrayView: {name}",
|
|
892
|
-
filepath=base_file,
|
|
893
989
|
floating=floating,
|
|
894
990
|
)
|
|
895
991
|
|
|
@@ -1052,11 +1148,11 @@ def _handle_cli_spawned_daemon(
|
|
|
1052
1148
|
stdout=subprocess.DEVNULL,
|
|
1053
1149
|
stderr=subprocess.DEVNULL,
|
|
1054
1150
|
close_fds=True,
|
|
1151
|
+
start_new_session=(sys.platform != "win32"),
|
|
1055
1152
|
)
|
|
1056
1153
|
|
|
1057
1154
|
early_webview_opened = False
|
|
1058
1155
|
early_webview_proc = None
|
|
1059
|
-
early_webview_notified = False
|
|
1060
1156
|
early_webview_connected = False
|
|
1061
1157
|
if (
|
|
1062
1158
|
use_webview
|
|
@@ -1083,29 +1179,12 @@ def _handle_cli_spawned_daemon(
|
|
|
1083
1179
|
sys.exit(1)
|
|
1084
1180
|
|
|
1085
1181
|
if early_webview_opened:
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
url=_viewer_path(sid),
|
|
1093
|
-
wait=True,
|
|
1094
|
-
)
|
|
1095
|
-
early_webview_notified = bool(notify_result.get("notified"))
|
|
1096
|
-
except Exception:
|
|
1097
|
-
early_webview_notified = False
|
|
1098
|
-
if early_webview_notified:
|
|
1099
|
-
early_webview_connected = _wait_for_viewer_connection(
|
|
1100
|
-
port,
|
|
1101
|
-
before=seen_before,
|
|
1102
|
-
)
|
|
1103
|
-
if not early_webview_connected:
|
|
1104
|
-
_vprint(
|
|
1105
|
-
"[ArrayView] Native window did not connect to the backend; falling back to browser",
|
|
1106
|
-
flush=True,
|
|
1107
|
-
)
|
|
1108
|
-
_terminate_native_process(early_webview_proc)
|
|
1182
|
+
early_webview_connected = _activate_early_cli_native_shell(
|
|
1183
|
+
port=port,
|
|
1184
|
+
sid=sid,
|
|
1185
|
+
name=name,
|
|
1186
|
+
proc=early_webview_proc,
|
|
1187
|
+
)
|
|
1109
1188
|
|
|
1110
1189
|
should_retry_webview = use_webview and not (
|
|
1111
1190
|
early_webview_opened and not early_webview_connected
|
|
@@ -1154,21 +1233,20 @@ def _open_cli_spawned_view(
|
|
|
1154
1233
|
if _should_notify_webview(use_webview, overlay_sid):
|
|
1155
1234
|
if webview_already_opened:
|
|
1156
1235
|
return
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
_terminate_native_process(proc)
|
|
1236
|
+
native_ready = _open_cli_native_shell_after_server(
|
|
1237
|
+
port=port,
|
|
1238
|
+
sid=sid,
|
|
1239
|
+
name=name,
|
|
1240
|
+
compare_sids=compare_sids,
|
|
1241
|
+
win_w=1400,
|
|
1242
|
+
win_h=900,
|
|
1243
|
+
)
|
|
1244
|
+
if not native_ready:
|
|
1167
1245
|
_vprint("[ArrayView] Falling back to browser", flush=True)
|
|
1168
1246
|
_print_viewer_location(url)
|
|
1169
1247
|
_open_browser(
|
|
1170
1248
|
url,
|
|
1171
|
-
blocking=
|
|
1249
|
+
blocking=(window_mode == "vscode"),
|
|
1172
1250
|
force_vscode=(window_mode == "vscode"),
|
|
1173
1251
|
title=f"ArrayView: {name}",
|
|
1174
1252
|
floating=floating,
|