arrayview 0.43.2__py3-none-any.whl → 0.43.3__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/_routes_query.py +25 -10
- arrayview/_viewer.html +111 -8
- arrayview/_vscode_extension.py +241 -9
- arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.43.2.dist-info → arrayview-0.43.3.dist-info}/METADATA +1 -1
- {arrayview-0.43.2.dist-info → arrayview-0.43.3.dist-info}/RECORD +9 -9
- {arrayview-0.43.2.dist-info → arrayview-0.43.3.dist-info}/WHEEL +0 -0
- {arrayview-0.43.2.dist-info → arrayview-0.43.3.dist-info}/entry_points.txt +0 -0
- {arrayview-0.43.2.dist-info → arrayview-0.43.3.dist-info}/licenses/LICENSE +0 -0
arrayview/_routes_query.py
CHANGED
|
@@ -51,6 +51,7 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
|
|
|
51
51
|
if not token or len(token) > 256 or not window_id:
|
|
52
52
|
raise HTTPException(status_code=400, detail="Incomplete viewer phase identity")
|
|
53
53
|
allowed = {
|
|
54
|
+
"navigation-arrived",
|
|
54
55
|
"script-loaded",
|
|
55
56
|
"ws-open",
|
|
56
57
|
"metadata-loaded",
|
|
@@ -58,13 +59,16 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
|
|
|
58
59
|
}
|
|
59
60
|
if phase != "launch-prepared" and phase not in allowed:
|
|
60
61
|
raise HTTPException(status_code=400, detail="Invalid viewer phase")
|
|
61
|
-
if
|
|
62
|
+
if (
|
|
63
|
+
phase not in {"launch-prepared", "navigation-arrived"}
|
|
64
|
+
and not viewer_instance_id
|
|
65
|
+
):
|
|
62
66
|
raise HTTPException(status_code=400, detail="Incomplete viewer phase identity")
|
|
63
67
|
# Preparation and script startup are control-plane events. The viewer
|
|
64
68
|
# is meant to open while a large array is still loading, so hiding
|
|
65
69
|
# either event behind Session readiness creates a false pre-script
|
|
66
70
|
# timeout. Data-dependent phases still wait for the real Session.
|
|
67
|
-
if phase in {"launch-prepared", "script-loaded"}:
|
|
71
|
+
if phase in {"launch-prepared", "navigation-arrived", "script-loaded"}:
|
|
68
72
|
if sid not in SESSIONS and sid not in _session_mod.PENDING_SESSIONS:
|
|
69
73
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
70
74
|
else:
|
|
@@ -207,13 +211,24 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
|
|
|
207
211
|
raise HTTPException(status_code=409, detail="Viewer phase owner changed")
|
|
208
212
|
if body.get("sid") != sid:
|
|
209
213
|
raise HTTPException(status_code=409, detail="Viewer primary session changed")
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
if phase == "navigation-arrived":
|
|
215
|
+
navigation_attempt = body.get("navigation_attempt")
|
|
216
|
+
if (
|
|
217
|
+
type(navigation_attempt) is not int
|
|
218
|
+
or navigation_attempt != journal.get("navigation_attempt")
|
|
219
|
+
):
|
|
220
|
+
raise HTTPException(
|
|
221
|
+
status_code=409,
|
|
222
|
+
detail="Viewer navigation attempt changed",
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
related_sids = _viewer_related_sids(body, sid)
|
|
226
|
+
if journal["related_sids"] is None:
|
|
227
|
+
journal["related_sids"] = related_sids
|
|
228
|
+
elif journal["related_sids"] != related_sids:
|
|
229
|
+
raise HTTPException(status_code=409, detail="Viewer related sessions changed")
|
|
230
|
+
if viewer_instance_id not in journal["viewer_instance_ids"]:
|
|
231
|
+
journal["viewer_instance_ids"].append(viewer_instance_id)
|
|
217
232
|
phases = journal["phases"]
|
|
218
233
|
if phase not in phases:
|
|
219
234
|
phases.append(phase)
|
|
@@ -245,7 +260,7 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
|
|
|
245
260
|
"token": token,
|
|
246
261
|
"phases": list(phases),
|
|
247
262
|
"viewer_instance_ids": list(journal["viewer_instance_ids"]),
|
|
248
|
-
"related_sids": list(journal["related_sids"]),
|
|
263
|
+
"related_sids": list(journal["related_sids"] or []),
|
|
249
264
|
}
|
|
250
265
|
|
|
251
266
|
@app.get("/viewer-phase/{sid}/{request_id}")
|
arrayview/_viewer.html
CHANGED
|
@@ -3,6 +3,55 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<title>ArrayView</title>
|
|
5
5
|
<script>
|
|
6
|
+
// Tell the opener as soon as this document starts parsing. The full
|
|
7
|
+
// viewer script is large and sits near the end of this file, so using
|
|
8
|
+
// its startup as proof that navigation arrived can make a slow but
|
|
9
|
+
// valid page look dropped and cause its tab to be closed prematurely.
|
|
10
|
+
(function () {
|
|
11
|
+
try {
|
|
12
|
+
var launchSearch = __ARRAYVIEW_QUERY__ || location.search;
|
|
13
|
+
var params = new URLSearchParams(launchSearch);
|
|
14
|
+
if (params.get('_av_integrated_browser') !== '1') return;
|
|
15
|
+
var sid = params.get('sid');
|
|
16
|
+
var requestId = params.get('_av_launch_request_id');
|
|
17
|
+
var serverId = params.get('_av_launch_server_id');
|
|
18
|
+
var windowId = params.get('_av_launch_window_id');
|
|
19
|
+
var token = params.get('_av_launch_token');
|
|
20
|
+
var attemptValue = params.get('_av_navigation_attempt');
|
|
21
|
+
var attempt = Number(attemptValue);
|
|
22
|
+
if (
|
|
23
|
+
!sid || !requestId || !serverId || !windowId || !token
|
|
24
|
+
|| attemptValue === null
|
|
25
|
+
|| !Number.isInteger(attempt) || attempt < 0
|
|
26
|
+
) return;
|
|
27
|
+
var phaseUrl = `/viewer-phase/${encodeURIComponent(sid)}/${encodeURIComponent(requestId)}`;
|
|
28
|
+
var phaseBody = JSON.stringify({
|
|
29
|
+
phase: 'navigation-arrived',
|
|
30
|
+
sid,
|
|
31
|
+
server_id: serverId,
|
|
32
|
+
window_id: windowId,
|
|
33
|
+
token,
|
|
34
|
+
navigation_attempt: attempt,
|
|
35
|
+
});
|
|
36
|
+
var reportArrival = function (retriesLeft) {
|
|
37
|
+
fetch(phaseUrl, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: phaseBody,
|
|
41
|
+
}).then(function (response) {
|
|
42
|
+
if (!response.ok) throw new Error('arrival rejected');
|
|
43
|
+
}).catch(function () {
|
|
44
|
+
if (retriesLeft > 0) {
|
|
45
|
+
setTimeout(function () {
|
|
46
|
+
reportArrival(retriesLeft - 1);
|
|
47
|
+
}, 100);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
reportArrival(3);
|
|
52
|
+
} catch (_) {}
|
|
53
|
+
})();
|
|
54
|
+
|
|
6
55
|
// Name the tab at parse time. Without this the tab shows the host
|
|
7
56
|
// ("localhost:8000") until metadata arrives over the WebSocket, and
|
|
8
57
|
// that wait is the array load — seconds for a large file. The name
|
|
@@ -2332,7 +2381,7 @@
|
|
|
2332
2381
|
#info-box { padding: 18px 22px; font-size: 12px; line-height: 1.8; font-family: 'SF Mono', ui-monospace, monospace; }
|
|
2333
2382
|
#info-box table { border-collapse: collapse; width: 100%; }
|
|
2334
2383
|
#info-box td:first-child { color: var(--muted); padding-right: 24px; }
|
|
2335
|
-
#help-hint, #tool-hint, #qmri-hint {
|
|
2384
|
+
#help-hint, #tool-hint, #qmri-hint, #ras-hint {
|
|
2336
2385
|
position: fixed; bottom: 14px; cursor: pointer;
|
|
2337
2386
|
width: 22px; height: 22px; border-radius: 50%;
|
|
2338
2387
|
border: 1px solid var(--border); color: var(--muted);
|
|
@@ -2348,7 +2397,25 @@
|
|
|
2348
2397
|
#qmri-hint.visible { display: flex; }
|
|
2349
2398
|
#qmri-hint .q-dim-icon { width: 17px; height: 17px; }
|
|
2350
2399
|
#tool-hint.active { color: var(--active-dim); border-color: var(--active-dim); }
|
|
2351
|
-
#help-hint:hover, #tool-hint:hover, #qmri-hint:hover { color: var(--text); border-color: var(--muted); }
|
|
2400
|
+
#help-hint:hover, #tool-hint:hover, #qmri-hint:hover, #ras-hint:hover { color: var(--text); border-color: var(--muted); }
|
|
2401
|
+
#ras-hint {
|
|
2402
|
+
right: 106px; display: none; position: fixed;
|
|
2403
|
+
font-family: 'SF Mono', ui-monospace, 'Cascadia Code', 'JetBrains Mono', monospace;
|
|
2404
|
+
font-size: 8px; font-weight: 600; letter-spacing: -0.3px;
|
|
2405
|
+
}
|
|
2406
|
+
#ras-hint.visible { display: flex; }
|
|
2407
|
+
#ras-hint.active { color: var(--active-dim); border-color: var(--active-dim); }
|
|
2408
|
+
#ras-hint.loading { border-color: transparent; }
|
|
2409
|
+
#ras-hint.loading::before {
|
|
2410
|
+
content: ''; position: absolute; inset: -1px; border-radius: 50%;
|
|
2411
|
+
padding: 1px;
|
|
2412
|
+
background: conic-gradient(var(--active-dim) 0deg, var(--active-dim) 100deg, transparent 100deg, transparent 360deg);
|
|
2413
|
+
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
2414
|
+
-webkit-mask-composite: xor;
|
|
2415
|
+
mask-composite: exclude;
|
|
2416
|
+
animation: ras-hint-spin 0.85s linear infinite;
|
|
2417
|
+
}
|
|
2418
|
+
@keyframes ras-hint-spin { to { transform: rotate(360deg); } }
|
|
2352
2419
|
#dynamic-island {
|
|
2353
2420
|
position: fixed; left: 8px; top: 8px;
|
|
2354
2421
|
background: rgba(24,24,24,0.78); border: 1px solid rgba(255,255,255,0.07);
|
|
@@ -4502,7 +4569,7 @@
|
|
|
4502
4569
|
/* ── Zen / focus mode ────────────────────────────────────── */
|
|
4503
4570
|
body.zen-mode #array-name, body.zen-mode #info,
|
|
4504
4571
|
body.zen-mode #slim-cb-wrap, body.zen-mode #mode-eggs,
|
|
4505
|
-
body.zen-mode #help-hint, body.zen-mode #tool-hint, body.zen-mode #status,
|
|
4572
|
+
body.zen-mode #help-hint, body.zen-mode #tool-hint, body.zen-mode #ras-hint, body.zen-mode #status,
|
|
4506
4573
|
body.zen-mode #toast, body.zen-mode #dim-active-underline,
|
|
4507
4574
|
body.zen-mode #overlay-palette { opacity:0 !important; transition:opacity 0.6s ease; pointer-events:none !important; }
|
|
4508
4575
|
body.zen-mode .qv-cb-island,
|
|
@@ -5503,6 +5570,7 @@
|
|
|
5503
5570
|
<div id="tool-hint" title="Tool launcher (/)">/</div>
|
|
5504
5571
|
<div id="preferences-hint" title="Preferences" aria-label="Preferences"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.83 2.83-.06-.06a1.7 1.7 0 0 0-1.88-.34 1.7 1.7 0 0 0-1.03 1.56V21h-4v-.08A1.7 1.7 0 0 0 9 19.37a1.7 1.7 0 0 0-1.88.34l-.06.06-2.83-2.83.06-.06A1.7 1.7 0 0 0 4.63 15 1.7 1.7 0 0 0 3.08 14H3v-4h.08A1.7 1.7 0 0 0 4.63 9a1.7 1.7 0 0 0-.34-1.88l-.06-.06 2.83-2.83.06.06A1.7 1.7 0 0 0 9 4.63 1.7 1.7 0 0 0 10 3.08V3h4v.08A1.7 1.7 0 0 0 15 4.63a1.7 1.7 0 0 0 1.88-.34l.06-.06 2.83 2.83-.06.06A1.7 1.7 0 0 0 19.37 9 1.7 1.7 0 0 0 20.92 10H21v4h-.08A1.7 1.7 0 0 0 19.4 15z"></path></svg></div>
|
|
5505
5572
|
<div id="qmri-hint" title="qMRI options"></div>
|
|
5573
|
+
<div id="ras-hint" title="Convert to RAS orientation — aligns axes so every view scrolls fast, not just the native one (Shift+A)">RAS</div>
|
|
5506
5574
|
<canvas id="mv-orientation"></canvas>
|
|
5507
5575
|
<div id="dynamic-island"></div>
|
|
5508
5576
|
<div id="tool-launcher" aria-hidden="true"></div>
|
|
@@ -6458,7 +6526,12 @@
|
|
|
6458
6526
|
let isRendering = false;
|
|
6459
6527
|
let pendingRequest = false;
|
|
6460
6528
|
let wsSlowTimer = null; // show the busy panel for slow renders
|
|
6461
|
-
|
|
6529
|
+
// Deliberately much higher than PATIENT_LOADING_THRESHOLD_MS: this one
|
|
6530
|
+
// fires for every render, including plain in-array scrolling, so on a
|
|
6531
|
+
// slow network drive a short threshold flickers on/off as each frame
|
|
6532
|
+
// just barely crosses it. This should only catch renders that are
|
|
6533
|
+
// actually stalled, not ordinary slow-drive catch-up.
|
|
6534
|
+
const WS_SLOW_THRESHOLD_MS = 500;
|
|
6462
6535
|
const PATIENT_LOADING_THRESHOLD_MS = 80;
|
|
6463
6536
|
let _displayedPatientIndex = null;
|
|
6464
6537
|
let _patientLoadingIndex = null;
|
|
@@ -6731,6 +6804,7 @@
|
|
|
6731
6804
|
let excludeZeros = false; // exclude zero-valued voxels from histogram computation
|
|
6732
6805
|
let spatialMeta = null; // {voxel_sizes, axis_labels, is_oblique, is_canonical} or null
|
|
6733
6806
|
let rasResampleActive = false;
|
|
6807
|
+
let _rasResampleLoading = false;
|
|
6734
6808
|
let multiViewActive = false;
|
|
6735
6809
|
let _mvOrientationHidden = false; // ~ toggle — hides orientation cube
|
|
6736
6810
|
let mipActive = false;
|
|
@@ -24421,6 +24495,7 @@
|
|
|
24421
24495
|
title: 'Toggle explicit RAS conversion (NIfTI)',
|
|
24422
24496
|
when: [],
|
|
24423
24497
|
run: (ctx, e) => {
|
|
24498
|
+
if (_rasResampleLoading) return;
|
|
24424
24499
|
if (compareActive || qmriActive) {
|
|
24425
24500
|
showToast('RAS resample not supported in compare/qMRI mode');
|
|
24426
24501
|
return;
|
|
@@ -24436,13 +24511,15 @@
|
|
|
24436
24511
|
}
|
|
24437
24512
|
const wantOn = !rasResampleActive;
|
|
24438
24513
|
showToast(wantOn ? 'converting to RAS...' : 'reverting to native orientation...');
|
|
24514
|
+
_rasResampleLoading = true;
|
|
24515
|
+
document.getElementById('ras-hint')?.classList.add('loading');
|
|
24439
24516
|
proxyFetch(`/resample_ras/${sid}`, {
|
|
24440
24517
|
method: 'POST',
|
|
24441
24518
|
headers: {'Content-Type': 'application/json'},
|
|
24442
24519
|
body: JSON.stringify({ enabled: wantOn }),
|
|
24443
24520
|
}).then(r => r.json()).then(d => {
|
|
24444
|
-
if (d.error) { showToast('RAS resample error: ' + d.error); return; }
|
|
24445
|
-
if (d.skipped) { showToast('RAS resample skipped: ' + d.reason); return; }
|
|
24521
|
+
if (d.error) { _clearRasHintLoading(); showToast('RAS resample error: ' + d.error); return; }
|
|
24522
|
+
if (d.skipped) { _clearRasHintLoading(); showToast('RAS resample skipped: ' + d.reason); return; }
|
|
24446
24523
|
rasResampleActive = wantOn;
|
|
24447
24524
|
// Refresh shape from /info, then re-center indices and refresh
|
|
24448
24525
|
// canvases. Because shape changes (e.g. (240,240,155) ->
|
|
@@ -24475,9 +24552,12 @@
|
|
|
24475
24552
|
updateView(); triggerPreload();
|
|
24476
24553
|
}
|
|
24477
24554
|
renderInfo();
|
|
24555
|
+
_reconcileUI();
|
|
24556
|
+
_clearRasHintLoading();
|
|
24478
24557
|
});
|
|
24479
24558
|
showToast('RAS resample: ' + (wantOn ? 'on' : 'off') + (d.elapsed_ms ? ` (${d.elapsed_ms}ms)` : ''));
|
|
24480
24559
|
}).catch(err => {
|
|
24560
|
+
_clearRasHintLoading();
|
|
24481
24561
|
showToast('RAS resample failed: ' + err);
|
|
24482
24562
|
});
|
|
24483
24563
|
},
|
|
@@ -27298,6 +27378,11 @@
|
|
|
27298
27378
|
if (cmd && typeof cmd.run === 'function') cmd.run(makeContext(null), null);
|
|
27299
27379
|
}
|
|
27300
27380
|
|
|
27381
|
+
function _clearRasHintLoading() {
|
|
27382
|
+
_rasResampleLoading = false;
|
|
27383
|
+
document.getElementById('ras-hint')?.classList.remove('loading');
|
|
27384
|
+
}
|
|
27385
|
+
|
|
27301
27386
|
function _saveFullArrayNpy() {
|
|
27302
27387
|
if (_inPyWebView) {
|
|
27303
27388
|
proxyFetch(`/export_array/${sid}?save_to_downloads=1`)
|
|
@@ -35596,6 +35681,9 @@
|
|
|
35596
35681
|
document.getElementById('tool-hint').addEventListener('click', () => {
|
|
35597
35682
|
openToolMenu();
|
|
35598
35683
|
});
|
|
35684
|
+
document.getElementById('ras-hint').addEventListener('click', () => {
|
|
35685
|
+
_runCommand('nifti.toggleRasResample');
|
|
35686
|
+
});
|
|
35599
35687
|
document.getElementById('tool-launcher').addEventListener('click', (e) => {
|
|
35600
35688
|
if (e.target === e.currentTarget) closeToolLauncher();
|
|
35601
35689
|
});
|
|
@@ -37357,6 +37445,15 @@
|
|
|
37357
37445
|
if (!qmriHintActive) _qmriOptionsPinned = false;
|
|
37358
37446
|
_reconcileQmriAltOptions();
|
|
37359
37447
|
}
|
|
37448
|
+
|
|
37449
|
+
// 8. RAS conversion hint — visible whenever the loaded array carries
|
|
37450
|
+
// NIfTI spatial metadata (single .nii/.nii.gz sessions).
|
|
37451
|
+
const rasHint = document.getElementById('ras-hint');
|
|
37452
|
+
if (rasHint) {
|
|
37453
|
+
rasHint.classList.toggle('visible', !!spatialMeta);
|
|
37454
|
+
rasHint.classList.toggle('active', rasResampleActive);
|
|
37455
|
+
rasHint.setAttribute('aria-hidden', spatialMeta ? 'false' : 'true');
|
|
37456
|
+
}
|
|
37360
37457
|
_updateMvPaneFill();
|
|
37361
37458
|
}
|
|
37362
37459
|
|
|
@@ -38503,7 +38600,7 @@
|
|
|
38503
38600
|
},
|
|
38504
38601
|
chromeToFade: () => {
|
|
38505
38602
|
const sel = ['#mv-cb-wrap', '#array-name', '#mv-orientation',
|
|
38506
|
-
'#help-hint', '#tool-hint',
|
|
38603
|
+
'#help-hint', '#tool-hint', '#ras-hint',
|
|
38507
38604
|
'.cb-island', '.cv-pixel-info', '.axes-indicator', '.mv-label'];
|
|
38508
38605
|
const set = new Set();
|
|
38509
38606
|
sel.forEach(s => document.querySelectorAll(s).forEach(el => set.add(el)));
|
|
@@ -39306,7 +39403,13 @@
|
|
|
39306
39403
|
return true;
|
|
39307
39404
|
}
|
|
39308
39405
|
|
|
39309
|
-
function _dmenuCyclePresetFromKey() {
|
|
39406
|
+
async function _dmenuCyclePresetFromKey() {
|
|
39407
|
+
// In qMRI mode each pane owns its own window, so once the picker
|
|
39408
|
+
// is already open, repeat `d` must keep following the mouse
|
|
39409
|
+
// instead of falling back to the single shared primary path.
|
|
39410
|
+
if ((qmriActive || compareQmriActive) && _hoveredQmriView) {
|
|
39411
|
+
return await _cycleQmriPaneRangePreset();
|
|
39412
|
+
}
|
|
39310
39413
|
const preset = _cycleQuantilePreset();
|
|
39311
39414
|
if (preset) {
|
|
39312
39415
|
_dmenuSelectCurrentPreset();
|
arrayview/_vscode_extension.py
CHANGED
|
@@ -10,6 +10,7 @@ import signal
|
|
|
10
10
|
import subprocess
|
|
11
11
|
import time
|
|
12
12
|
import zipfile
|
|
13
|
+
from contextlib import contextmanager
|
|
13
14
|
from importlib.resources import files as _pkg_files
|
|
14
15
|
|
|
15
16
|
from arrayview._session import _vprint
|
|
@@ -57,13 +58,85 @@ _VSCODE_EXT_FRESH_INSTALL = False # True if we just installed it this session
|
|
|
57
58
|
_VSCODE_EXT_RELOAD_REQUIRED = False # installed files are newer than the live host
|
|
58
59
|
_VSCODE_EXT_INSTALL_FAILED = False # automatic install could not complete safely
|
|
59
60
|
_VSCODE_EXT_NO_LIVE_WINDOW = False # no live host claims this terminal's window id
|
|
60
|
-
_VSCODE_EXT_VERSION = "0.15.
|
|
61
|
+
_VSCODE_EXT_VERSION = "0.15.53" # current bundled extension version
|
|
61
62
|
_VSCODE_CONFIGURED_PORTS: set[int] = set()
|
|
62
63
|
# Version-skew notices already printed, so a long-lived process (Jupyter, a
|
|
63
64
|
# script opening several arrays) states the mismatch once instead of per call.
|
|
64
65
|
_VSCODE_EXT_SKEW_REPORTED: set[tuple] = set()
|
|
65
66
|
_VSCODE_EXT_INSTALL_GUARD_PREFIX = "vscode-extension-install-failure-"
|
|
66
67
|
_VSCODE_EXT_INSTALL_NO_HOST_COOLDOWN_SECONDS = 300.0
|
|
68
|
+
_VSCODE_EXT_INSTALL_LOCK_TIMEOUT_SECONDS = 60.0
|
|
69
|
+
_VSCODE_EXT_INSTALL_LOCK_STALE_SECONDS = 90.0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _vscode_extension_install_lock_path() -> str:
|
|
73
|
+
return os.path.join(
|
|
74
|
+
os.path.expanduser("~/.arrayview"), "vscode-extension-install.lock"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@contextmanager
|
|
79
|
+
def _vscode_extension_install_lock(
|
|
80
|
+
*, timeout: float = _VSCODE_EXT_INSTALL_LOCK_TIMEOUT_SECONDS
|
|
81
|
+
):
|
|
82
|
+
"""Serialize the rare shared-profile install transaction across processes."""
|
|
83
|
+
path = _vscode_extension_install_lock_path()
|
|
84
|
+
token = f"{os.getpid()}-{time.time_ns()}"
|
|
85
|
+
deadline = time.monotonic() + timeout
|
|
86
|
+
acquired = False
|
|
87
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
88
|
+
while True:
|
|
89
|
+
try:
|
|
90
|
+
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
91
|
+
with os.fdopen(fd, "w") as handle:
|
|
92
|
+
json.dump(
|
|
93
|
+
{"pid": os.getpid(), "token": token, "createdAt": time.time()},
|
|
94
|
+
handle,
|
|
95
|
+
)
|
|
96
|
+
acquired = True
|
|
97
|
+
break
|
|
98
|
+
except FileExistsError:
|
|
99
|
+
try:
|
|
100
|
+
with open(path) as handle:
|
|
101
|
+
owner = json.load(handle)
|
|
102
|
+
owner_pid = owner.get("pid")
|
|
103
|
+
created_at = owner.get("createdAt")
|
|
104
|
+
# A live installer keeps ownership however long VS Code takes.
|
|
105
|
+
# Age alone must not permit a second process to install into the
|
|
106
|
+
# same shared registry concurrently.
|
|
107
|
+
stale = not _process_is_alive(owner_pid)
|
|
108
|
+
except (OSError, ValueError, TypeError, AttributeError):
|
|
109
|
+
# Recover a lock whose owner died between the exclusive create
|
|
110
|
+
# and writing valid JSON, but do not race an in-progress write.
|
|
111
|
+
try:
|
|
112
|
+
stale = (
|
|
113
|
+
time.time() - os.stat(path).st_mtime
|
|
114
|
+
> _VSCODE_EXT_INSTALL_LOCK_STALE_SECONDS
|
|
115
|
+
)
|
|
116
|
+
except OSError:
|
|
117
|
+
stale = False
|
|
118
|
+
if stale:
|
|
119
|
+
try:
|
|
120
|
+
os.unlink(path)
|
|
121
|
+
except OSError:
|
|
122
|
+
pass
|
|
123
|
+
continue
|
|
124
|
+
if time.monotonic() >= deadline:
|
|
125
|
+
break
|
|
126
|
+
time.sleep(0.1)
|
|
127
|
+
except OSError:
|
|
128
|
+
break
|
|
129
|
+
try:
|
|
130
|
+
yield acquired
|
|
131
|
+
finally:
|
|
132
|
+
if acquired:
|
|
133
|
+
try:
|
|
134
|
+
with open(path) as handle:
|
|
135
|
+
owner = json.load(handle)
|
|
136
|
+
if owner.get("token") == token:
|
|
137
|
+
os.unlink(path)
|
|
138
|
+
except (OSError, ValueError, TypeError, AttributeError):
|
|
139
|
+
pass
|
|
67
140
|
|
|
68
141
|
|
|
69
142
|
def _extension_install_guard_key(
|
|
@@ -256,6 +329,62 @@ def _extension_bases(*, remote: bool | None = None) -> tuple[str, ...]:
|
|
|
256
329
|
return (local, remote_base)
|
|
257
330
|
|
|
258
331
|
|
|
332
|
+
def _extension_registered(base: str, version: str) -> bool:
|
|
333
|
+
"""Return whether one extension profile registers this exact installation."""
|
|
334
|
+
registry = os.path.join(base, "extensions.json")
|
|
335
|
+
expected = os.path.realpath(
|
|
336
|
+
os.path.join(base, f"arrayview.arrayview-opener-{version}")
|
|
337
|
+
)
|
|
338
|
+
try:
|
|
339
|
+
with open(registry) as handle:
|
|
340
|
+
entries = json.load(handle)
|
|
341
|
+
except (OSError, ValueError, TypeError):
|
|
342
|
+
return False
|
|
343
|
+
if not isinstance(entries, list):
|
|
344
|
+
return False
|
|
345
|
+
for entry in entries:
|
|
346
|
+
if not isinstance(entry, dict) or entry.get("version") != version:
|
|
347
|
+
continue
|
|
348
|
+
identifier = entry.get("identifier")
|
|
349
|
+
if not isinstance(identifier, dict) or (
|
|
350
|
+
str(identifier.get("id", "")).lower()
|
|
351
|
+
!= "arrayview.arrayview-opener"
|
|
352
|
+
):
|
|
353
|
+
continue
|
|
354
|
+
locations: list[str] = []
|
|
355
|
+
relative = entry.get("relativeLocation")
|
|
356
|
+
if isinstance(relative, str):
|
|
357
|
+
locations.append(os.path.join(base, relative))
|
|
358
|
+
location = entry.get("location")
|
|
359
|
+
if isinstance(location, dict):
|
|
360
|
+
for field in ("fsPath", "path"):
|
|
361
|
+
value = location.get(field)
|
|
362
|
+
if isinstance(value, str):
|
|
363
|
+
locations.append(value)
|
|
364
|
+
if any(os.path.realpath(path) == expected for path in locations):
|
|
365
|
+
return True
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _wait_for_extension_registration(
|
|
370
|
+
base: str, version: str, *, timeout: float = 5.0
|
|
371
|
+
) -> bool:
|
|
372
|
+
"""Wait for VS Code to commit an install to the selected profile registry."""
|
|
373
|
+
deadline = time.monotonic() + timeout
|
|
374
|
+
while time.monotonic() < deadline:
|
|
375
|
+
if _extension_registered(base, version):
|
|
376
|
+
return True
|
|
377
|
+
time.sleep(0.1)
|
|
378
|
+
return _extension_registered(base, version)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _extension_directory_present(version: str, *, remote: bool) -> bool:
|
|
382
|
+
return any(
|
|
383
|
+
os.path.isdir(os.path.join(base, f"arrayview.arrayview-opener-{version}"))
|
|
384
|
+
for base in _extension_bases(remote=remote)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
259
388
|
def _remove_old_extension_versions(
|
|
260
389
|
current_version: str, *, remote: bool | None = None
|
|
261
390
|
) -> None:
|
|
@@ -310,7 +439,7 @@ def _extension_on_disk(
|
|
|
310
439
|
"""
|
|
311
440
|
for base in _extension_bases(remote=remote):
|
|
312
441
|
ext_dir = os.path.join(base, f"arrayview.arrayview-opener-{version}")
|
|
313
|
-
if not os.path.isdir(ext_dir):
|
|
442
|
+
if not os.path.isdir(ext_dir) or not _extension_registered(base, version):
|
|
314
443
|
continue
|
|
315
444
|
if vsix_path is None:
|
|
316
445
|
return True
|
|
@@ -414,7 +543,11 @@ def _newer_extension_on_disk(version: str, *, remote: bool) -> str | None:
|
|
|
414
543
|
continue
|
|
415
544
|
candidate = entry[len(prefix) :]
|
|
416
545
|
key = _version_tuple(candidate)
|
|
417
|
-
if
|
|
546
|
+
if (
|
|
547
|
+
key is not None
|
|
548
|
+
and key > wanted
|
|
549
|
+
and _extension_registered(base, candidate)
|
|
550
|
+
):
|
|
418
551
|
newer.append((key, candidate))
|
|
419
552
|
return max(newer)[1] if newer else None
|
|
420
553
|
|
|
@@ -742,6 +875,14 @@ def _write_vscode_extension_hash(
|
|
|
742
875
|
f.write(vsix_hash)
|
|
743
876
|
|
|
744
877
|
|
|
878
|
+
def _installed_extension_candidate(
|
|
879
|
+
version: str, vsix_path: str, *, remote: bool
|
|
880
|
+
) -> str | None:
|
|
881
|
+
if _extension_on_disk(version, vsix_path, remote=remote):
|
|
882
|
+
return version
|
|
883
|
+
return _newer_extension_on_disk(version, remote=remote)
|
|
884
|
+
|
|
885
|
+
|
|
745
886
|
def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
746
887
|
"""Verify or install the bundled opener for the current VS Code window.
|
|
747
888
|
|
|
@@ -813,7 +954,10 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
813
954
|
)
|
|
814
955
|
return False
|
|
815
956
|
|
|
816
|
-
|
|
957
|
+
installed_version = _installed_extension_candidate(
|
|
958
|
+
ext_version, vsix_path, remote=is_remote
|
|
959
|
+
)
|
|
960
|
+
if installed_version == ext_version:
|
|
817
961
|
active_matches = (
|
|
818
962
|
active_version == ext_version
|
|
819
963
|
if is_remote
|
|
@@ -830,7 +974,7 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
830
974
|
)
|
|
831
975
|
return True
|
|
832
976
|
|
|
833
|
-
newer_version =
|
|
977
|
+
newer_version = installed_version if installed_version != ext_version else None
|
|
834
978
|
if newer_version is not None:
|
|
835
979
|
active_matches = (
|
|
836
980
|
active_version == newer_version
|
|
@@ -851,6 +995,72 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
851
995
|
if is_remote:
|
|
852
996
|
_VSCODE_EXT_INSTALL_FAILED = True
|
|
853
997
|
return False
|
|
998
|
+
|
|
999
|
+
with _vscode_extension_install_lock() as acquired:
|
|
1000
|
+
if not acquired:
|
|
1001
|
+
_VSCODE_EXT_INSTALL_FAILED = True
|
|
1002
|
+
print(
|
|
1003
|
+
"[ArrayView] timed out waiting for another VS Code opener install; "
|
|
1004
|
+
"no viewer request was sent",
|
|
1005
|
+
flush=True,
|
|
1006
|
+
)
|
|
1007
|
+
return False
|
|
1008
|
+
|
|
1009
|
+
# Another ArrayView process may have completed the shared-profile install
|
|
1010
|
+
# while this process waited. Re-read both disk/registry and live-host state
|
|
1011
|
+
# before consulting failure guards or starting a duplicate installer.
|
|
1012
|
+
installed_version = _installed_extension_candidate(
|
|
1013
|
+
ext_version, vsix_path, remote=is_remote
|
|
1014
|
+
)
|
|
1015
|
+
if installed_version is not None:
|
|
1016
|
+
active_registration = _active_extension_registration()
|
|
1017
|
+
active_version = (
|
|
1018
|
+
active_registration.get("extensionVersion")
|
|
1019
|
+
if active_registration is not None
|
|
1020
|
+
else None
|
|
1021
|
+
)
|
|
1022
|
+
active_marker = _extension_registration_marker(active_registration)
|
|
1023
|
+
active_matches = (
|
|
1024
|
+
active_version == installed_version
|
|
1025
|
+
if is_remote
|
|
1026
|
+
else active_version in (None, installed_version)
|
|
1027
|
+
)
|
|
1028
|
+
if not active_matches:
|
|
1029
|
+
return _reject_unmatched_host(installed_version)
|
|
1030
|
+
if not is_remote and installed_version == ext_version:
|
|
1031
|
+
_remove_old_extension_versions(ext_version, remote=False)
|
|
1032
|
+
_VSCODE_EXT_INSTALLED = True
|
|
1033
|
+
_vprint(
|
|
1034
|
+
f"[ArrayView] extension v{installed_version} already installed — "
|
|
1035
|
+
"skipping reinstall",
|
|
1036
|
+
flush=True,
|
|
1037
|
+
)
|
|
1038
|
+
return True
|
|
1039
|
+
|
|
1040
|
+
return _install_vscode_extension_locked(
|
|
1041
|
+
code=code,
|
|
1042
|
+
env_ipc=_find_vscode_ipc_hook(),
|
|
1043
|
+
ext_version=ext_version,
|
|
1044
|
+
vsix_path=vsix_path,
|
|
1045
|
+
is_remote=is_remote,
|
|
1046
|
+
active_version=active_version,
|
|
1047
|
+
active_marker=active_marker,
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def _install_vscode_extension_locked(
|
|
1052
|
+
*,
|
|
1053
|
+
code: str,
|
|
1054
|
+
env_ipc: str | None,
|
|
1055
|
+
ext_version: str,
|
|
1056
|
+
vsix_path: str,
|
|
1057
|
+
is_remote: bool,
|
|
1058
|
+
active_version: str | None,
|
|
1059
|
+
active_marker: tuple | None,
|
|
1060
|
+
) -> bool:
|
|
1061
|
+
"""Run and verify one install while the shared-profile lock is held."""
|
|
1062
|
+
global _VSCODE_EXT_INSTALLED, _VSCODE_EXT_FRESH_INSTALL
|
|
1063
|
+
global _VSCODE_EXT_RELOAD_REQUIRED, _VSCODE_EXT_INSTALL_FAILED
|
|
854
1064
|
install_guard = _extension_install_failure_guard(
|
|
855
1065
|
ext_version, code, active_marker
|
|
856
1066
|
)
|
|
@@ -870,11 +1080,13 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
870
1080
|
return False
|
|
871
1081
|
|
|
872
1082
|
env = dict(os.environ)
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
env["VSCODE_IPC_HOOK_CLI"] = ipc
|
|
1083
|
+
if env_ipc:
|
|
1084
|
+
env["VSCODE_IPC_HOOK_CLI"] = env_ipc
|
|
876
1085
|
|
|
877
1086
|
install_snapshot = _extension_base_snapshot() if is_remote else {}
|
|
1087
|
+
orphaned_install = _extension_directory_present(
|
|
1088
|
+
ext_version, remote=is_remote
|
|
1089
|
+
)
|
|
878
1090
|
# Say so before the slow part, not after it. Installing the opener takes
|
|
879
1091
|
# several seconds and may end in "reload this window once"; announcing that
|
|
880
1092
|
# only in the final error message left the terminal looking hung, with the
|
|
@@ -885,8 +1097,11 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
885
1097
|
flush=True,
|
|
886
1098
|
)
|
|
887
1099
|
try:
|
|
1100
|
+
command = [code, "--install-extension", vsix_path]
|
|
1101
|
+
if orphaned_install:
|
|
1102
|
+
command.append("--force")
|
|
888
1103
|
r = _run_extension_installer(
|
|
889
|
-
|
|
1104
|
+
command, env
|
|
890
1105
|
)
|
|
891
1106
|
combined = (r.stdout or "") + (r.stderr or "")
|
|
892
1107
|
install_failed = (
|
|
@@ -921,6 +1136,23 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
921
1136
|
install_bases = (active_base,)
|
|
922
1137
|
else:
|
|
923
1138
|
install_bases = _extension_bases(remote=False)
|
|
1139
|
+
registered_bases = tuple(
|
|
1140
|
+
base
|
|
1141
|
+
for base in install_bases
|
|
1142
|
+
if _wait_for_extension_registration(base, ext_version)
|
|
1143
|
+
)
|
|
1144
|
+
if not registered_bases:
|
|
1145
|
+
_VSCODE_EXT_INSTALL_FAILED = True
|
|
1146
|
+
message = (
|
|
1147
|
+
"the installer extracted the opener but did not register it "
|
|
1148
|
+
"in the selected VS Code profile"
|
|
1149
|
+
)
|
|
1150
|
+
_write_extension_install_failure_guard(
|
|
1151
|
+
ext_version, code, active_marker, message
|
|
1152
|
+
)
|
|
1153
|
+
print(f"[ArrayView] extension install incomplete: {message}", flush=True)
|
|
1154
|
+
return False
|
|
1155
|
+
install_bases = registered_bases
|
|
924
1156
|
_patch_vscode_extension_metadata(ext_version, bases=install_bases)
|
|
925
1157
|
try:
|
|
926
1158
|
_write_vscode_extension_hash(
|
arrayview/arrayview-opener.vsix
CHANGED
|
Binary file
|
|
@@ -26,7 +26,7 @@ arrayview/_routes_loading.py,sha256=s7Vc33Og9Cr2YrsFX7VP5lwjLY5Aa6fv9WoqQfJAsR8,
|
|
|
26
26
|
arrayview/_routes_persistence.py,sha256=T03csHb7ppGT-8yMDf227XkqhnhjgSLYj0RW2wmg59g,17176
|
|
27
27
|
arrayview/_routes_preferences.py,sha256=SKnhTf2X2vRyYi06Wb2si4dXMB1y0XiSBr28RZ5oKUw,4691
|
|
28
28
|
arrayview/_routes_preload.py,sha256=KOsankNguxdIWmFW9iZcY9p2SvUN1mOKY7KkW1qMGcc,1729
|
|
29
|
-
arrayview/_routes_query.py,sha256=
|
|
29
|
+
arrayview/_routes_query.py,sha256=6ECRn15sPa0h0bxlu61u2l7pjRiettds1d_82SqalyM,21964
|
|
30
30
|
arrayview/_routes_rendering.py,sha256=EQq2QzdtUS_pv35k7prLdlRdO5nByIlQ_Eu0nhVsn1Y,22970
|
|
31
31
|
arrayview/_routes_segmentation.py,sha256=x2bBjQffZryeTMmOH38YdBWethw1o3KpA8V0chJcZHI,23599
|
|
32
32
|
arrayview/_routes_state.py,sha256=T_xoYcRlv7L47ydTxxiXb0Fp5M5x9KR2U-wkxaVeuMY,12462
|
|
@@ -42,17 +42,17 @@ arrayview/_synthetic_mri.py,sha256=CNL-2WC835i6WCqkvmWBCO6WQ5w7HE5trNb8OyEfe64,7
|
|
|
42
42
|
arrayview/_torch.py,sha256=13sqIMCUu_-b9SQ9KfDCExDz2UMStL87EDVjnbJxAEo,7536
|
|
43
43
|
arrayview/_tutorial.py,sha256=U09j6hcps_dp3_azivvAoTChwqjNYGNLFqu9Cz1bOzE,6839
|
|
44
44
|
arrayview/_vectorfield.py,sha256=eDqMCMEGjO8Jdt-6Auh01cz3WD9dbF7xYUWD9xHAf1E,8115
|
|
45
|
-
arrayview/_viewer.html,sha256=
|
|
45
|
+
arrayview/_viewer.html,sha256=k5LoQgwjo4GcRBTJNkQoaRJqpjwRIRr1tUrvMwEQO-g,2103699
|
|
46
46
|
arrayview/_vscode.py,sha256=EKnGN2zrjlybc4FQOmYA8BvYdtkcJbLHDEdgtvO7ul4,837
|
|
47
47
|
arrayview/_vscode_browser.py,sha256=xypCgIwphII1Jgb6xC92gJu4Zo-oHtwjceymOqq4D_Y,20132
|
|
48
|
-
arrayview/_vscode_extension.py,sha256=
|
|
48
|
+
arrayview/_vscode_extension.py,sha256=RFXB1rMHnW2BUdbFtrSGvrok8iii3zNWM2dN3U6XV-c,53156
|
|
49
49
|
arrayview/_vscode_signal.py,sha256=8kf6rHrowQ0P6HzHavqb1IWweKh-JyB_rq39v80Bkks,51366
|
|
50
50
|
arrayview/_watch.py,sha256=PxxS_5hcj_S6xQf9Ep4p1HLRkrI5iDMh1aRa4olV7ew,1873
|
|
51
|
-
arrayview/arrayview-opener.vsix,sha256=
|
|
51
|
+
arrayview/arrayview-opener.vsix,sha256=BBE4qdw5k1CRrbkkO7ok15FeCRsbPxRNjsZlv56UK_A,55048
|
|
52
52
|
arrayview/gsap.min.js,sha256=VGihm4idI0E1uqrWezWsiScB9F_jMosn7qs6pPSkeac,72304
|
|
53
53
|
arrayview/todo.md,sha256=4rzoaIY4nJJACDHPJKeqP8guasCMxeRQ1cyNLxUYink,999
|
|
54
|
-
arrayview-0.43.
|
|
55
|
-
arrayview-0.43.
|
|
56
|
-
arrayview-0.43.
|
|
57
|
-
arrayview-0.43.
|
|
58
|
-
arrayview-0.43.
|
|
54
|
+
arrayview-0.43.3.dist-info/METADATA,sha256=az_70CEhvCdtbjXQq8UhTvHPpJfTk6UaDJCFxeLGM6E,2515
|
|
55
|
+
arrayview-0.43.3.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
56
|
+
arrayview-0.43.3.dist-info/entry_points.txt,sha256=-0s08axutXkTbINiKs0Q7iv-33aWuTaamUg2q2-NyQ4,60
|
|
57
|
+
arrayview-0.43.3.dist-info/licenses/LICENSE,sha256=34OivjM9faB2QaPjZpMRTIiSx31V3U9cEyQ6F2k-bH0,1062
|
|
58
|
+
arrayview-0.43.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|