@m2canvas/connect 0.6.0 → 0.7.1
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/README.md +17 -0
- package/dist/bridge/adapters.js +12 -6
- package/dist/bridge/handoff.js +197 -0
- package/dist/bridge/install-layout.js +37 -0
- package/dist/bridge/instance.js +7 -1
- package/dist/bridge/main.js +133 -14
- package/dist/bridge/npm-command.js +25 -0
- package/dist/bridge/pair-call.js +31 -0
- package/dist/bridge/pairing.js +17 -4
- package/dist/bridge/release-manifest.js +80 -0
- package/dist/bridge/self-test.js +30 -0
- package/dist/bridge/server.js +3 -12
- package/dist/bridge/update-io.js +118 -0
- package/dist/bridge/update.js +365 -0
- package/package.json +1 -1
package/dist/bridge/pairing.js
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
* 不要为了「方便」放宽次数、延长有效期或加一个页面能触发的解锁:码是唯一挡住本机恶意页面的东西。
|
|
19
19
|
* 也不要改成「在终端按回车解锁」:那需要读终端,而 bridge 放到后台(`… bridge &`)时读终端会被系统挂起(review r2 实测)。
|
|
20
20
|
* - 比较用常量时间(timingSafeEqual),不要改回 ===。
|
|
21
|
+
* - 后台模式(`issueCodes: false`,`bridge --background`,change acp-bridge-one-time-install D17)不出码:没有终端,stdout 写的是日志文件,
|
|
22
|
+
* 出码就等于把码写盘。这时 `issueCode` / `ensureCode` 什么都不做,`pair` 一律 pairing_code_unavailable,配对只剩命令承诺与已有 token。
|
|
23
|
+
* 不要给后台模式另开出码通道(文件、系统对话框):「把码写进文件」已被否决过(`connect/local-bridge` 契约 2 / 13)。
|
|
21
24
|
*/
|
|
22
25
|
import { createHash, randomBytes, randomInt, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
23
26
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
@@ -84,6 +87,7 @@ export class PairingManager {
|
|
|
84
87
|
lastPrompt = new Map();
|
|
85
88
|
store;
|
|
86
89
|
now;
|
|
90
|
+
issueCodes;
|
|
87
91
|
onCodeIssued;
|
|
88
92
|
onLocked;
|
|
89
93
|
onProblem;
|
|
@@ -91,13 +95,16 @@ export class PairingManager {
|
|
|
91
95
|
constructor(options) {
|
|
92
96
|
this.store = options.store;
|
|
93
97
|
this.now = options.now ?? Date.now;
|
|
98
|
+
this.issueCodes = options.issueCodes ?? true;
|
|
94
99
|
this.onCodeIssued = options.onCodeIssued;
|
|
95
100
|
this.onLocked = options.onLocked ?? (() => undefined);
|
|
96
101
|
this.onProblem = options.onProblem ?? (() => undefined);
|
|
97
102
|
this.onCommitmentReused = options.onCommitmentReused ?? (() => undefined);
|
|
98
103
|
}
|
|
99
|
-
/**
|
|
104
|
+
/** 生成新码(旧码立即失效)并通知调用方打印,返回显示形式;后台模式不出码,返回 null */
|
|
100
105
|
issueCode(reason, origin = null) {
|
|
106
|
+
if (!this.issueCodes)
|
|
107
|
+
return null;
|
|
101
108
|
let code = '';
|
|
102
109
|
for (let i = 0; i < CODE_LENGTH; i++)
|
|
103
110
|
code += ALPHABET[randomInt(ALPHABET.length)];
|
|
@@ -118,10 +125,10 @@ export class PairingManager {
|
|
|
118
125
|
* 未配对页面连上时调用。没有有效码就出新码;有有效码也把它连同发起方 Origin 再提示一次 ——
|
|
119
126
|
* 最常见的首次流程是「启动 bridge 后马上打开页面」,终端里只有一个不带 Origin 的启动码,
|
|
120
127
|
* 不再提示的话用户看不到是哪个页面在请求配对。同一 Origin、同一个码一分钟内只提示一次(页面重连不刷屏)。
|
|
121
|
-
*
|
|
128
|
+
* 锁定期间、后台模式下什么都不做。
|
|
122
129
|
*/
|
|
123
130
|
ensureCode(origin) {
|
|
124
|
-
if (this.locked)
|
|
131
|
+
if (this.locked || !this.issueCodes)
|
|
125
132
|
return;
|
|
126
133
|
if (!this.hasValidCode() || !this.active) {
|
|
127
134
|
this.issueCode('requested', origin);
|
|
@@ -141,6 +148,9 @@ export class PairingManager {
|
|
|
141
148
|
* (review r1 实测 1.6 秒 20000 次),还会让陌生页面把用户的配对码一直刷掉。
|
|
142
149
|
*/
|
|
143
150
|
pair(input, origin) {
|
|
151
|
+
// 后台模式没有码可比:在锁定检查之前返回,也不计入错码次数(页面并没有猜码,只是不知道这个 bridge 不出码)
|
|
152
|
+
if (!this.issueCodes)
|
|
153
|
+
return { ok: false, code: 'pairing_code_unavailable' };
|
|
144
154
|
if (this.locked)
|
|
145
155
|
return { ok: false, code: 'pairing_locked' };
|
|
146
156
|
if (!this.active) {
|
|
@@ -238,7 +248,10 @@ export class PairingManager {
|
|
|
238
248
|
this.store.save(records.slice(0, MAX_PAIRINGS));
|
|
239
249
|
return token;
|
|
240
250
|
}
|
|
241
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* 读出未过期的记录;顺带把过期的从存储里删掉(删不掉只告警:不影响本次判定)。
|
|
253
|
+
* 每次都从存储读(不缓存):交接 socket 的门槛论证依赖这一点(`handoff.ts` 文件头),改成缓存时要一起重新评估。
|
|
254
|
+
*/
|
|
242
255
|
liveRecords() {
|
|
243
256
|
const all = this.store.load();
|
|
244
257
|
const live = all.filter((record) => this.now() - Date.parse(record.lastUsedAt) <= TOKEN_IDLE_MS);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 更新清单 `https://m2ui.ai/connect/manifest.json` 的格式、解析与下载地址(change acp-bridge-one-time-install D13 / D14 / R7)。
|
|
3
|
+
*
|
|
4
|
+
* 清单由仓库的 `packages/connect/install/release.json` 生成,随 App 部署发布;部署 App 才是放量开关,发 npm 不等于推给所有用户。
|
|
5
|
+
* 清单**只提供版本号与哈希**:下载地址在这里由版本号拼出,固定指向官方源与 npmmirror。
|
|
6
|
+
* 不要在清单里加 URL 字段 —— 那会让清单(或篡改清单的人)把下载指到任意地方,信任根就不止 m2ui.ai 加哈希了。
|
|
7
|
+
*
|
|
8
|
+
* 跨版本兼容:已安装的旧版本 bridge 会读新部署的清单(`--check-update` 跑的是当前版本的代码)。
|
|
9
|
+
* - 同一 `schema` 内只加字段;本解析器忽略不认识的字段。
|
|
10
|
+
* - 要改语义就升 `schema`;旧版本认不出新 schema 时按「读清单失败」处理,用当前版本继续(不会误装)。
|
|
11
|
+
* - 版本号与哈希一律严格校验格式:它们会进入文件路径与下载地址。
|
|
12
|
+
*/
|
|
13
|
+
export const MANIFEST_URL = 'https://m2ui.ai/connect/manifest.json';
|
|
14
|
+
export const MANIFEST_SCHEMA = 1;
|
|
15
|
+
/** 只认纯 x.y.z(不接受预发布、构建元数据):版本号会拼进目录名与下载地址 */
|
|
16
|
+
const VERSION_RE = /^\d{1,4}\.\d{1,4}\.\d{1,6}$/;
|
|
17
|
+
const SHA512_RE = /^[0-9a-f]{128}$/;
|
|
18
|
+
const SHA256_RE = /^[0-9a-f]{64}$/;
|
|
19
|
+
export function isReleaseVersion(value) {
|
|
20
|
+
return typeof value === 'string' && VERSION_RE.test(value);
|
|
21
|
+
}
|
|
22
|
+
function record(value) {
|
|
23
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : null;
|
|
24
|
+
}
|
|
25
|
+
function pinned(value, hash) {
|
|
26
|
+
const entry = record(value);
|
|
27
|
+
if (!entry || !isReleaseVersion(entry.version) || typeof entry.sha512 !== 'string' || !hash.test(entry.sha512))
|
|
28
|
+
return null;
|
|
29
|
+
return { version: entry.version, sha512: entry.sha512 };
|
|
30
|
+
}
|
|
31
|
+
/** 解析清单正文;格式不对时返回原因(英文,写进 update.log) */
|
|
32
|
+
export function parseReleaseManifest(text) {
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(text);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return 'manifest is not valid JSON';
|
|
39
|
+
}
|
|
40
|
+
const root = record(parsed);
|
|
41
|
+
if (!root)
|
|
42
|
+
return 'manifest is not a JSON object';
|
|
43
|
+
if (root.schema !== MANIFEST_SCHEMA)
|
|
44
|
+
return `unsupported manifest schema ${JSON.stringify(root.schema)}`;
|
|
45
|
+
const connect = pinned(root.connect, SHA512_RE);
|
|
46
|
+
if (!connect)
|
|
47
|
+
return 'manifest has no valid connect version / sha512';
|
|
48
|
+
const ws = pinned(root.ws, SHA512_RE);
|
|
49
|
+
if (!ws)
|
|
50
|
+
return 'manifest has no valid ws version / sha512';
|
|
51
|
+
const node = record(root.node);
|
|
52
|
+
const sha256 = record(node?.sha256);
|
|
53
|
+
const arm64 = sha256?.arm64;
|
|
54
|
+
const x64 = sha256?.x64;
|
|
55
|
+
if (!node || !isReleaseVersion(node.version) || typeof arm64 !== 'string' || !SHA256_RE.test(arm64) || typeof x64 !== 'string' || !SHA256_RE.test(x64)) {
|
|
56
|
+
return 'manifest has no valid node version / sha256';
|
|
57
|
+
}
|
|
58
|
+
return { schema: MANIFEST_SCHEMA, connect, ws, node: { version: node.version, sha256: { arm64, x64 } } };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 下载源:官方优先,失败(含校验不符)再退 npmmirror(D14)。顺序有意义,不要打乱。
|
|
62
|
+
* 安装脚本的生成块用的是同一组基址(src/install/assets.ts),两边只此一处。
|
|
63
|
+
*/
|
|
64
|
+
export const NPM_REGISTRY_BASES = ['https://registry.npmjs.org', 'https://registry.npmmirror.com'];
|
|
65
|
+
export const NODE_DIST_BASES = ['https://nodejs.org/dist', 'https://npmmirror.com/mirrors/node'];
|
|
66
|
+
export function connectTarballUrls(version) {
|
|
67
|
+
return NPM_REGISTRY_BASES.map((base) => `${base}/@m2canvas/connect/-/connect-${version}.tgz`);
|
|
68
|
+
}
|
|
69
|
+
export function wsTarballUrls(version) {
|
|
70
|
+
return NPM_REGISTRY_BASES.map((base) => `${base}/ws/-/ws-${version}.tgz`);
|
|
71
|
+
}
|
|
72
|
+
/** 私有 Node 的目录名,也是官方包解开后的顶层目录名 */
|
|
73
|
+
export function nodeDirName(version, arch) {
|
|
74
|
+
return `node-v${version}-darwin-${arch}`;
|
|
75
|
+
}
|
|
76
|
+
/** `.tar.xz`:只有 `.tar.gz` 的一半大小,macOS 自带的 bsdtar 能解(R9) */
|
|
77
|
+
export function nodeTarballUrls(version, arch) {
|
|
78
|
+
const file = `${nodeDirName(version, arch)}.tar.xz`;
|
|
79
|
+
return NODE_DIST_BASES.map((base) => `${base}/v${version}/${file}`);
|
|
80
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bridge --self-test`:自动更新时对新版本的冒烟测试(change acp-bridge-one-time-install D20 ④、N4)。
|
|
3
|
+
*
|
|
4
|
+
* 当前版本的 `--check-update` 用新版本自己的 Node 运行新版本的 cli.js。能走到这里,说明 cli.js → bridge/main.js
|
|
5
|
+
* 及其全部静态依赖(含 ws)都加载成功;这里再核对几种「能加载、但装得不全」的情况,任何一项不满足就退出 1,
|
|
6
|
+
* 更新流程就不会切换到这个版本。
|
|
7
|
+
*
|
|
8
|
+
* 不监听端口、不写文件、不联网:它在旧版本 bridge 还在运行时执行。
|
|
9
|
+
* 检测不到「起来了但功能坏了」(例如协议处理有 bug):那种情况靠改清单回滚(`connect/local-bridge` 契约 16 delta)。
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { ADAPTERS } from './adapters.js';
|
|
14
|
+
import { bundledNpmCli } from './npm-command.js';
|
|
15
|
+
/** 返回发现的问题(英文,写进 update.log);空数组 = 通过 */
|
|
16
|
+
export function selfTestProblems(input) {
|
|
17
|
+
const exists = input.exists ?? existsSync;
|
|
18
|
+
const problems = [];
|
|
19
|
+
if (!/^\d+\.\d+\.\d+/.test(input.version) || input.version === '0.0.0')
|
|
20
|
+
problems.push(`unreadable package version (${input.version})`);
|
|
21
|
+
for (const spec of Object.values(ADAPTERS)) {
|
|
22
|
+
const lock = join(input.packageRoot, 'adapters', spec.dir, 'package-lock.json');
|
|
23
|
+
if (!exists(lock))
|
|
24
|
+
problems.push(`missing ${lock}`);
|
|
25
|
+
}
|
|
26
|
+
const npm = bundledNpmCli(input.execPath);
|
|
27
|
+
if (!exists(npm))
|
|
28
|
+
problems.push(`missing ${npm}`);
|
|
29
|
+
return problems;
|
|
30
|
+
}
|
package/dist/bridge/server.js
CHANGED
|
@@ -22,6 +22,7 @@ import { TOKEN_IDLE_DAYS } from './pairing.js';
|
|
|
22
22
|
import { ProcessError, ProcessManager } from './processes.js';
|
|
23
23
|
import { isBridgeAgentId } from './agents.js';
|
|
24
24
|
import { handleAuthCall } from './auth-call.js';
|
|
25
|
+
import { handlePairCall } from './pair-call.js';
|
|
25
26
|
import { AuthorizationManager } from './authorize.js';
|
|
26
27
|
import { BRIDGE_CLOSE_CODES, BRIDGE_METHODS, BRIDGE_SUPPORTED_PROTOCOLS, } from './protocol.js';
|
|
27
28
|
import { checkPageAcpLine } from './acp-guard.js';
|
|
@@ -105,20 +106,10 @@ export function startBridgeServer(options) {
|
|
|
105
106
|
};
|
|
106
107
|
async function handleCall(conn, id, method, params) {
|
|
107
108
|
if (method === 'pair') {
|
|
108
|
-
const
|
|
109
|
-
if (typeof code !== 'string')
|
|
110
|
-
throw new BridgeFailure('bad_message', 'pair needs a string "code"');
|
|
111
|
-
const result = options.pairing.pair(code, conn.origin);
|
|
112
|
-
if (!result.ok) {
|
|
113
|
-
if (result.code === 'pairing_invalid')
|
|
114
|
-
throw new BridgeFailure('pairing_invalid', 'wrong pairing code', { attemptsLeft: result.attemptsLeft });
|
|
115
|
-
if (result.code === 'pairing_locked')
|
|
116
|
-
throw new BridgeFailure('pairing_locked', 'too many wrong pairing codes; new pages cannot pair until the bridge is restarted');
|
|
117
|
-
throw new BridgeFailure('pairing_code_rotated', 'pairing code expired or used up; a new one is shown in the terminal');
|
|
118
|
-
}
|
|
109
|
+
const token = handlePairCall(options.pairing, conn.origin, params);
|
|
119
110
|
markPaired(conn);
|
|
120
111
|
log(`Paired with ${conn.origin}.`);
|
|
121
|
-
return { token
|
|
112
|
+
return { token, cwd: options.cwd, idleExpiryDays: TOKEN_IDLE_DAYS };
|
|
122
113
|
}
|
|
123
114
|
if (!conn.paired)
|
|
124
115
|
throw new BridgeFailure('not_paired', 'pair this page first');
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bridge --check-update` 的真实 IO 与命令入口(change acp-bridge-one-time-install D20、R7)。更新的判断都在 update.ts,这里只做外部调用:
|
|
3
|
+
* - 读清单、下载:一律 `/usr/bin/curl`。安装脚本也用 curl,两边代理行为相同 —— curl 读 HTTPS_PROXY / ALL_PROXY / NO_PROXY 与
|
|
4
|
+
* `~/.curlrc`,Node 的 fetch 默认不读代理变量。`-f`:HTTP 错误码当失败,不把错误页写成「下载好的文件」。
|
|
5
|
+
* - 解压:`/usr/bin/tar`(macOS 的 bsdtar 自带 xz;内容已按清单哈希校验过)。
|
|
6
|
+
* - 冒烟:用新版本自己的 Node 跑新版本的 `cli.js bridge --self-test`。
|
|
7
|
+
* 子进程环境由调用方给:生产用 env.ts 的白名单(代理与证书变量都在里面),不把宿主注入的变量带给 curl。
|
|
8
|
+
*
|
|
9
|
+
* 不提供清单地址、下载地址的覆盖参数或环境变量(design R8):测试经 UpdateDeps 注入,或直接对本模块的原语传本机地址。
|
|
10
|
+
*/
|
|
11
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
12
|
+
import { home } from '../system.js';
|
|
13
|
+
import { buildAgentEnv } from './env.js';
|
|
14
|
+
import { combinedOutput, execCommand } from './exec.js';
|
|
15
|
+
import { installLayout } from './install-layout.js';
|
|
16
|
+
import { tail } from './precheck-parse.js';
|
|
17
|
+
import { checkForUpdate } from './update.js';
|
|
18
|
+
const CURL = '/usr/bin/curl';
|
|
19
|
+
const TAR = '/usr/bin/tar';
|
|
20
|
+
/** 单个文件的下载上限:Node 包约 27MB,慢网(约 100KB/s)也要下得完 */
|
|
21
|
+
const DOWNLOAD_TIMEOUT_MS = 10 * 60 * 1000;
|
|
22
|
+
const EXTRACT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
23
|
+
/** 冒烟时限(D20 ④) */
|
|
24
|
+
const SELF_TEST_TIMEOUT_MS = 20_000;
|
|
25
|
+
/** update.log 超过这个大小先轮转成 `.1`(每次启动只写几行,很少触发) */
|
|
26
|
+
const UPDATE_LOG_MAX_BYTES = 1024 * 1024;
|
|
27
|
+
function failure(what, result) {
|
|
28
|
+
const reason = result.timedOut ? 'timed out' : result.code === null ? (result.error ?? 'did not start') : `exited ${result.code}`;
|
|
29
|
+
const output = tail(combinedOutput(result).trim());
|
|
30
|
+
return new Error(`${what} ${reason}${output ? `: ${output}` : ''}`);
|
|
31
|
+
}
|
|
32
|
+
export function createUpdateIO(options) {
|
|
33
|
+
const { exec, env } = options;
|
|
34
|
+
return {
|
|
35
|
+
async fetchText(url, timeoutMs) {
|
|
36
|
+
const seconds = String(Math.max(1, Math.ceil(timeoutMs / 1000)));
|
|
37
|
+
const result = await exec(CURL, ['-fsSL', '--connect-timeout', '5', '--max-time', seconds, url], { env, timeoutMs: timeoutMs + 2_000, maxOutput: 256 * 1024 });
|
|
38
|
+
if (result.code !== 0)
|
|
39
|
+
throw failure('curl', result);
|
|
40
|
+
return result.stdout;
|
|
41
|
+
},
|
|
42
|
+
async download(url, dest) {
|
|
43
|
+
const seconds = String(DOWNLOAD_TIMEOUT_MS / 1000);
|
|
44
|
+
// 低于 10KB/s 持续 30 秒就放弃、换下一个源:官方源只有涓流速度时不能拖到总时限(review r1 R1-S4,与安装脚本相同)
|
|
45
|
+
const result = await exec(CURL, ['-fsSL', '--connect-timeout', '10', '--speed-limit', '10240', '--speed-time', '30', '--max-time', seconds, '-o', dest, url], {
|
|
46
|
+
env,
|
|
47
|
+
timeoutMs: DOWNLOAD_TIMEOUT_MS + 5_000,
|
|
48
|
+
maxOutput: 16 * 1024,
|
|
49
|
+
});
|
|
50
|
+
if (result.code !== 0) {
|
|
51
|
+
// curl 失败时可能留下半截或空文件:删掉,免得被当成下载好的内容
|
|
52
|
+
rmSync(dest, { force: true });
|
|
53
|
+
throw failure('curl', result);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
async extract(archive, dir) {
|
|
57
|
+
const result = await exec(TAR, ['-xf', archive, '-C', dir], { env, timeoutMs: EXTRACT_TIMEOUT_MS, maxOutput: 16 * 1024 });
|
|
58
|
+
if (result.code !== 0)
|
|
59
|
+
throw failure('tar', result);
|
|
60
|
+
},
|
|
61
|
+
async selfTest(nodePath, cliPath) {
|
|
62
|
+
const result = await exec(nodePath, [cliPath, 'bridge', '--self-test'], { env, timeoutMs: SELF_TEST_TIMEOUT_MS, maxOutput: 16 * 1024 });
|
|
63
|
+
return { ok: result.code === 0, output: result.timedOut ? `self-test timed out after ${SELF_TEST_TIMEOUT_MS / 1000}s` : combinedOutput(result) };
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** 追加一行到 `logs/update.log`(目录 0700、文件 0600,与 bridge.log 相同);超过上限先轮转 */
|
|
68
|
+
export function appendUpdateLog(layout, line, maxBytes = UPDATE_LOG_MAX_BYTES) {
|
|
69
|
+
mkdirSync(layout.logs, { recursive: true, mode: 0o700 });
|
|
70
|
+
chmodSync(layout.logs, 0o700);
|
|
71
|
+
if (existsSync(layout.updateLog) && statSync(layout.updateLog).size > maxBytes)
|
|
72
|
+
renameSync(layout.updateLog, `${layout.updateLog}.1`);
|
|
73
|
+
appendFileSync(layout.updateLog, `${new Date().toISOString()} ${line}\n`, { mode: 0o600 });
|
|
74
|
+
chmodSync(layout.updateLog, 0o600);
|
|
75
|
+
}
|
|
76
|
+
function describeOutcome(outcome) {
|
|
77
|
+
switch (outcome.kind) {
|
|
78
|
+
case 'unsupported':
|
|
79
|
+
return 'skipped (only macOS is supported)';
|
|
80
|
+
case 'not_installed':
|
|
81
|
+
return 'skipped (not installed by the M2 installer)';
|
|
82
|
+
case 'manifest_failed':
|
|
83
|
+
return `kept the current version (couldn't read the manifest: ${outcome.reason})`;
|
|
84
|
+
case 'up_to_date':
|
|
85
|
+
return `up to date (${outcome.version})`;
|
|
86
|
+
case 'rejected':
|
|
87
|
+
return `kept the current version (${outcome.version} failed to start before)`;
|
|
88
|
+
case 'failed':
|
|
89
|
+
return `kept the current version (update to ${outcome.version} failed: ${outcome.reason})`;
|
|
90
|
+
case 'updated':
|
|
91
|
+
return `updated ${outcome.from} -> ${outcome.to}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 命令入口(由启动器调用)。日志写进 update.log;标准输出只打一行摘要(启动器把它追加进 bridge.log)。
|
|
96
|
+
* 退出码只供排障:启动器不论成败都用 current 继续,是否刚切换过版本由它自己比较 current 的前后指向得出。
|
|
97
|
+
*/
|
|
98
|
+
export async function runCheckUpdate() {
|
|
99
|
+
const layout = installLayout(home);
|
|
100
|
+
const io = createUpdateIO({ exec: execCommand, env: buildAgentEnv() });
|
|
101
|
+
const outcome = await checkForUpdate({
|
|
102
|
+
layout,
|
|
103
|
+
platform: process.platform,
|
|
104
|
+
arch: process.arch,
|
|
105
|
+
...io,
|
|
106
|
+
log: (line) => {
|
|
107
|
+
try {
|
|
108
|
+
appendUpdateLog(layout, line);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// 日志写不进(磁盘满、权限)不能拦住更新判断;摘要仍会打到标准输出
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
now: Date.now,
|
|
115
|
+
});
|
|
116
|
+
console.log(`M2 bridge update: ${describeOutcome(outcome)}`);
|
|
117
|
+
return outcome.kind === 'failed' || outcome.kind === 'manifest_failed' ? 1 : 0;
|
|
118
|
+
}
|