@playcraft/cli 0.1.6 → 0.1.7
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.
|
@@ -2,6 +2,11 @@ import { connect as connectTcp } from 'node:net';
|
|
|
2
2
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
3
3
|
import { desktopHealthSchema, desktopResourceName, runtimeError, } from '@playcraft/common/cloud-workspace/runtime';
|
|
4
4
|
import { controlTokenMatches, extractControlToken } from './auth.js';
|
|
5
|
+
/**
|
|
6
|
+
* A takeover holds the desktop for this long, whether or not a page is watching, and ends on its
|
|
7
|
+
* own unless the user renews it.
|
|
8
|
+
*/
|
|
9
|
+
const HOLD_MS = 20 * 60_000;
|
|
5
10
|
const sources = new WeakMap();
|
|
6
11
|
const hosts = new WeakMap();
|
|
7
12
|
export function setDesktopSource(state, source) {
|
|
@@ -33,6 +38,9 @@ export class DesktopHost {
|
|
|
33
38
|
sockets = new WebSocketServer({ noServer: true });
|
|
34
39
|
source;
|
|
35
40
|
upstream = loopbackUpstream(process.env.PLAYCRAFT_DESKTOP_UPSTREAM);
|
|
41
|
+
holdMs = holdDuration(process.env.PLAYCRAFT_DESKTOP_HOLD_MS);
|
|
42
|
+
holdExpiresAt;
|
|
43
|
+
holdTimer;
|
|
36
44
|
constructor(state) {
|
|
37
45
|
this.state = state;
|
|
38
46
|
this.source = sources.get(state);
|
|
@@ -46,6 +54,9 @@ export class DesktopHost {
|
|
|
46
54
|
streamGeneration: this.generation,
|
|
47
55
|
holder: this.holder,
|
|
48
56
|
agentInput: this.holder === 'human' ? 'revoked' : 'open',
|
|
57
|
+
...(this.holder === 'human' && this.holdExpiresAt
|
|
58
|
+
? { holdExpiresAt: new Date(this.holdExpiresAt).toISOString() }
|
|
59
|
+
: {}),
|
|
49
60
|
});
|
|
50
61
|
}
|
|
51
62
|
async refresh() {
|
|
@@ -71,12 +82,21 @@ export class DesktopHost {
|
|
|
71
82
|
}
|
|
72
83
|
this.holder = 'human';
|
|
73
84
|
this.cutViewers();
|
|
85
|
+
this.hold();
|
|
86
|
+
writeJson(response, 200, this.snapshot());
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (request.method === 'POST' && url.pathname === '/runtime/desktop/renew') {
|
|
90
|
+
if (this.holder !== 'human') {
|
|
91
|
+
writeError(response, 'desktop_not_held');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.hold();
|
|
74
95
|
writeJson(response, 200, this.snapshot());
|
|
75
96
|
return;
|
|
76
97
|
}
|
|
77
98
|
if (request.method === 'DELETE' && url.pathname === '/runtime/desktop') {
|
|
78
|
-
this.
|
|
79
|
-
this.cutViewers();
|
|
99
|
+
this.release();
|
|
80
100
|
writeJson(response, 200, this.snapshot());
|
|
81
101
|
return;
|
|
82
102
|
}
|
|
@@ -114,9 +134,30 @@ export class DesktopHost {
|
|
|
114
134
|
this.sockets.handleUpgrade(request, socket, head, (ws) => this.attach(ws, generation));
|
|
115
135
|
}
|
|
116
136
|
dispose() {
|
|
137
|
+
this.release();
|
|
138
|
+
this.sockets.close();
|
|
139
|
+
}
|
|
140
|
+
release() {
|
|
141
|
+
this.clearHold();
|
|
117
142
|
this.holder = 'none';
|
|
118
143
|
this.cutViewers();
|
|
119
|
-
|
|
144
|
+
}
|
|
145
|
+
/** Starts or renews the hold; when it runs out the desktop is released as if by hand. */
|
|
146
|
+
hold() {
|
|
147
|
+
this.clearHold();
|
|
148
|
+
this.holdExpiresAt = Date.now() + this.holdMs;
|
|
149
|
+
this.holdTimer = setTimeout(() => {
|
|
150
|
+
this.holdTimer = undefined;
|
|
151
|
+
if (this.holder === 'human')
|
|
152
|
+
this.release();
|
|
153
|
+
}, this.holdMs);
|
|
154
|
+
this.holdTimer.unref?.();
|
|
155
|
+
}
|
|
156
|
+
clearHold() {
|
|
157
|
+
if (this.holdTimer)
|
|
158
|
+
clearTimeout(this.holdTimer);
|
|
159
|
+
this.holdTimer = undefined;
|
|
160
|
+
this.holdExpiresAt = undefined;
|
|
120
161
|
}
|
|
121
162
|
currentStatus() {
|
|
122
163
|
if (this.source)
|
|
@@ -248,6 +289,7 @@ export function desktopRoute(pathname) {
|
|
|
248
289
|
return (pathname === '/runtime/desktop/health' ||
|
|
249
290
|
pathname === '/runtime/desktop/connect' ||
|
|
250
291
|
pathname === '/runtime/desktop/takeover' ||
|
|
292
|
+
pathname === '/runtime/desktop/renew' ||
|
|
251
293
|
pathname === '/runtime/desktop' ||
|
|
252
294
|
pathname.startsWith('/runtime/desktop/resources/'));
|
|
253
295
|
}
|
|
@@ -268,6 +310,10 @@ function loopbackUpstream(value) {
|
|
|
268
310
|
return undefined;
|
|
269
311
|
return { port };
|
|
270
312
|
}
|
|
313
|
+
function holdDuration(value) {
|
|
314
|
+
const parsed = Number(value);
|
|
315
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : HOLD_MS;
|
|
316
|
+
}
|
|
271
317
|
function probePort(port) {
|
|
272
318
|
return new Promise((resolve) => {
|
|
273
319
|
const socket = connectTcp({ host: '127.0.0.1', port });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"@gltf-transform/core": "^4.3.0",
|
|
49
49
|
"@gltf-transform/extensions": "^4.3.0",
|
|
50
50
|
"@gltf-transform/functions": "^4.3.0",
|
|
51
|
-
"@playcraft/build": "^0.1.
|
|
52
|
-
"@playcraft/common": "^0.1.
|
|
51
|
+
"@playcraft/build": "^0.1.7",
|
|
52
|
+
"@playcraft/common": "^0.1.7",
|
|
53
53
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
54
54
|
"chokidar": "^4.0.3",
|
|
55
55
|
"commander": "^13.1.0",
|