fix(cua): retain packaged helper startup diagnostics

This commit is contained in:
creatixchu committed 2026-10-28 16:39:43 +08:00
1 parent c6f3dff031
commit ce1ce9f2a1
6 files changed
+76 -9

No files matched your search

@@ -15,8 +15,8 @@
en: e8cbc43d38786c40
zh: 52b3ba85d98011b9
/dsh-cua-native-helper/build-and-service:
en: b123dd551e7bbdb4
zh: b41c61e09c758dcb
en: 2316cb8b4f961469
zh: 4616a6de11fe933c
/dsh-cua-native-helper/request-protocol:
en: fc8043c425322cdd
zh: cf1edb9a78fda301
@@ -27,7 +27,7 @@ Run `pnpm --dir packages/experimental/cua-macos run build:native` to publish a s
Generations in `native/.build/prepared/<arch>` and `native/bin/darwin-<arch>` are retained without automatic garbage collection. Use a fresh `--output-directory` for release staging, or clean the packaging output before building while no client uses it, so old generations do not enter the tarball. Secure signing timestamps can create distinct generations for identical sources.
The manual **Package CUA helper · macOS** [workflow](../../../../.github/workflows/cua-helper-package.yml) builds both architectures on `dsh-mac-package-trial`. Select a trusted `source_ref` and machine `preset`; credentials use the existing [macOS packaging configuration](../../../../apps/desktop/packaging/README.md#macos-actions). The runner needs an active macOS login session and Rosetta for the x64 startup check. `pnpm exec tsx scripts/cua-helper/package.ts --out <new-directory>` runs the same preparation from the repository root. It signs, notarizes and staples each complete application before selecting its final content digest, then verifies signatures, tickets, Gatekeeper, helper identity and cleanup from the extracted `cua-helper.tgz`. Permission checks do not request access or inspect applications.
The manual **Package CUA helper · macOS** [workflow](../../../../.github/workflows/cua-helper-package.yml) builds both architectures on `dsh-mac-package-trial`. Select a trusted `source_ref` and machine `preset`; credentials use the existing [macOS packaging configuration](../../../../apps/desktop/packaging/README.md#macos-actions). The runner needs an active macOS login session and Rosetta for the x64 startup check. `pnpm exec tsx scripts/cua-helper/package.ts --out <new-directory>` runs the same preparation from the repository root. It signs, notarizes and staples each complete application before selecting its final content digest, then verifies signatures, tickets, Gatekeeper, helper identity and cleanup from the extracted `cua-helper.tgz`. Permission checks do not request access or inspect applications. Failed smoke commands include captured native stdout and stderr in the workflow log, including startup error codes and messages.
The workflow retains `cua-helper.tgz` and `cua-helper.json`, which binds the product version, source commit, Team ID and both application digests. [Release publish](../../../../.github/workflows/release-publish.yml) consumes this artifact before packing the provider for npm; ordinary PR release rehearsals have no signing credentials. Staging rejects a source/version mismatch, incomplete architectures, stale generations, links, changed bytes and missing executable permissions. npm publication rechecks the exact provider tarball before any registry write. A re-signed release has different bytes; preserve the retained artifact when retrying a partially published version.
@@ -27,7 +27,7 @@ kind: "package-reference"
`native/.build/prepared/<arch>` 和 `native/bin/darwin-<arch>` 中的各代应用不会自动清理。正式打包时使用全新的 `--output-directory`,或确认没有客户端使用后清理打包输出再构建,避免旧应用进入 tarball。安全签名时间戳可能让相同源码生成不同代应用。
手动工作流 **Package CUA helper · macOS** 的[配置](../../../../.github/workflows/cua-helper-package.yml)在 `dsh-mac-package-trial` 构建两种架构。选择可信的 `source_ref` 和机器 `preset`;凭据沿用[macOS 打包配置](../../../../apps/desktop/packaging/README.zh.md#macos-actions)。runner 需要已登录的 macOS 桌面会话,以及用于 x64 启动检查的 Rosetta。在仓库根目录运行 `pnpm exec tsx scripts/cua-helper/package.ts --out <new-directory>` 可执行相同准备。完整应用签名、公证和贴票后才确定最终内容摘要,再从解包后的 `cua-helper.tgz` 验证签名、票据、Gatekeeper、helper 身份和退出清理。权限查询不会申请授权或观察应用。
手动工作流 **Package CUA helper · macOS** 的[配置](../../../../.github/workflows/cua-helper-package.yml)在 `dsh-mac-package-trial` 构建两种架构。选择可信的 `source_ref` 和机器 `preset`;凭据沿用[macOS 打包配置](../../../../apps/desktop/packaging/README.zh.md#macos-actions)。runner 需要已登录的 macOS 桌面会话,以及用于 x64 启动检查的 Rosetta。在仓库根目录运行 `pnpm exec tsx scripts/cua-helper/package.ts --out <new-directory>` 可执行相同准备。完整应用签名、公证和贴票后才确定最终内容摘要,再从解包后的 `cua-helper.tgz` 验证签名、票据、Gatekeeper、helper 身份和退出清理。权限查询不会申请授权或观察应用。smoke 命令失败时,工作流日志保留原生进程的 stdout 和 stderr,包括启动错误码与错误消息。
工作流保留 `cua-helper.tgz` 和 `cua-helper.json`,后者记录产品版本、源码 commit、Team ID 和两份应用摘要。[Release publish](../../../../.github/workflows/release-publish.yml)在生成 provider 的 npm 包前接入该产物;普通 PR 发布检查不使用签名凭据。准备步骤拒绝源码或版本不匹配、架构不全、旧应用目录、链接、字节变化和执行权限缺失。npm 发布在写入注册表前重新检查实际 provider tarball。重新签名会改变产物字节,重试部分发布的版本时需保留原产物。
+16 -5
View File
@@ -11,6 +11,18 @@ import tempfile
import time
def run_checked(command, timeout):
"""Preserve captured native diagnostics when startup or registration cleanup fails."""
try:
return subprocess.run(command, check=True, capture_output=True, text=True, timeout=timeout)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error:
for label, output in [("stdout", error.stdout), ("stderr", error.stderr)]:
if output:
text = output.decode("utf-8", errors="replace") if isinstance(output, bytes) else output
print(f"CUA smoke command {label}:\n{text}", file=sys.stderr, flush=True)
raise
def wait_for_exit(pid, seconds):
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
@@ -29,9 +41,8 @@ def main():
with tempfile.TemporaryDirectory(prefix="cua-smoke-", dir="/tmp") as directory:
os.chmod(directory, 0o700)
try:
started = subprocess.run([str(executable), "--ensure-shared", "--runtime-key", key,
"--runtime-directory", directory, "--startup-timeout-ms", "15000"],
check=True, capture_output=True, text=True, timeout=25)
started = run_checked([str(executable), "--ensure-shared", "--runtime-key", key,
"--runtime-directory", directory, "--startup-timeout-ms", "15000"], 25)
ready = json.loads(started.stdout)
assert ready["runtimeKey"] == key and ready["type"] == "ready", ready
pid = ready["pid"]
@@ -74,8 +85,8 @@ def main():
break
else:
raise RuntimeError("failed helper did not exit during cleanup")
subprocess.run(["/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister",
"-u", str(executable.parents[2])], check=True, capture_output=True, timeout=10)
run_checked(["/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister",
"-u", str(executable.parents[2])], 10)
if __name__ == "__main__":
+12
View File
@@ -0,0 +1,12 @@
/** Native smoke failures must expose the captured launch error in CI logs. */
import { execa } from 'execa'
import { expect, it } from 'vitest'
it.skipIf(process.platform === 'win32')('retains native startup and timeout diagnostics', async ({ signal }) => {
const result = await execa('python3', ['-m', 'unittest', 'discover', '-s', 'scripts/cua-helper', '-p', 'test_smoke.py'], {
cancelSignal: signal, killDescendants: true, reject: false,
})
expect(result.isCanceled).toBe(false)
expect(result.signal).toBeUndefined()
expect(result.exitCode, result.stderr).toBe(0)
})
+44
View File
@@ -0,0 +1,44 @@
"""Smoke command failures retain native diagnostics without performing desktop operations."""
from contextlib import redirect_stderr
import io
import subprocess
import unittest
from unittest.mock import patch
import smoke
class SmokeDiagnostics(unittest.TestCase):
def test_success_returns_captured_reply_without_replaying_it(self):
result = subprocess.CompletedProcess(["helper"], 0, '{"type":"ready"}', "")
diagnostics = io.StringIO()
with patch.object(smoke.subprocess, "run", return_value=result) as run, redirect_stderr(diagnostics):
self.assertIs(smoke.run_checked(["helper"], 25), result)
run.assert_called_once_with(["helper"], check=True, capture_output=True, text=True, timeout=25)
self.assertEqual(diagnostics.getvalue(), "")
def test_native_failure_keeps_code_message_and_stderr(self):
error = subprocess.CalledProcessError(1, ["helper"],
output='{"type":"error","code":"NATIVE_STARTUP","message":"launch failed"}',
stderr="native diagnostic")
diagnostics = io.StringIO()
with patch.object(smoke.subprocess, "run", side_effect=error), redirect_stderr(diagnostics):
with self.assertRaises(subprocess.CalledProcessError) as raised:
smoke.run_checked(["helper"], 25)
self.assertIs(raised.exception, error)
self.assertIn(error.stdout, diagnostics.getvalue())
self.assertIn(error.stderr, diagnostics.getvalue())
def test_timeout_keeps_partial_bytes_and_exit_failure(self):
error = subprocess.TimeoutExpired(["helper"], 25, output=b"partial reply", stderr=b"startup timeout")
diagnostics = io.StringIO()
with patch.object(smoke.subprocess, "run", side_effect=error), redirect_stderr(diagnostics):
with self.assertRaises(subprocess.TimeoutExpired) as raised:
smoke.run_checked(["helper"], 25)
self.assertIs(raised.exception, error)
self.assertIn("partial reply", diagnostics.getvalue())
self.assertIn("startup timeout", diagnostics.getvalue())
if __name__ == "__main__":
unittest.main()