termdock 1.4.155 → 1.4.156
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/dist/server/entry.js +16 -23
- package/dist/server/utils/runtimeClient.js +73 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
package/dist/server/entry.js
CHANGED
|
@@ -27,7 +27,7 @@ import { RuntimeMonitor } from './utils/runtimeMonitor.js';
|
|
|
27
27
|
import { startOnboardingServer, stopOnboardingServer, getOnboardingServerUrl } from './onboardingServer.js';
|
|
28
28
|
import { CertificateWatcher } from './certificateWatcher.js';
|
|
29
29
|
import { startTermdockLogMaintenance, writeDiffTraceLog, writeErrorLog, writeJsonLog, writeTextLog, } from './utils/serverLogger.js';
|
|
30
|
-
import { resolveRuntimeClientDist } from './utils/runtimeClient.js';
|
|
30
|
+
import { pinBundledRuntimeClientDist, resolveRuntimeClientDist } from './utils/runtimeClient.js';
|
|
31
31
|
import { getTermdockVersion, TERMDOCK_CAPABILITIES, TERMDOCK_PROTOCOL_VERSION, } from './utils/version.js';
|
|
32
32
|
import { PORT, DEFAULT_HOST } from './config.js';
|
|
33
33
|
const CLIENT_STATE_COOKIE = 'termdock-client';
|
|
@@ -448,35 +448,28 @@ export function createApp(options = {}) {
|
|
|
448
448
|
// 文件系统路由(继承 /api/terminal 上的 auth + CSRF 保护)
|
|
449
449
|
app.use('/api/terminal/fs', filesystemRoutes);
|
|
450
450
|
if (fs.existsSync(bundledClientIndexPath)) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
setStaticCacheHeaders({ url: relativePath, path: relativePath }, res);
|
|
464
|
-
},
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
return { clientPath, compression: cachedCompression, staticFiles: cachedStatic };
|
|
468
|
-
};
|
|
451
|
+
const selectedClientPath = resolveRuntimeClientDist(bundledClientDistPath);
|
|
452
|
+
const clientPath = selectedClientPath === bundledClientDistPath
|
|
453
|
+
? pinBundledRuntimeClientDist(bundledClientDistPath)
|
|
454
|
+
: selectedClientPath;
|
|
455
|
+
const compression = createStaticCompressionMiddleware(clientPath);
|
|
456
|
+
const staticFiles = express.static(clientPath, {
|
|
457
|
+
setHeaders: (res, filePath, stat) => {
|
|
458
|
+
void stat;
|
|
459
|
+
const relativePath = `/${path.relative(clientPath, filePath).split(path.sep).join('/')}`;
|
|
460
|
+
setStaticCacheHeaders({ url: relativePath, path: relativePath }, res);
|
|
461
|
+
},
|
|
462
|
+
});
|
|
469
463
|
app.use((req, res, next) => {
|
|
470
|
-
|
|
471
|
-
handlers.compression(req, res, (compressionError) => {
|
|
464
|
+
compression(req, res, (compressionError) => {
|
|
472
465
|
if (compressionError)
|
|
473
466
|
return next(compressionError);
|
|
474
|
-
|
|
467
|
+
staticFiles(req, res, next);
|
|
475
468
|
});
|
|
476
469
|
});
|
|
477
470
|
app.get(/^(?!\/api(?:\/|$)|\/health$|\/onboarding(?:\/|$)|\/ca(?:\/|$)).*/, (req, res) => {
|
|
478
471
|
setStaticCacheHeaders(req, res);
|
|
479
|
-
res.sendFile(path.join(
|
|
472
|
+
res.sendFile(path.join(clientPath, 'index.html'));
|
|
480
473
|
});
|
|
481
474
|
}
|
|
482
475
|
return app;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -9,6 +10,78 @@ function readManifest(filePath) {
|
|
|
9
10
|
return null;
|
|
10
11
|
}
|
|
11
12
|
}
|
|
13
|
+
function isCompleteSnapshot(clientDist, serverBundleHash) {
|
|
14
|
+
const marker = readManifest(path.join(clientDist, '..', '..', 'snapshot-manifest.json'));
|
|
15
|
+
return marker?.schemaVersion === 1
|
|
16
|
+
&& marker.serverBundleHash === serverBundleHash
|
|
17
|
+
&& marker.clientEntrypoint === 'dist/client/index.html'
|
|
18
|
+
&& fs.existsSync(path.join(clientDist, 'index.html'));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Keep the browser bundle served by this process immutable for its lifetime.
|
|
22
|
+
*
|
|
23
|
+
* `npm install --global` replaces files underneath a still-running Termdock
|
|
24
|
+
* process. Without a snapshot, Express immediately starts serving the new
|
|
25
|
+
* index/assets while the in-memory server is still the old version. That can
|
|
26
|
+
* pair a newer terminal protocol client with an older WebSocket server until
|
|
27
|
+
* the user confirms the restart.
|
|
28
|
+
*/
|
|
29
|
+
export function pinBundledRuntimeClientDist(defaultClientDist, homeDir = os.homedir()) {
|
|
30
|
+
try {
|
|
31
|
+
const packageRoot = path.resolve(defaultClientDist, '..', '..');
|
|
32
|
+
const manifest = readManifest(path.join(packageRoot, 'runtime-manifest.json'));
|
|
33
|
+
if (manifest?.schemaVersion !== 1
|
|
34
|
+
|| typeof manifest.serverBundleHash !== 'string'
|
|
35
|
+
|| manifest.clientEntrypoint !== 'dist/client/index.html'
|
|
36
|
+
|| !fs.existsSync(path.join(defaultClientDist, 'index.html'))) {
|
|
37
|
+
return defaultClientDist;
|
|
38
|
+
}
|
|
39
|
+
const snapshotKey = crypto
|
|
40
|
+
.createHash('sha256')
|
|
41
|
+
.update(manifest.serverBundleHash)
|
|
42
|
+
.digest('hex')
|
|
43
|
+
.slice(0, 24);
|
|
44
|
+
const snapshotsRoot = path.join(homeDir, '.termdock', 'client-snapshots');
|
|
45
|
+
const snapshotRoot = path.join(snapshotsRoot, snapshotKey);
|
|
46
|
+
const snapshotClientDist = path.join(snapshotRoot, 'dist', 'client');
|
|
47
|
+
if (isCompleteSnapshot(snapshotClientDist, manifest.serverBundleHash)) {
|
|
48
|
+
return snapshotClientDist;
|
|
49
|
+
}
|
|
50
|
+
fs.mkdirSync(snapshotsRoot, { recursive: true, mode: 0o700 });
|
|
51
|
+
const temporaryRoot = fs.mkdtempSync(path.join(snapshotsRoot, `.${snapshotKey}-`));
|
|
52
|
+
const temporaryClientDist = path.join(temporaryRoot, 'dist', 'client');
|
|
53
|
+
try {
|
|
54
|
+
fs.cpSync(defaultClientDist, temporaryClientDist, {
|
|
55
|
+
recursive: true,
|
|
56
|
+
preserveTimestamps: true,
|
|
57
|
+
});
|
|
58
|
+
fs.writeFileSync(path.join(temporaryRoot, 'snapshot-manifest.json'), `${JSON.stringify({
|
|
59
|
+
schemaVersion: 1,
|
|
60
|
+
serverBundleHash: manifest.serverBundleHash,
|
|
61
|
+
clientEntrypoint: 'dist/client/index.html',
|
|
62
|
+
})}\n`, { mode: 0o600 });
|
|
63
|
+
try {
|
|
64
|
+
fs.renameSync(temporaryRoot, snapshotRoot);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error.code !== 'EEXIST')
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (fs.existsSync(temporaryRoot)) {
|
|
73
|
+
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return isCompleteSnapshot(snapshotClientDist, manifest.serverBundleHash)
|
|
77
|
+
? snapshotClientDist
|
|
78
|
+
: defaultClientDist;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// A read-only home or interrupted snapshot must never block the server.
|
|
82
|
+
return defaultClientDist;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
12
85
|
export function resolveRuntimeClientDist(defaultClientDist, homeDir = os.homedir()) {
|
|
13
86
|
try {
|
|
14
87
|
const defaultPackageRoot = path.resolve(defaultClientDist, '..', '..');
|
package/package.json
CHANGED
package/runtime-manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "termdock",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.156",
|
|
5
5
|
"runtimeProtocolVersion": 1,
|
|
6
6
|
"minimumDesktopVersion": "1.4.46",
|
|
7
7
|
"nodeMajor": 22,
|
|
8
8
|
"dependencyHash": "sha256-HOcSzDZKWX251Njijj5fna89AzzVkOmrgAV2AuvVlJk=",
|
|
9
|
-
"serverBundleHash": "sha256-
|
|
9
|
+
"serverBundleHash": "sha256-03guIBU3l8lNaGGjmY7l/fSgflPMZ++rwcORglMctvo=",
|
|
10
10
|
"entrypoint": "dist/server/cli.js",
|
|
11
11
|
"clientEntrypoint": "dist/client/index.html"
|
|
12
12
|
}
|