opencode-docker-panel 0.5.0 → 0.6.0
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.
- package/CHANGELOG.md +96 -70
- package/README.md +90 -87
- package/README.ru.md +80 -77
- package/dist/commands.js +5 -5
- package/dist/compose.js +18 -18
- package/dist/events.js +103 -0
- package/dist/panel.js +78 -76
- package/dist/poll.js +92 -27
- package/dist/tui.js +0 -5
- package/package.json +79 -76
package/dist/events.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const COMMAND = "docker";
|
|
3
|
+
const ARGS = ["events", "--filter", "type=container", "--format", "{{json .}}"];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The actions that can change what the panel draws.
|
|
7
|
+
*
|
|
8
|
+
* `kill` is left out because it arrives as a pair ahead of `stop` and `die` and changes nothing on its
|
|
9
|
+
* own. `create` is out because the container stays `created` for the half second before `start` and the
|
|
10
|
+
* panel draws no row for that state. `stop` is out because `docker ps` already reports `exited` by the
|
|
11
|
+
* time it arrives. `restart` is out because a measured `docker restart` sends `die` and `start` first,
|
|
12
|
+
* which bracket the whole visible change, so it would repeat the frame `start` just produced.
|
|
13
|
+
* Everything with an `exec_` or `health_` prefix is the container's internals and never reaches the panel.
|
|
14
|
+
*/
|
|
15
|
+
export const INTERESTING_ACTIONS = ["start", "die", "destroy", "pause", "unpause", "rename"];
|
|
16
|
+
/** Returns the container name an event is about, or null when the panel has no reason to react */
|
|
17
|
+
export function parseEventLine(line) {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
if (!trimmed) return null;
|
|
20
|
+
let raw;
|
|
21
|
+
try {
|
|
22
|
+
raw = JSON.parse(trimmed);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
27
|
+
const event = raw;
|
|
28
|
+
if (event.Type !== "container") return null;
|
|
29
|
+
const action = String(event.Action ?? event.status ?? "");
|
|
30
|
+
if (!INTERESTING_ACTIONS.includes(action)) return null;
|
|
31
|
+
const name = event.Actor?.Attributes?.name;
|
|
32
|
+
return typeof name === "string" && name.length > 0 ? name : null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A long-lived `docker events` reader.
|
|
36
|
+
*
|
|
37
|
+
* The stream ends loudly rather than going quiet: stopping Docker Desktop leaves it silent for about
|
|
38
|
+
* thirty seconds and then writes `unexpected EOF` and exits with code 1. So the caller is told when the
|
|
39
|
+
* process is gone and decides on its own when reopening is worth the attempt.
|
|
40
|
+
*/
|
|
41
|
+
export function createEventWatch() {
|
|
42
|
+
let child;
|
|
43
|
+
let buffer = "";
|
|
44
|
+
let closed = false;
|
|
45
|
+
let consumer;
|
|
46
|
+
let onDead;
|
|
47
|
+
const open = () => {
|
|
48
|
+
if (closed) return;
|
|
49
|
+
const spawned = spawn(COMMAND, ARGS, {
|
|
50
|
+
windowsHide: true,
|
|
51
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
52
|
+
});
|
|
53
|
+
child = spawned;
|
|
54
|
+
// A docker that is not on the PATH makes spawn emit an error, and an unheard one throws
|
|
55
|
+
spawned.on("error", () => {});
|
|
56
|
+
// A half-written line from the killed process would prepend itself to the new process's first
|
|
57
|
+
// chunk and take that first event down with it
|
|
58
|
+
buffer = "";
|
|
59
|
+
spawned.stdout?.setEncoding("utf8");
|
|
60
|
+
spawned.stdout?.on("data", chunk => {
|
|
61
|
+
// The last split piece is a partial line and has to survive to the next chunk
|
|
62
|
+
const lines = (buffer + chunk).split("\n");
|
|
63
|
+
buffer = lines.pop() ?? "";
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
if (!parseEventLine(line)) continue;
|
|
66
|
+
consumer?.();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// Only the process still in hand gets to speak for the stream, and killing one is asynchronous, so
|
|
70
|
+
// after a restart the previous child still reports in. Trusting that report would drop the live
|
|
71
|
+
// child's reference, orphan the process and fake a dead stream. The same check covers our own stop,
|
|
72
|
+
// which clears `child` before killing.
|
|
73
|
+
spawned.on("close", () => {
|
|
74
|
+
if (child !== spawned) return;
|
|
75
|
+
child = undefined;
|
|
76
|
+
onDead?.();
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const stop = () => {
|
|
80
|
+
const current = child;
|
|
81
|
+
child = undefined;
|
|
82
|
+
current?.kill();
|
|
83
|
+
};
|
|
84
|
+
open();
|
|
85
|
+
return {
|
|
86
|
+
onEvent: onChange => {
|
|
87
|
+
consumer = onChange;
|
|
88
|
+
},
|
|
89
|
+
onDead: handler => {
|
|
90
|
+
onDead = handler;
|
|
91
|
+
},
|
|
92
|
+
restart: () => {
|
|
93
|
+
stop();
|
|
94
|
+
open();
|
|
95
|
+
},
|
|
96
|
+
close: () => {
|
|
97
|
+
closed = true;
|
|
98
|
+
consumer = undefined;
|
|
99
|
+
onDead = undefined;
|
|
100
|
+
stop();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
package/dist/panel.js
CHANGED
|
@@ -227,9 +227,10 @@ export function DockerPanel(props) {
|
|
|
227
227
|
const engine = createEngineProbe();
|
|
228
228
|
// The probe only has to be re-read when the poll changes its verdict: an available docker proves the
|
|
229
229
|
// engine answers, and a probe on every poll would stack several four second calls on the same pipe,
|
|
230
|
-
// while never re-reading it would leave a stale stopped engine once docker starts from the tray
|
|
230
|
+
// while never re-reading it would leave a stale stopped engine once docker starts from the tray, which
|
|
231
|
+
// poll.ts covers by keeping the poll fast while the last answer said stopped
|
|
231
232
|
let lastKind = "";
|
|
232
|
-
const polling = createDockerPolling(props.
|
|
233
|
+
const polling = createDockerPolling(props.reload, next => {
|
|
233
234
|
refreshStack();
|
|
234
235
|
if (next.kind === lastKind) return;
|
|
235
236
|
lastKind = next.kind;
|
|
@@ -275,12 +276,12 @@ export function DockerPanel(props) {
|
|
|
275
276
|
});
|
|
276
277
|
await polling.refreshAfterAction();
|
|
277
278
|
};
|
|
278
|
-
/**
|
|
279
|
-
* Opens the same actions for a container the sidebar does not show, and carries the one engine
|
|
280
|
-
* action last.
|
|
281
|
-
*
|
|
282
|
-
* The engine entry stays available with no containers at all, because that is exactly when Docker is
|
|
283
|
-
* down and it is the only way to turn it back on.
|
|
279
|
+
/**
|
|
280
|
+
* Opens the same actions for a container the sidebar does not show, and carries the one engine
|
|
281
|
+
* action last.
|
|
282
|
+
*
|
|
283
|
+
* The engine entry stays available with no containers at all, because that is exactly when Docker is
|
|
284
|
+
* down and it is the only way to turn it back on.
|
|
284
285
|
*/
|
|
285
286
|
const pickContainer = async all => {
|
|
286
287
|
const action = engine.state() === "running" ? "engine-stop" : undefined;
|
|
@@ -325,11 +326,11 @@ export function DockerPanel(props) {
|
|
|
325
326
|
await pickContainer(polling.state().containers);
|
|
326
327
|
};
|
|
327
328
|
|
|
328
|
-
/**
|
|
329
|
-
* Both engine actions ask first, and Stop names what it is about to end.
|
|
330
|
-
*
|
|
331
|
-
* Start confirms too: it brings up a virtual machine and starts consuming memory, so a misclick is
|
|
332
|
-
* not free even though nothing is running that it would interrupt.
|
|
329
|
+
/**
|
|
330
|
+
* Both engine actions ask first, and Stop names what it is about to end.
|
|
331
|
+
*
|
|
332
|
+
* Start confirms too: it brings up a virtual machine and starts consuming memory, so a misclick is
|
|
333
|
+
* not free even though nothing is running that it would interrupt.
|
|
333
334
|
*/
|
|
334
335
|
const runRuntime = async action => {
|
|
335
336
|
if (busy()) return;
|
|
@@ -536,8 +537,9 @@ export function DockerPanel(props) {
|
|
|
536
537
|
return _$memo(() => !!(!stopping() && !hasRows()))() && polling.state().containers.length > 0;
|
|
537
538
|
},
|
|
538
539
|
get children() {
|
|
539
|
-
var _el$30 = _$createElement("text")
|
|
540
|
-
|
|
540
|
+
var _el$30 = _$createElement("text"),
|
|
541
|
+
_el$31 = _$createTextNode(` click for the list`);
|
|
542
|
+
_$insertNode(_el$30, _el$31);
|
|
541
543
|
_$setProp(_el$30, "onMouseUp", event => arm(openBrowse, event));
|
|
542
544
|
_$effect(_$p => _$setProp(_el$30, "fg", theme().text.muted, _$p));
|
|
543
545
|
return _el$30;
|
|
@@ -548,27 +550,27 @@ export function DockerPanel(props) {
|
|
|
548
550
|
return _$memo(() => !!engineDown())() && !busy();
|
|
549
551
|
},
|
|
550
552
|
get children() {
|
|
551
|
-
var _el$
|
|
552
|
-
_el$
|
|
553
|
-
_el$
|
|
554
|
-
_$insertNode(_el$
|
|
555
|
-
_$insertNode(_el$
|
|
556
|
-
_$setProp(_el$
|
|
557
|
-
_$insertNode(_el$
|
|
558
|
-
_$insertNode(_el$
|
|
553
|
+
var _el$33 = _$createElement("text"),
|
|
554
|
+
_el$34 = _$createElement("b"),
|
|
555
|
+
_el$36 = _$createElement("span");
|
|
556
|
+
_$insertNode(_el$33, _el$34);
|
|
557
|
+
_$insertNode(_el$33, _el$36);
|
|
558
|
+
_$setProp(_el$33, "onMouseUp", event => arm(() => void runRuntime("engine-start"), event));
|
|
559
|
+
_$insertNode(_el$34, _$createTextNode(`Start Docker Desktop`));
|
|
560
|
+
_$insertNode(_el$36, _$createTextNode(` · click to start`));
|
|
559
561
|
_$effect(_p$ => {
|
|
560
562
|
var _v$6 = theme().text.base,
|
|
561
563
|
_v$7 = {
|
|
562
564
|
fg: theme().text.muted
|
|
563
565
|
};
|
|
564
|
-
_v$6 !== _p$.e && (_p$.e = _$setProp(_el$
|
|
565
|
-
_v$7 !== _p$.t && (_p$.t = _$setProp(_el$
|
|
566
|
+
_v$6 !== _p$.e && (_p$.e = _$setProp(_el$33, "fg", _v$6, _p$.e));
|
|
567
|
+
_v$7 !== _p$.t && (_p$.t = _$setProp(_el$36, "style", _v$7, _p$.t));
|
|
566
568
|
return _p$;
|
|
567
569
|
}, {
|
|
568
570
|
e: undefined,
|
|
569
571
|
t: undefined
|
|
570
572
|
});
|
|
571
|
-
return _el$
|
|
573
|
+
return _el$33;
|
|
572
574
|
}
|
|
573
575
|
}), null);
|
|
574
576
|
_$insert(_el$18, _$createComponent(Show, {
|
|
@@ -576,12 +578,12 @@ export function DockerPanel(props) {
|
|
|
576
578
|
return _$memo(() => !!(!engineDown() && polling.state().containers.length === 0))() && polling.state().detail.length > 0;
|
|
577
579
|
},
|
|
578
580
|
get children() {
|
|
579
|
-
var _el$
|
|
580
|
-
_el$
|
|
581
|
-
_$insertNode(_el$
|
|
582
|
-
_$insert(_el$
|
|
583
|
-
_$effect(_$p => _$setProp(_el$
|
|
584
|
-
return _el$
|
|
581
|
+
var _el$38 = _$createElement("text"),
|
|
582
|
+
_el$39 = _$createTextNode(` `);
|
|
583
|
+
_$insertNode(_el$38, _el$39);
|
|
584
|
+
_$insert(_el$38, () => polling.state().detail, null);
|
|
585
|
+
_$effect(_$p => _$setProp(_el$38, "fg", theme().text.muted, _$p));
|
|
586
|
+
return _el$38;
|
|
585
587
|
}
|
|
586
588
|
}), null);
|
|
587
589
|
_$insert(_el$18, _$createComponent(Show, {
|
|
@@ -594,86 +596,86 @@ export function DockerPanel(props) {
|
|
|
594
596
|
return visible();
|
|
595
597
|
},
|
|
596
598
|
children: container => (() => {
|
|
597
|
-
var _el$
|
|
598
|
-
_el$
|
|
599
|
-
_el$
|
|
600
|
-
_el$
|
|
601
|
-
_el$
|
|
602
|
-
_$insertNode(_el$
|
|
603
|
-
_$insertNode(_el$
|
|
604
|
-
_$setProp(_el$
|
|
605
|
-
_$setProp(_el$
|
|
606
|
-
_$setProp(_el$
|
|
607
|
-
_$insertNode(_el$
|
|
608
|
-
_$setProp(_el$
|
|
609
|
-
_$insertNode(_el$
|
|
610
|
-
_$insertNode(_el$
|
|
611
|
-
_$setProp(_el$
|
|
612
|
-
_$insert(_el$
|
|
613
|
-
_$insert(_el$
|
|
599
|
+
var _el$48 = _$createElement("box"),
|
|
600
|
+
_el$49 = _$createElement("text"),
|
|
601
|
+
_el$51 = _$createElement("text"),
|
|
602
|
+
_el$52 = _$createTextNode(` `),
|
|
603
|
+
_el$53 = _$createElement("span");
|
|
604
|
+
_$insertNode(_el$48, _el$49);
|
|
605
|
+
_$insertNode(_el$48, _el$51);
|
|
606
|
+
_$setProp(_el$48, "flexDirection", "row");
|
|
607
|
+
_$setProp(_el$48, "gap", 1);
|
|
608
|
+
_$setProp(_el$48, "onMouseUp", event => arm(() => openMenu(container()), event));
|
|
609
|
+
_$insertNode(_el$49, _$createTextNode(`•`));
|
|
610
|
+
_$setProp(_el$49, "flexShrink", 0);
|
|
611
|
+
_$insertNode(_el$51, _el$52);
|
|
612
|
+
_$insertNode(_el$51, _el$53);
|
|
613
|
+
_$setProp(_el$51, "wrapMode", "word");
|
|
614
|
+
_$insert(_el$51, () => container().name, _el$52);
|
|
615
|
+
_$insert(_el$53, () => detailLabel(container(), props.pinned));
|
|
614
616
|
_$effect(_p$ => {
|
|
615
617
|
var _v$0 = dotColor(theme(), container().state),
|
|
616
618
|
_v$1 = theme().text.base,
|
|
617
619
|
_v$10 = {
|
|
618
620
|
fg: theme().text.muted
|
|
619
621
|
};
|
|
620
|
-
_v$0 !== _p$.e && (_p$.e = _$setProp(_el$
|
|
621
|
-
_v$1 !== _p$.t && (_p$.t = _$setProp(_el$
|
|
622
|
-
_v$10 !== _p$.a && (_p$.a = _$setProp(_el$
|
|
622
|
+
_v$0 !== _p$.e && (_p$.e = _$setProp(_el$49, "fg", _v$0, _p$.e));
|
|
623
|
+
_v$1 !== _p$.t && (_p$.t = _$setProp(_el$51, "fg", _v$1, _p$.t));
|
|
624
|
+
_v$10 !== _p$.a && (_p$.a = _$setProp(_el$53, "style", _v$10, _p$.a));
|
|
623
625
|
return _p$;
|
|
624
626
|
}, {
|
|
625
627
|
e: undefined,
|
|
626
628
|
t: undefined,
|
|
627
629
|
a: undefined
|
|
628
630
|
});
|
|
629
|
-
return _el$
|
|
631
|
+
return _el$48;
|
|
630
632
|
})()
|
|
631
633
|
}), _$createComponent(Show, {
|
|
632
634
|
get when() {
|
|
633
635
|
return hasRows();
|
|
634
636
|
},
|
|
635
637
|
get children() {
|
|
636
|
-
var _el$
|
|
637
|
-
_el$
|
|
638
|
-
_$insertNode(_el$
|
|
639
|
-
_$setProp(_el$
|
|
640
|
-
_$insert(_el$
|
|
641
|
-
_$effect(_$p => _$setProp(_el$
|
|
642
|
-
return _el$
|
|
638
|
+
var _el$40 = _$createElement("text"),
|
|
639
|
+
_el$41 = _$createTextNode(` `);
|
|
640
|
+
_$insertNode(_el$40, _el$41);
|
|
641
|
+
_$setProp(_el$40, "onMouseUp", event => arm(openBrowse, event));
|
|
642
|
+
_$insert(_el$40, browseLabel, null);
|
|
643
|
+
_$effect(_$p => _$setProp(_el$40, "fg", theme().text.muted, _$p));
|
|
644
|
+
return _el$40;
|
|
643
645
|
}
|
|
644
646
|
})];
|
|
645
647
|
}
|
|
646
648
|
}), null);
|
|
647
649
|
_$insert(_el$18, _$createComponent(Show, {
|
|
648
650
|
get when() {
|
|
649
|
-
return _$memo(() => !!pendingStack())() && !
|
|
651
|
+
return _$memo(() => !!(pendingStack() && !engineDown()))() && !stopping();
|
|
650
652
|
},
|
|
651
653
|
get children() {
|
|
652
|
-
var _el$
|
|
653
|
-
_el$
|
|
654
|
-
_el$
|
|
655
|
-
_el$
|
|
656
|
-
_el$
|
|
657
|
-
_$insertNode(_el$
|
|
658
|
-
_$insertNode(_el$
|
|
659
|
-
_$setProp(_el$
|
|
660
|
-
_$insertNode(_el$
|
|
661
|
-
_$insertNode(_el$
|
|
662
|
-
_$insertNode(_el$
|
|
663
|
-
_$insert(_el$
|
|
654
|
+
var _el$42 = _$createElement("text"),
|
|
655
|
+
_el$43 = _$createElement("b"),
|
|
656
|
+
_el$45 = _$createElement("span"),
|
|
657
|
+
_el$46 = _$createTextNode(` · `),
|
|
658
|
+
_el$47 = _$createTextNode(` is not running here`);
|
|
659
|
+
_$insertNode(_el$42, _el$43);
|
|
660
|
+
_$insertNode(_el$42, _el$45);
|
|
661
|
+
_$setProp(_el$42, "onMouseUp", event => arm(() => void openStack(), event));
|
|
662
|
+
_$insertNode(_el$43, _$createTextNode(`Up stack`));
|
|
663
|
+
_$insertNode(_el$45, _el$46);
|
|
664
|
+
_$insertNode(_el$45, _el$47);
|
|
665
|
+
_$insert(_el$45, () => pendingStack()?.project, _el$47);
|
|
664
666
|
_$effect(_p$ => {
|
|
665
667
|
var _v$8 = theme().text.base,
|
|
666
668
|
_v$9 = {
|
|
667
669
|
fg: theme().text.muted
|
|
668
670
|
};
|
|
669
|
-
_v$8 !== _p$.e && (_p$.e = _$setProp(_el$
|
|
670
|
-
_v$9 !== _p$.t && (_p$.t = _$setProp(_el$
|
|
671
|
+
_v$8 !== _p$.e && (_p$.e = _$setProp(_el$42, "fg", _v$8, _p$.e));
|
|
672
|
+
_v$9 !== _p$.t && (_p$.t = _$setProp(_el$45, "style", _v$9, _p$.t));
|
|
671
673
|
return _p$;
|
|
672
674
|
}, {
|
|
673
675
|
e: undefined,
|
|
674
676
|
t: undefined
|
|
675
677
|
});
|
|
676
|
-
return _el$
|
|
678
|
+
return _el$42;
|
|
677
679
|
}
|
|
678
680
|
}), null);
|
|
679
681
|
_$effect(_$p => _$setProp(_el$20, "fg", theme().text.base, _$p));
|
package/dist/poll.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createSignal, onCleanup } from "solid-js";
|
|
2
|
+
import { createEventWatch } from "./events.js";
|
|
2
3
|
import { runDockerPs } from "./docker.js";
|
|
3
4
|
import { runDesktopStatus } from "./runtime.js";
|
|
4
5
|
const INITIAL = {
|
|
@@ -10,10 +11,26 @@ const INITIAL = {
|
|
|
10
11
|
/** docker reports the old state for a moment after start or stop, so an action is polled twice */
|
|
11
12
|
const SETTLE_MS = 1200;
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
/**
|
|
15
|
+
* Events drive the panel now, so this is only the net for the case where docker says nothing at all:
|
|
16
|
+
* a container changed outside anything we would hear about, or the stream is gone and the engine has
|
|
17
|
+
* come back since the last attempt.
|
|
18
|
+
*/
|
|
19
|
+
export const FALLBACK_INTERVAL_MS = 30000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Nothing can change while the engine is down, so a start from the tray is the one thing worth waiting
|
|
23
|
+
* on closely, and the engine probe has no timer of its own to notice it.
|
|
24
|
+
*/
|
|
25
|
+
const ENGINE_DOWN_RETRY_MS = 3000;
|
|
26
|
+
export function fallbackDelayMs(kind) {
|
|
27
|
+
return kind === "stopped" ? ENGINE_DOWN_RETRY_MS : FALLBACK_INTERVAL_MS;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A Docker Desktop cold start boots a virtual machine, which takes a minute and answers `unknown` while
|
|
32
|
+
* it goes. Polling faster than the container loop is what makes the panel notice it came up at all,
|
|
33
|
+
* and the deadline is what stops a dead engine from holding `working` forever.
|
|
17
34
|
*/
|
|
18
35
|
export const ENGINE_TRANSITION_MS = 2000;
|
|
19
36
|
export const ENGINE_DEADLINE_MS = 120000;
|
|
@@ -27,11 +44,11 @@ export function keepContainers(previous, next) {
|
|
|
27
44
|
/** The sidebar belongs to what runs right now, not to every container that exists */
|
|
28
45
|
export const MAX_RUNNING = 5;
|
|
29
46
|
|
|
30
|
-
/**
|
|
31
|
-
* A pinned container is shown whatever its state and spends the running budget when it happens to
|
|
32
|
-
* run, so pinning everything shows everything.
|
|
33
|
-
*
|
|
34
|
-
* Stopped containers get no row, the dialog is their only door.
|
|
47
|
+
/**
|
|
48
|
+
* A pinned container is shown whatever its state and spends the running budget when it happens to
|
|
49
|
+
* run, so pinning everything shows everything.
|
|
50
|
+
*
|
|
51
|
+
* Stopped containers get no row, the dialog is their only door.
|
|
35
52
|
*/
|
|
36
53
|
export function selectRows(all, pinned) {
|
|
37
54
|
const isPinned = item => pinned.includes(item.name);
|
|
@@ -39,20 +56,27 @@ export function selectRows(all, pinned) {
|
|
|
39
56
|
const running = all.filter(item => !isPinned(item) && item.state === "running");
|
|
40
57
|
return [...fixed, ...running.slice(0, Math.max(0, MAX_RUNNING - fixed.length))];
|
|
41
58
|
}
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
59
|
+
/**
|
|
60
|
+
* A stream that has closed is only worth reopening once docker answers again. Reopening while the
|
|
61
|
+
* engine is down would spawn a process that cannot work, every tick, for as long as the engine is off.
|
|
62
|
+
*/
|
|
63
|
+
export function shouldReconnect(kind, streamAlive) {
|
|
64
|
+
return !streamAlive && (kind === "ok" || kind === "empty");
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The engine has its own probe because `docker ps` cannot report it: on a stopped engine the ps call
|
|
68
|
+
* fails to connect and says only `unavailable`, which is also what a missing CLI or a locked socket
|
|
69
|
+
* produces. `docker desktop status` is the only thing that tells those apart.
|
|
46
70
|
*/
|
|
47
71
|
export function createEngineProbe() {
|
|
48
72
|
const [state, setState] = createSignal("unknown");
|
|
49
73
|
let disposed = false;
|
|
50
74
|
let probing = false;
|
|
51
75
|
|
|
52
|
-
/**
|
|
53
|
-
* A stopped engine takes over four seconds to answer, which is longer than the container poll, so a
|
|
54
|
-
* probe per poll would leave several of them waiting on the same pipe at once. One at a time is the
|
|
55
|
-
* whole guard: a call that arrives while one is in flight reads the last known state instead.
|
|
76
|
+
/**
|
|
77
|
+
* A stopped engine takes over four seconds to answer, which is longer than the container poll, so a
|
|
78
|
+
* probe per poll would leave several of them waiting on the same pipe at once. One at a time is the
|
|
79
|
+
* whole guard: a call that arrives while one is in flight reads the last known state instead.
|
|
56
80
|
*/
|
|
57
81
|
const refresh = async () => {
|
|
58
82
|
if (probing) return state();
|
|
@@ -93,17 +117,26 @@ function signature(containers) {
|
|
|
93
117
|
|
|
94
118
|
/** A container in a crash loop changes state on every poll and each remount costs a fresh docker ps */
|
|
95
119
|
const RELOAD_COOLDOWN_MS = 10000;
|
|
96
|
-
export function createDockerPolling(
|
|
120
|
+
export function createDockerPolling(onContentChange, onPoll) {
|
|
97
121
|
const [state, setState] = createSignal(INITIAL);
|
|
98
122
|
let timer;
|
|
99
123
|
let settle;
|
|
100
124
|
let disposed = false;
|
|
101
|
-
let
|
|
102
|
-
let
|
|
125
|
+
let shown = "";
|
|
126
|
+
let mounted = false;
|
|
103
127
|
let reloadedAt = 0;
|
|
104
|
-
|
|
105
|
-
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* `shown` is what the host has been told, not what was last seen. A change that arrives inside the
|
|
131
|
+
* cooldown has to stay owed rather than be written off, because the signature would match on every
|
|
132
|
+
* later poll and nothing would ever deliver it.
|
|
133
|
+
*/
|
|
134
|
+
const apply = next => {
|
|
106
135
|
if (disposed) return;
|
|
136
|
+
if (shouldReconnect(next.kind, streamAlive)) {
|
|
137
|
+
streamAlive = true;
|
|
138
|
+
events.restart();
|
|
139
|
+
}
|
|
107
140
|
const visible = keepContainers(state().containers, next);
|
|
108
141
|
setState({
|
|
109
142
|
...next,
|
|
@@ -112,26 +145,58 @@ export function createDockerPolling(intervalMs, onContentChange, onPoll) {
|
|
|
112
145
|
onPoll?.(next);
|
|
113
146
|
if (next.kind === "unavailable" || next.kind === "stale") return;
|
|
114
147
|
const current = signature(visible);
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
seen = true;
|
|
148
|
+
if (!mounted) {
|
|
149
|
+
mounted = true;
|
|
150
|
+
shown = current;
|
|
119
151
|
return;
|
|
120
152
|
}
|
|
153
|
+
if (current === shown) return;
|
|
121
154
|
if (Date.now() - reloadedAt < RELOAD_COOLDOWN_MS) return;
|
|
155
|
+
shown = current;
|
|
122
156
|
reloadedAt = Date.now();
|
|
123
157
|
onContentChange();
|
|
124
158
|
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* One `docker ps` at a time. Events arrive faster than a ps call returns, and without this the older
|
|
162
|
+
* result lands last and overwrites the newer one. A call that arrives while one is in flight asks for
|
|
163
|
+
* one more pass instead of starting a parallel one, the same guard `createEngineProbe` uses.
|
|
164
|
+
*/
|
|
165
|
+
let reading = false;
|
|
166
|
+
let reread = false;
|
|
167
|
+
const refresh = async () => {
|
|
168
|
+
if (reading) {
|
|
169
|
+
reread = true;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
reading = true;
|
|
173
|
+
try {
|
|
174
|
+
do {
|
|
175
|
+
reread = false;
|
|
176
|
+
apply(await runDockerPs());
|
|
177
|
+
} while (reread && !disposed);
|
|
178
|
+
} finally {
|
|
179
|
+
reading = false;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const events = createEventWatch();
|
|
183
|
+
let streamAlive = true;
|
|
184
|
+
events.onEvent(() => void refresh());
|
|
185
|
+
events.onDead(() => {
|
|
186
|
+
streamAlive = false;
|
|
187
|
+
});
|
|
125
188
|
const tick = async () => {
|
|
126
189
|
await refresh();
|
|
127
190
|
if (disposed) return;
|
|
128
|
-
timer = setTimeout(tick,
|
|
191
|
+
timer = setTimeout(tick, fallbackDelayMs(state().kind));
|
|
129
192
|
};
|
|
130
193
|
void tick();
|
|
131
194
|
onCleanup(() => {
|
|
132
195
|
disposed = true;
|
|
133
196
|
if (timer) clearTimeout(timer);
|
|
134
197
|
if (settle) clearTimeout(settle);
|
|
198
|
+
// A docker events process outlives the panel otherwise and stays in the task list
|
|
199
|
+
events.close();
|
|
135
200
|
});
|
|
136
201
|
return {
|
|
137
202
|
state,
|
package/dist/tui.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
2
2
|
import { DockerPanel } from "./panel.js";
|
|
3
|
-
const MIN_INTERVAL = 1000;
|
|
4
|
-
const MAX_INTERVAL = 60000;
|
|
5
3
|
const plugin = {
|
|
6
4
|
id: "docker.panel.cli",
|
|
7
5
|
setup(context) {
|
|
8
|
-
const requested = Number(context.options.intervalMs);
|
|
9
|
-
const intervalMs = Number.isFinite(requested) ? Math.min(Math.max(requested, MIN_INTERVAL), MAX_INTERVAL) : 3000;
|
|
10
6
|
const agentDir = context.location?.directory ?? process.cwd();
|
|
11
7
|
const [pinned, setPinned] = context.storage.store("docker-panel.pinned", {
|
|
12
8
|
initial: {
|
|
@@ -33,7 +29,6 @@ const plugin = {
|
|
|
33
29
|
const claim = {
|
|
34
30
|
append: "sidebar.content",
|
|
35
31
|
render: () => _$createComponent(DockerPanel, {
|
|
36
|
-
intervalMs: intervalMs,
|
|
37
32
|
agentDir: agentDir,
|
|
38
33
|
get pinned() {
|
|
39
34
|
return pinned.names;
|