@lenorin/dsh-tauri-launcher 1.0.10 → 1.0.11

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.
Binary file
package/lib/index.js CHANGED
@@ -27,6 +27,18 @@ export const inject = ['webServer', 'fs', 'subprocess']
27
27
 
28
28
  const STDIO = { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' }
29
29
 
30
+ /**
31
+ * 拉起桌面应用时使用的 stdio(**不能用 inherit**)。
32
+ *
33
+ * DSH 自身可能是被桌面应用拉起的:它的 stdout/stderr 就是桌面应用当时创建的管道
34
+ * (`harness.rs` 用 `Stdio::piped()` 拉起 `dsh web`)。桌面应用退出后这根管道的读端
35
+ * 消失,此后以 inherit 拉起的子进程继承的是**已断开的手柄**,一写即 panic 退出
36
+ * (2026-09-15 实测:exit code 101,现象为「设置里开关打开但桌面端起不来」,而关闭
37
+ * 方向正常——关闭只写标记文件)。收集模式让管道由 DSH 这一侧持有,既避开坏句柄,
38
+ * 又把桌面应用启动期的输出留下来供诊断使用。
39
+ */
40
+ const APP_STDIO = { stdin: 'ignore', stdout: { maxBytes: 16384 }, stderr: { maxBytes: 16384 } }
41
+
30
42
  /** 本包随附的预编译桌面应用 exe 目录(无论安装到哪个 profile 都能定位)。 */
31
43
  const PACKAGE_BIN = fileURLToPath(new URL('../launcher/bin', import.meta.url))
32
44
 
@@ -157,6 +169,10 @@ export function apply(ctx, config) {
157
169
  let lastProbeError = ''
158
170
  let spawnHandle = null
159
171
  let shortcutCache = { t: 0, value: false }
172
+ /** 最近一次拉起桌面应用的退出事实(未退出为 null),用于把「起来即死」报出来。 */
173
+ let appExit = null
174
+ /** 该桌面应用进程启动期的输出(收集模式),仅在启动失败时用于诊断。 */
175
+ let appOutput = ''
160
176
 
161
177
  /**
162
178
  * 已在本次插件运行中发现过的候选目录(最近使用在前)。实例退出后仍保留:
@@ -191,6 +207,31 @@ export function apply(ctx, config) {
191
207
  }
192
208
  }
193
209
 
210
+ /** 收集模式下该句柄的输出(stdout/stderr 合并、限长),用于诊断「起来即死」。 */
211
+ function collectedText(handle) {
212
+ if (!handle || !handle.collected) return ''
213
+ const parts = []
214
+ for (const stream of ['stdout', 'stderr']) {
215
+ const reader = handle.collected[stream]
216
+ if (!reader) continue
217
+ try {
218
+ const text = String(reader.readFrom(0).text || '').trim()
219
+ if (text) parts.push(stream + ': ' + text)
220
+ } catch {
221
+ // 收集流不可读取时忽略:诊断信息缺一行不影响主流程。
222
+ }
223
+ }
224
+ return parts.join('\n').slice(0, 2000)
225
+ }
226
+
227
+ /** 退出事实的可读描述(exit code / signal / 未知原因)。 */
228
+ function describeExit(outcome) {
229
+ if (!outcome) return '未知原因'
230
+ if (typeof outcome.exitCode === 'number') return 'exit code ' + outcome.exitCode
231
+ if (outcome.signal) return 'signal ' + outcome.signal
232
+ return outcome.error ? String(outcome.error) : '未知原因'
233
+ }
234
+
194
235
  /** 记住一个已发现的候选目录(去重、最近使用在前、限量)。 */
195
236
  function rememberDir(dir) {
196
237
  if (!dir) return
@@ -494,6 +535,8 @@ export function apply(ctx, config) {
494
535
  if (lastFsError) lines.push('fsError: ' + lastFsError)
495
536
  if (lastProbeError) lines.push('probeError: ' + lastProbeError)
496
537
  lines.push('markerWrite: ' + (lastMarkerError || 'ok'))
538
+ if (appExit) lines.push('appExit: ' + describeExit(appExit))
539
+ if (appOutput) lines.push('appOutput: ' + appOutput)
497
540
  for (const dir of dirs) {
498
541
  lines.push('heartbeat(' + dir.split('\\').slice(-2).join('\\') + '): ' + String(await hbState(dir)))
499
542
  }
@@ -513,6 +556,8 @@ export function apply(ctx, config) {
513
556
  }
514
557
 
515
558
  async function setDesktop(enabled) {
559
+ appExit = null
560
+ appOutput = ''
516
561
  if (enabled) {
517
562
  if (await quitMarkerIsOne()) await writeQuitMarker('0')
518
563
  const exe = await pickExe()
@@ -526,7 +571,16 @@ export function apply(ctx, config) {
526
571
  const dir = dirOf(exe)
527
572
  // 拉起前记住该目录:即使探测缓存仍是旧的,心跳扫描也已覆盖新实例的目录。
528
573
  rememberDir(dir)
529
- spawnHandle = subprocess.spawn({ argv: [exe], cwd: dir, stdio: STDIO, graceMs: 3000 })
574
+ const handle = subprocess.spawn({ argv: [exe], cwd: dir, stdio: APP_STDIO, graceMs: 3000 })
575
+ spawnHandle = handle
576
+ // 记录退出事实:桌面应用「起来即死」时(例如继承了坏句柄而 panic),
577
+ // 只有退出码 + 它自己的输出能说明原因,否则用户只看到开关弹回而没有解释。
578
+ if (handle && handle.done) {
579
+ handle.done.then(
580
+ (outcome) => { appExit = outcome || { exitCode: null, signal: null } },
581
+ (error) => { appExit = { exitCode: null, signal: null, error: String((error && error.message) || error) } },
582
+ )
583
+ }
530
584
  } catch (error) {
531
585
  return { ok: false, error: '启动桌面应用失败:' + String((error && error.message) || error), diag: await buildDiag() }
532
586
  }
@@ -544,6 +598,17 @@ export function apply(ctx, config) {
544
598
  }
545
599
  return { ok: true, desktop: true, shortcut: true, diag: '' }
546
600
  }
601
+ // 未在就绪窗口内看到新鲜心跳:若进程已退出,把退出事实与输出报出来。
602
+ appOutput = collectedText(spawnHandle)
603
+ if (appExit) {
604
+ return {
605
+ ok: false,
606
+ desktop: false,
607
+ shortcut: await shortcutExists(),
608
+ error: '桌面应用启动后立即退出(' + describeExit(appExit) + ')。',
609
+ diag: await buildDiag(),
610
+ }
611
+ }
547
612
  if (state === null) return { ok: true, desktop: null, shortcut: await shortcutExists(), diag: '' }
548
613
  return { ok: true, desktop: false, shortcut: await shortcutExists(), diag: '' }
549
614
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenorin/dsh-tauri-launcher",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "DSH Web 插件:基于 Tauri 2 的 DeepSeek Harness 桌面启动器,自动联动桌面快捷方式,exe 路径与探测窗口可配置。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",