dsh-pocket 1.0.12 → 1.0.13
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.en.md +1 -0
- package/README.md +1 -0
- package/client/client.js +15 -1
- package/client/index.jsx +12 -1
- package/lib/index.js +27 -2
- package/lib/restart.js +53 -22
- package/lib/web-rpc.js +12 -5
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -101,6 +101,7 @@ DSH's `/api` browser-trust fence only accepts loopback or `--trusted-host` autho
|
|
|
101
101
|
| Version stuck below 1.x | `^0.x` ranges never jump to 1.x: update with `--latest` (`dsh plugin --profile web update dsh-pocket --latest -w`) |
|
|
102
102
|
| No push on iOS Safari | Safari Web Push requires **"Add to Home Screen"** first, then open from the home-screen icon (Chrome/Android don't need this) |
|
|
103
103
|
| Public `error 1033` | See "Public tunnel troubleshooting" below — usually a local proxy/VPN (Clash etc. TUN mode) killing the tunnel |
|
|
104
|
+
| After "Restart dsh web", the page says the process is running in the background | The new process from in-page self-restart is a detached background process (not attached to your terminal) — that's the standard way to apply updates in-page; stop it with `lsof -ti :3080 \| xargs kill -9` (logs under `$DSH_HOME` as `dsh-pocket-restart-*.log`) |
|
|
104
105
|
|
|
105
106
|
## ⚠️ Public tunnel troubleshooting (read first)
|
|
106
107
|
|
package/README.md
CHANGED
|
@@ -101,6 +101,7 @@ DSH 的 `/api` 浏览器信任栅栏只认 loopback 或 `--trusted-host`(官
|
|
|
101
101
|
| 版本停在 0.x 升不上去 | `^0.x` 范围不允许升到 1.x:更新用 `--latest`(`dsh plugin --profile web update dsh-pocket --latest -w`) |
|
|
102
102
|
| 手机 iOS 收不到推送 | Safari 的 Web Push 要求**先把网页「添加到主屏幕」**,从主屏幕图标打开后才生效(Chrome/Android 无此要求) |
|
|
103
103
|
| 公网 `error 1033` | 见下方「公网隧道常见问题」——多半是本机代理/VPN(Clash 等 TUN 模式)掐断了隧道 |
|
|
104
|
+
| 点「重启 dsh web」后页面提示进程在后台运行 | 自重启的新进程是 detached 后台进程(不挂终端),是页内更新的标准做法;停止它:`lsof -ti :3080 \| xargs kill -9`(日志在 `$DSH_HOME` 下 `dsh-pocket-restart-*.log`) |
|
|
104
105
|
|
|
105
106
|
## ⚠️ 公网隧道常见问题(必读)
|
|
106
107
|
|
package/client/client.js
CHANGED
|
@@ -1306,6 +1306,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1306
1306
|
const [pushEnabled, setPushEnabled] = (0, import_react2.useState)(true);
|
|
1307
1307
|
const [pushState, setPushState] = (0, import_react2.useState)("checking");
|
|
1308
1308
|
const [tunnelState, setTunnelState] = (0, import_react2.useState)(null);
|
|
1309
|
+
const [restartNotice, setRestartNotice] = (0, import_react2.useState)(false);
|
|
1309
1310
|
const [updateInfo, setUpdateInfo] = (0, import_react2.useState)(null);
|
|
1310
1311
|
const call = async (endpoint, payload) => {
|
|
1311
1312
|
const res = await rpcCall(endpoint, payload);
|
|
@@ -1317,6 +1318,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1317
1318
|
const s = await call(POCKET_ENDPOINTS.status, {});
|
|
1318
1319
|
setStatus(s);
|
|
1319
1320
|
setTunnelState(s.tunnelState ?? null);
|
|
1321
|
+
if (s.restartNotice) setRestartNotice(true);
|
|
1320
1322
|
} catch {
|
|
1321
1323
|
}
|
|
1322
1324
|
};
|
|
@@ -1428,10 +1430,22 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1428
1430
|
"\u5F00\u53D1\u8005\uFF1A\u7A0B\u5E8F\u5458\u5C11\u5317\u6668"
|
|
1429
1431
|
)
|
|
1430
1432
|
),
|
|
1433
|
+
// 重启后提示(进程在后台运行,停止方法)
|
|
1434
|
+
restartNotice ? (0, import_react2.createElement)(
|
|
1435
|
+
"div",
|
|
1436
|
+
{ style: { ...styles.block, border: "1px solid var(--dsw-alias-brand-primary,#4f6ef7)", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" } },
|
|
1437
|
+
(0, import_react2.createElement)(
|
|
1438
|
+
"div",
|
|
1439
|
+
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
|
|
1440
|
+
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F504} \u5DF2\u91CD\u542F | Restarted"),
|
|
1441
|
+
(0, import_react2.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, "\u77E5\u9053\u4E86 | OK")
|
|
1442
|
+
),
|
|
1443
|
+
(0, import_react2.createElement)("div", { style: styles.muted, marginTop: 4, wordBreak: "break-all" }, "\u8FDB\u7A0B\u5728\u540E\u53F0\u8FD0\u884C\uFF08\u4E0D\u6302\u7EC8\u7AEF\uFF09\u3002\u5982\u9700\u505C\u6B62\uFF1Alsof -ti :3080 | xargs kill -9")
|
|
1444
|
+
) : null,
|
|
1431
1445
|
// 更新提示
|
|
1432
1446
|
updateInfo ? (0, import_react2.createElement)(
|
|
1433
1447
|
"div",
|
|
1434
|
-
{ style: { ...styles.block, border: "1px solid var(--dsw-alias-state-warn-primary,#b45309)", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" }
|
|
1448
|
+
{ style: { ...styles.block, border: "1px solid var(--dsw-alias-state-warn-primary,#b45309)" }, borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" },
|
|
1435
1449
|
(0, import_react2.createElement)(
|
|
1436
1450
|
"div",
|
|
1437
1451
|
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
|
package/client/index.jsx
CHANGED
|
@@ -61,6 +61,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
61
61
|
const [pushEnabled, setPushEnabled] = useState(true); // 宿主开关
|
|
62
62
|
const [pushState, setPushState] = useState('checking'); // checking|on|unsupported|insecure|off
|
|
63
63
|
const [tunnelState, setTunnelState] = useState(null); // 隧道进度 {phase, detail, startedAt}
|
|
64
|
+
const [restartNotice, setRestartNotice] = useState(false); // 重启后提示
|
|
64
65
|
const [updateInfo, setUpdateInfo] = useState(null); // { current, latest, updating, result } | null
|
|
65
66
|
|
|
66
67
|
const call = async (endpoint, payload) => {
|
|
@@ -74,6 +75,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
74
75
|
const s = await call(POCKET_ENDPOINTS.status, {});
|
|
75
76
|
setStatus(s);
|
|
76
77
|
setTunnelState(s.tunnelState ?? null);
|
|
78
|
+
if (s.restartNotice) setRestartNotice(true);
|
|
77
79
|
} catch { /* 忽略瞬时失败 */ }
|
|
78
80
|
};
|
|
79
81
|
|
|
@@ -186,8 +188,17 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
186
188
|
'开发者:程序员少北晨'),
|
|
187
189
|
),
|
|
188
190
|
|
|
191
|
+
// 重启后提示(进程在后台运行,停止方法)
|
|
192
|
+
restartNotice ? h('div', { style: { ...styles.block, border: '1px solid var(--dsw-alias-brand-primary,#4f6ef7)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '10px 12px' } },
|
|
193
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
|
|
194
|
+
h('div', { style: { fontWeight: 600, fontSize: 13 } }, '🔄 已重启 | Restarted'),
|
|
195
|
+
h('button', { style: styles.btn, onClick: () => setRestartNotice(false) }, '知道了 | OK'),
|
|
196
|
+
),
|
|
197
|
+
h('div', { style: styles.muted, marginTop: 4, wordBreak: 'break-all' }, '进程在后台运行(不挂终端)。如需停止:lsof -ti :3080 | xargs kill -9'),
|
|
198
|
+
) : null,
|
|
199
|
+
|
|
189
200
|
// 更新提示
|
|
190
|
-
updateInfo ? h('div', { style: { ...styles.block, border: '1px solid var(--dsw-alias-state-warn-primary,#b45309)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '10px 12px' }
|
|
201
|
+
updateInfo ? h('div', { style: { ...styles.block, border: '1px solid var(--dsw-alias-state-warn-primary,#b45309)' }, borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '10px 12px' },
|
|
191
202
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
|
|
192
203
|
h('div', { style: { fontWeight: 600, fontSize: 13 } }, `📦 新版本 v${updateInfo.latest} | Update available`),
|
|
193
204
|
updateInfo.result !== 'ok'
|
package/lib/index.js
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
// - 更新提示:有新版本时显示一键更新按钮(dsh plugin update --latest)
|
|
8
8
|
// 手机看到的界面 = 电脑上的 dsh web,实时同步(WebSocket 透传)。
|
|
9
9
|
|
|
10
|
-
import { readFile } from 'node:fs/promises';
|
|
11
10
|
import { fileURLToPath } from 'node:url';
|
|
12
11
|
import { spawn } from 'node:child_process';
|
|
13
12
|
import { createRequire } from 'node:module';
|
|
13
|
+
import { writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
14
16
|
|
|
15
17
|
import { createPocketService } from './service.mjs';
|
|
16
18
|
import { installPocketRpc } from './web-rpc.js';
|
|
@@ -30,6 +32,28 @@ function currentVersion() {
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
const restartNoticeRel = join('dsh-pocket', 'restarted.json');
|
|
36
|
+
function restartNoticePath() {
|
|
37
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), restartNoticeRel);
|
|
38
|
+
}
|
|
39
|
+
function readRestartNotice() {
|
|
40
|
+
try {
|
|
41
|
+
const raw = JSON.parse(readFile(restartNoticePath(), 'utf8'));
|
|
42
|
+
if (!raw?.at) return null;
|
|
43
|
+
if (Date.now() - raw.at > 30 * 60 * 1000) return null; // 30 分钟后过期
|
|
44
|
+
return raw;
|
|
45
|
+
} catch { return null; }
|
|
46
|
+
}
|
|
47
|
+
function writeRestartNotice() {
|
|
48
|
+
return mkdir(dirname(restartNoticePath()), { recursive: true })
|
|
49
|
+
.then(() => writeFile(restartNoticePath(), JSON.stringify({ at: Date.now(), pid: process.pid }), 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
/** 自重启(先落 notice,让重启后的页面提示停止方法)。 */
|
|
52
|
+
function pocketRestart() {
|
|
53
|
+
writeRestartNotice().catch(() => {});
|
|
54
|
+
return restartHost({ internals });
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
/** 执行更新:dsh plugin --profile <p> update dsh-pocket --latest -w(超时保护)。 */
|
|
34
58
|
function performUpdate(profile, { timeoutMs = 180_000 } = {}) {
|
|
35
59
|
return new Promise((resolve) => {
|
|
@@ -84,7 +108,8 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
84
108
|
service,
|
|
85
109
|
push: internals.push ?? { vapidPublicKey: () => '', count: () => 0, subscribe: async () => false, unsubscribe: async () => false, isEnabled: () => true, setEnabled: async () => true },
|
|
86
110
|
runUpdate: internals.runUpdate ?? { currentVersion, perform: performUpdate },
|
|
87
|
-
restart: internals.restart ??
|
|
111
|
+
restart: internals.restart ?? pocketRestart,
|
|
112
|
+
restartNotice: internals.restartNotice ?? readRestartNotice,
|
|
88
113
|
log: logger,
|
|
89
114
|
});
|
|
90
115
|
disposers.push(disposeRpc);
|
package/lib/restart.js
CHANGED
|
@@ -1,45 +1,76 @@
|
|
|
1
|
-
// dsh-pocket 自重启:重新拉起启动本宿主的确切 dsh
|
|
2
|
-
//
|
|
3
|
-
// self-restart 方案,见 https://github.com/dsh-market/dsh-market lib/restart.js)。
|
|
1
|
+
// dsh-pocket 自重启:重新拉起启动本宿主的确切 dsh 调用(detached 交接),
|
|
2
|
+
// 让更新后的插件代码生效——用户无需离开界面手动重启。
|
|
4
3
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// 方案借鉴 dshmarket 的 self-restart(lib/restart.js,MIT):不直接拉起新
|
|
5
|
+
// 进程,而是先拉一个 detached 的 node 辅助进程,等旧进程退出、端口释放
|
|
6
|
+
// (1.5s)后再拉起新 dsh,并把新进程输出写入临时日志——避免端口竞争
|
|
7
|
+
// (EADDRINUSE)导致新进程静默崩溃。
|
|
8
|
+
//
|
|
9
|
+
// 注意:新进程 detached,不挂终端——停止方式:lsof -ti :3080 | xargs kill -9。
|
|
8
10
|
|
|
9
11
|
import { spawn } from 'node:child_process';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
10
14
|
|
|
11
|
-
/**
|
|
12
|
-
* 重建启动调用(与当前宿主相同的命令)。
|
|
13
|
-
* @returns {{file:string, args:string[]}}
|
|
14
|
-
*/
|
|
15
|
+
/** 重建启动调用(与当前宿主相同的命令)。 */
|
|
15
16
|
export function restartLaunch() {
|
|
16
17
|
return {
|
|
17
18
|
file: process.argv[0], // node
|
|
18
19
|
args: [process.argv[1], ...process.argv.slice(2)], // <bin.js> + web [flags]
|
|
20
|
+
cwd: process.cwd(),
|
|
19
21
|
};
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/** 辅助进程代码:等待 1.5s(旧进程释放端口)→ 拉起新 dsh → 输出写日志。 */
|
|
25
|
+
function helperCode(launch, logOut, logErr) {
|
|
26
|
+
const spawn = "const { spawn } = require('node:child_process')";
|
|
27
|
+
const fs = "const fs = require('node:fs')";
|
|
28
|
+
const file = `const file = ${JSON.stringify(launch.file)}`;
|
|
29
|
+
const args = `const args = ${JSON.stringify(launch.args)}`;
|
|
30
|
+
const cwd = `const cwd = ${JSON.stringify(launch.cwd)}`;
|
|
31
|
+
const o = `const logOut = ${JSON.stringify(logOut)}`;
|
|
32
|
+
const e = `const logErr = ${JSON.stringify(logErr)}`;
|
|
33
|
+
const body = [
|
|
34
|
+
'setTimeout(() => {',
|
|
35
|
+
' try {',
|
|
36
|
+
' const out = fs.openSync(logOut, "a")',
|
|
37
|
+
' const err = fs.openSync(logErr, "a")',
|
|
38
|
+
' const child = spawn(file, args, { cwd, detached: true, stdio: ["ignore", out, err], env: process.env })',
|
|
39
|
+
' child.unref()',
|
|
40
|
+
' } catch {}',
|
|
41
|
+
'}, 1500)',
|
|
42
|
+
].join('\n');
|
|
43
|
+
return [spawn, fs, file, args, cwd, o, e, body].join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
22
46
|
/**
|
|
23
|
-
* 拉起替代宿主(detached
|
|
47
|
+
* 拉起替代宿主(detached 辅助进程交接),随后结束当前进程。
|
|
24
48
|
* @param {object} opts
|
|
25
|
-
* @param {number} [opts.
|
|
26
|
-
* @param {object} [opts.internals] 测试注入:
|
|
27
|
-
* @returns {{
|
|
49
|
+
* @param {number} [opts.handoffMs] 等待旧进程释放端口的时长(默认 1500ms)
|
|
50
|
+
* @param {object} [opts.internals] 测试注入:spawn / kill
|
|
51
|
+
* @returns {{helperPid:number|null, logOut:string, logErr:string}}
|
|
28
52
|
*/
|
|
29
|
-
export function restartHost({
|
|
53
|
+
export function restartHost({ handoffMs = 1500, internals = {} } = {}) {
|
|
30
54
|
const spawnFn = internals.spawn ?? spawn;
|
|
31
|
-
const
|
|
32
|
-
const
|
|
55
|
+
const killFn = internals.kill ?? ((pid) => process.kill(pid, 'SIGTERM'));
|
|
56
|
+
const launch = restartLaunch();
|
|
57
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
58
|
+
const logOut = join(tmpdir(), `dsh-pocket-restart-${stamp}.out.log`);
|
|
59
|
+
const logErr = join(tmpdir(), `dsh-pocket-restart-${stamp}.err.log`);
|
|
60
|
+
|
|
61
|
+
let helperPid = null;
|
|
33
62
|
try {
|
|
34
|
-
const
|
|
63
|
+
const helper = spawnFn(process.execPath, ['-e', helperCode(launch, logOut, logErr)], {
|
|
35
64
|
detached: true,
|
|
36
65
|
stdio: 'ignore',
|
|
37
66
|
env: process.env,
|
|
38
67
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
helper.unref?.();
|
|
69
|
+
helperPid = helper.pid ?? null;
|
|
70
|
+
// 短暂等待后结束当前进程(释放端口);由辅助进程在 1.5s 后拉起新宿主
|
|
71
|
+
setTimeout(() => { try { killFn(process.pid); } catch { /* 忽略 */ } }, 500);
|
|
42
72
|
} catch (err) {
|
|
43
|
-
return {
|
|
73
|
+
return { helperPid: null, logOut, logErr, error: err?.message ?? String(err) };
|
|
44
74
|
}
|
|
75
|
+
return { helperPid, logOut, logErr };
|
|
45
76
|
}
|
package/lib/web-rpc.js
CHANGED
|
@@ -17,7 +17,7 @@ function fail(code, message) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** 注册 /dsh-pocket 逻辑通道(仅本机 loopback 可调)。 */
|
|
20
|
-
export function installPocketRpc(ctx, { service, push, log = console, runUpdate = null, restart = null }) {
|
|
20
|
+
export function installPocketRpc(ctx, { service, push, log = console, runUpdate = null, restart = null, restartNotice = null }) {
|
|
21
21
|
if (!ctx?.connection?.rpc?.handle) {
|
|
22
22
|
log.warn?.('dsh-pocket: DSH Host Connection RPC unavailable — settings tab disabled | 无 Connection RPC,设置页不可用');
|
|
23
23
|
return () => {};
|
|
@@ -25,17 +25,24 @@ export function installPocketRpc(ctx, { service, push, log = console, runUpdate
|
|
|
25
25
|
return ctx.connection.rpc.handle(POCKET_RPC_CHANNEL, async (endpoint, payload = {}, signal) => {
|
|
26
26
|
if (signal?.aborted) return fail('cancelled', 'The request was cancelled.');
|
|
27
27
|
|
|
28
|
+
// status 响应:redact 后的服务状态 + 重启提示(重启后 30 分钟内有效)
|
|
29
|
+
const statusPayload = async () => {
|
|
30
|
+
let notice = null;
|
|
31
|
+
try { notice = restartNotice?.() ?? null; } catch { notice = null; }
|
|
32
|
+
return ok({ ...redactStatus(await service.status()), restartNotice: notice });
|
|
33
|
+
};
|
|
34
|
+
|
|
28
35
|
try {
|
|
29
36
|
if (endpoint === POCKET_ENDPOINTS.status) {
|
|
30
|
-
return
|
|
37
|
+
return await statusPayload();
|
|
31
38
|
}
|
|
32
39
|
if (endpoint === POCKET_ENDPOINTS.tunnelStart) {
|
|
33
40
|
await service.startTunnel();
|
|
34
|
-
return
|
|
41
|
+
return await statusPayload();
|
|
35
42
|
}
|
|
36
43
|
if (endpoint === POCKET_ENDPOINTS.tunnelStop) {
|
|
37
44
|
service.stopTunnel();
|
|
38
|
-
return
|
|
45
|
+
return await statusPayload();
|
|
39
46
|
}
|
|
40
47
|
if (endpoint === POCKET_ENDPOINTS.pushVapidKey) {
|
|
41
48
|
return ok({ publicKey: push.vapidPublicKey() });
|
|
@@ -66,7 +73,7 @@ export function installPocketRpc(ctx, { service, push, log = console, runUpdate
|
|
|
66
73
|
if (endpoint === POCKET_ENDPOINTS.restart) {
|
|
67
74
|
if (!restart) return fail('bad-request', '重启不可用 | restart unavailable');
|
|
68
75
|
const result = restart();
|
|
69
|
-
return ok(result);
|
|
76
|
+
return ok({ ...result, hint: '重启后进程在后台运行;如需停止:lsof -ti :3080 | xargs kill -9' });
|
|
70
77
|
}
|
|
71
78
|
return fail('bad-request', `Unknown endpoint: ${endpoint}`);
|
|
72
79
|
} catch (err) {
|
package/package.json
CHANGED