@coboot/headless 0.2.2-rc.4 → 0.2.2-rc.6
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 +18 -14
- package/cli.mjs +8 -8
- package/daemon.mjs +3 -3
- package/entry.mjs +571 -343
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,18 +6,22 @@ Headless 业务运行时要求 Node.js 20+;当入口检测到 Node.js 18/19
|
|
|
6
6
|
首次启动只需一次性设备 token:
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
9
|
+
npx -y @coboot/headless@0.2.2-rc.6 '<device-token>'
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
命令会把运行时安装到 `/data/coboot/.siboot`,在客户端鉴权和运行时初始化完成后启动
|
|
13
13
|
detached daemon 并退出。不需要设置 `BACKEND_URL`、`CLIENT_UID`。终端、文件等非浏览器能力不要求
|
|
14
14
|
目标机安装 Chromium。
|
|
15
15
|
|
|
16
|
+
同一数据目录已经运行 Headless 时,再次执行上述命令并传入新 token,会先停止旧 daemon 和 client,
|
|
17
|
+
再事务式切换到新设备身份。新凭证和新 client 都就绪后才删除旧凭证;注册或启动失败时会恢复旧凭证
|
|
18
|
+
并重新启动旧 client,不会让两个 Headless 进程争抢同一台物理机的 Driver 锁。
|
|
19
|
+
|
|
16
20
|
```bash
|
|
17
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
18
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
19
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
20
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
21
|
+
npx -y @coboot/headless@0.2.2-rc.6 status
|
|
22
|
+
npx -y @coboot/headless@0.2.2-rc.6 logs
|
|
23
|
+
npx -y @coboot/headless@0.2.2-rc.6 restart
|
|
24
|
+
npx -y @coboot/headless@0.2.2-rc.6 stop
|
|
21
25
|
```
|
|
22
26
|
|
|
23
27
|
更新到 npm `latest` 并重启:
|
|
@@ -41,13 +45,13 @@ Coboot daemon 默认禁用 updater 和浏览器预热,第一次调用 `browser
|
|
|
41
45
|
|
|
42
46
|
```bash
|
|
43
47
|
HEADLESS_BROWSER_EXECUTABLE=/absolute/path/to/chrome \
|
|
44
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
48
|
+
npx -y @coboot/headless@0.2.2-rc.6 '<device-token>'
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
需要安装浏览器时,由用户执行:
|
|
48
52
|
|
|
49
53
|
```bash
|
|
50
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
54
|
+
npx -y @coboot/headless@0.2.2-rc.6 install-browser
|
|
51
55
|
```
|
|
52
56
|
|
|
53
57
|
该命令不需要设备 token,也不要求 root。它会先检查现有浏览器并执行启动验证;浏览器已经可用时
|
|
@@ -82,13 +86,13 @@ dnf/yum 先试仓库中的 `chromium`,x86_64 回退到官方 RPM 并关闭 wea
|
|
|
82
86
|
仍会执行。目标网络需要能够访问对应发行版仓库或 `dl.google.com`。
|
|
83
87
|
|
|
84
88
|
|
|
85
|
-
|
|
89
|
+
自动化或排障需要显式表达重新注册时,也可以使用:
|
|
86
90
|
|
|
87
91
|
```bash
|
|
88
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
92
|
+
npx -y @coboot/headless@0.2.2-rc.6 --force-enroll '<device-token>'
|
|
89
93
|
```
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
当本地已经存在设备身份时,该命令与再次传入 positional token 使用同一套事务式替换流程。
|
|
92
96
|
|
|
93
97
|
|
|
94
98
|
## Linux 沙箱
|
|
@@ -122,7 +126,7 @@ HTTP 与 WebSocket 必须同域并与设备 token 匹配:
|
|
|
122
126
|
HEADLESS_ALLOW_ENDPOINT_OVERRIDE=1 \
|
|
123
127
|
BACKEND_URL=https://example.com \
|
|
124
128
|
WS_URL=wss://example.com/wsschat \
|
|
125
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
129
|
+
npx -y @coboot/headless@0.2.2-rc.6 '<device-token>'
|
|
126
130
|
```
|
|
127
131
|
|
|
128
132
|
WebSocket 覆盖变量名是 `WS_URL`,不是 `WSS_URL`。
|
|
@@ -136,7 +140,7 @@ endpoint 覆盖同样不会写入配置;后续执行 `restart` 时必须再次
|
|
|
136
140
|
CU_EDITION=coboot \
|
|
137
141
|
PKG_NAME=@coboot/headless \
|
|
138
142
|
PKG_BIN_NAME=coboot-headless \
|
|
139
|
-
PKG_VERSION=0.2.2-rc.
|
|
143
|
+
PKG_VERSION=0.2.2-rc.6 \
|
|
140
144
|
pnpm build:headless:npm
|
|
141
145
|
```
|
|
142
146
|
|
|
@@ -144,8 +148,8 @@ pnpm build:headless:npm
|
|
|
144
148
|
|
|
145
149
|
```bash
|
|
146
150
|
npm pack --dry-run --json ./dist-headless
|
|
147
|
-
tar -xOf dist-headless/coboot-headless-0.2.2-rc.
|
|
148
|
-
sha256sum dist-headless/coboot-headless-0.2.2-rc.
|
|
151
|
+
tar -xOf dist-headless/coboot-headless-0.2.2-rc.6.tgz package/package.json
|
|
152
|
+
sha256sum dist-headless/coboot-headless-0.2.2-rc.6.tgz
|
|
149
153
|
```
|
|
150
154
|
|
|
151
155
|
|
package/cli.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{execFileSync as
|
|
2
|
+
import{execFileSync as Ve,spawn as Q}from"child_process";import m from"fs";import i from"path";import{fileURLToPath as Je}from"url";var ie=new Set(["help","install-browser","start","stop","restart","update","status","logs"]);function M(e="siboot-headless",r="/data/siboot",t=!1){let n=t?` ${e} install-browser
|
|
3
3
|
`:"";console.log(`Usage:
|
|
4
4
|
${e} <device-token>
|
|
5
5
|
${e} --force-enroll <device-token>
|
|
6
6
|
${n} ${e} start|stop|restart|update|status|logs
|
|
7
7
|
|
|
8
8
|
Environment:
|
|
9
|
-
SIBOOT_DATA_ROOT Persistent data root (default: ${r})`)}function
|
|
9
|
+
SIBOOT_DATA_ROOT Persistent data root (default: ${r})`)}function H(e){let r=e[0]?.trim()||"";if(!r||r==="--help"||r==="-h"||r==="help")return{command:"help"};if(r==="--force-enroll"){let t=e[1]?.trim()||"";if(!t||e.length!==2)throw new Error("--force-enroll requires one device token");return{command:"install",token:t,forceEnroll:!0}}return ie.has(r)?{command:r}:{command:"install",token:r}}import{execFileSync as V}from"child_process";import{createRequire as ae}from"module";import h from"fs";import ce from"os";import w from"path";var le=["/usr/local/bin/google-chrome","/usr/local/bin/chromium","/usr/local/bin/chromium-browser","/usr/bin/google-chrome","/usr/bin/google-chrome-stable","/usr/bin/chromium","/usr/bin/chromium-browser"],de="https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm",ue="https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb",me=[/error while loading shared libraries:\s*([^\s:]+)/gi,/([A-Za-z0-9_.+-]+\.so(?:\.[0-9A-Za-z.]+)?):\s*cannot open shared object file/gi,/Missing libraries:\s*([^]*?)(?:\n\s*\n|$)/gi],fe=[/error while loading shared libraries:/i,/cannot open shared object file/i,/host system is missing dependencies/i,/missing libraries:/i];function P(e,r=process.env){let t=r.PLAYWRIGHT_BROWSERS_PATH?.trim();return t||w.join(e,"browsers")}function B(e){try{return h.accessSync(e,h.constants.X_OK),!0}catch{return!1}}function D(e){for(let r of(process.env.PATH||"").split(w.delimiter)){if(!r)continue;let t=w.join(r,e);if(B(t))return t}return null}function U(){let e=process.env.HEADLESS_BROWSER_EXECUTABLE?.trim();if(e&&B(e))return e;for(let r of le)if(B(r))return r;return D("google-chrome")||D("google-chrome-stable")||D("chromium")}function pe(e){return fe.some(r=>r.test(e))}function we(e){let r=new Set;for(let t of me){t.lastIndex=0;for(let n of e.matchAll(t))for(let o of n[1].split(/[\s,]+/)){let s=o.trim().replace(/[.,:;]+$/,"");/\.so(?:\.[0-9A-Za-z.]+)?$/.test(s)&&r.add(s)}}return[...r]}function he(e={arch:process.arch},r=D){let t=e.arch==="x64";if(r("apt-get"))return[{command:"apt-get",args:["update"],optional:!0},{attempts:[{command:"apt-get",args:["install","-y","--no-install-recommends","chromium"]},{command:"apt-get",args:["install","-y","--no-install-recommends","chromium-browser"]},...t?[{download:ue,command:"apt-get",args:["install","-y","--no-install-recommends"]}]:[]]}];if(r("apk"))return[{attempts:[{command:"apk",args:["add","--no-cache","chromium"]},{command:"apk",args:["add","--no-cache","--repository=https://dl-cdn.alpinelinux.org/alpine/edge/community","chromium"]}]}];let n=r("dnf")?"dnf":r("yum")?"yum":"";if(n){let o=[{command:n,args:["install","-y","--setopt=install_weak_deps=False","chromium"]}];return t&&o.push({command:n,args:["install","-y","--setopt=install_weak_deps=False",de]}),[{attempts:o}]}throw new Error("unsupported Linux package manager; install system Chrome/Chromium manually and set HEADLESS_BROWSER_EXECUTABLE")}async function Ee(e){let r=h.mkdtempSync(w.join(ce.tmpdir(),"coboot-browser-")),t=w.join(r,w.basename(new URL(e).pathname)||"browser-package");console.log(`Downloading: ${e}`);let n=await fetch(e);if(!n.ok)throw new Error(`download failed with HTTP ${n.status}: ${e}`);return h.writeFileSync(t,Buffer.from(await n.arrayBuffer())),{file:t,directory:r}}async function W(e){let r=e.download?await Ee(e.download):null,t=r?[...e.args,r.file]:e.args;try{console.log(`Running: ${e.command} ${t.join(" ")}`),V(e.command,t,{stdio:"inherit",env:{...process.env,DEBIAN_FRONTEND:"noninteractive"}})}finally{r&&h.rmSync(r.directory,{recursive:!0,force:!0})}}function _(e){return e instanceof Error?e.message:String(e)}async function Se(){for(let e of he()){if(!e.attempts){try{await W(e)}catch(n){if(!e.optional)throw n;console.warn(`Skipping optional step ${e.command}: ${_(n)}`)}continue}let r=[],t=!1;for(let n of e.attempts)try{await W(n),t=!0;break}catch(o){let s=n.download||n.args[n.args.length-1];r.push(`${s}: ${_(o)}`),console.warn(`Browser package candidate failed (${s}); trying the next one`)}if(!t)throw new Error(`no browser package could be installed:
|
|
10
10
|
${r.join(`
|
|
11
|
-
`)}`)}}function
|
|
12
|
-
\u8BF7\u786E\u8BA4\u5F53\u524D\u7528\u6237\u5BF9 SIBOOT_DATA_ROOT \u6709\u5199\u6743\u9650\uFF0C\u6216\u7528\u8BE5\u76EE\u5F55\u7684\u5C5E\u4E3B\u8D26\u53F7\u91CD\u65B0\u6267\u884C\u3002`)}return console.log(`Downloading Chromium into ${
|
|
13
|
-
`))}async function N(e,r){console.log(`Browser executable: ${r}`);try{let
|
|
14
|
-
`))}async function
|
|
15
|
-
`)),s}import
|
|
16
|
-
`,256)}function y(e,r){Te(e,r);let o=d.join(e.scripts,"daemon.mjs");u.existsSync(o)||g(d.join(r,"daemon.mjs"),o,320)}function Fe(e,r,o={platform:process.platform,arch:process.arch}){let n=x(e,r,o);return y(r,n.releaseDir),n}var Q=Ue(import.meta.url),ee={siboot:"/data/siboot",coboot:"/data/coboot"},We=15e3,K=7e3,Ve=73,z=20,Je=["BACKEND_URL","WS_URL","SIBOOT_URL","MCP_CREDENTIAL_API_BASE_URL","SKILL_CREDENTIAL_API_BASE_URL"],Ge=["HEADLESS_BWRAP_EXECUTABLE","HEADLESS_BWRAP_LOADER","HEADLESS_BWRAP_LIBRARY_DIR"];function Xe(e,r){let o=e||ee[r],n=i.resolve(o);if(!i.isAbsolute(o)||n===i.parse(n).root)throw new Error(`SIBOOT_DATA_ROOT must be a non-root absolute path: ${e}`);return n}function Ye(e=process.env.SIBOOT_DATA_ROOT,r="siboot"){if(!(r in ee))throw new Error(`unsupported headless edition: ${r}`);let o=Xe(e,r),n=i.join(o,".siboot");return{edition:r,root:o,home:n,releases:i.join(n,"releases"),current:i.join(n,"current"),scripts:i.join(n,"scripts"),run:i.join(n,"run"),logs:i.join(n,"logs"),workspace:i.join(o,"workspace"),credential:i.join(n,"headless-device-credential.json"),authSession:i.join(n,"headless-auth-session.json")}}function Ke(e,r){return`npx -y '${e}@${r}' install-browser`}function R(e){try{let r=Number.parseInt(m.readFileSync(e,"utf8").trim(),10);return Number.isInteger(r)&&r>0?r:null}catch{return null}}function h(e,r=process.platform==="linux"?"/proc":""){if(!e)return!1;try{process.kill(e,0)}catch{return!1}if(!r)return!0;try{let o=m.readFileSync(i.join(r,String(e),"stat"),"utf8"),n=o.lastIndexOf(") ")+2;return n<2||o[n]!=="Z"}catch{return!0}}function re(e,r){let o="";try{o=m.readFileSync(`/proc/${e}/cmdline`,"utf8").replaceAll("\0"," ").trim()}catch{}if(!o)try{o=Me("ps",["-ww","-p",String(e),"-o","command="],{encoding:"utf8"}).trim()}catch{return!1}return r.some(n=>[n,C(n)].some(t=>o.includes(t)))}var O=e=>new Promise(r=>setTimeout(r,e));function L(e=process.versions.node){let r=Number.parseInt(String(e).split(".")[0],10);if(!Number.isInteger(r)||r<z)throw new Error(`Node.js >=${z} is required; current version is ${e}`)}async function Z(e,r,o){let n=R(e);if(!h(n)){try{m.unlinkSync(e)}catch{}return!1}if(!re(n,r))throw new Error(`refusing to stop unrelated process from stale ${i.basename(e)}: ${n}`);process.kill(n,"SIGTERM");let t=Date.now()+K;for(;Date.now()<t&&h(n);)await O(100);if(h(n))throw new Error(`${o} did not stop within ${K/1e3} seconds: ${n}`);return R(e)===n&&m.rmSync(e,{force:!0}),!0}async function b(e){let r=await Z(i.join(e.run,"daemon.pid"),[i.join(e.current,"daemon.mjs"),i.join(e.scripts,"daemon.mjs")],"daemon"),o=await Z(i.join(e.run,"node.pid"),[i.join(e.current,"entry.mjs")],"client");return r||o}function ze(e,r,o=process.env){let n={...o,CU_EDITION:e.edition,NO_ELECTRON:"1",CLIENT_TYPE:"anydev",SIBOOT_BASE_DIR:e.home,SIBOOT_DATA_ROOT:e.root,HEADLESS_DATA_DIR:e.root,SIBOOT_DATA_DIR:e.root,HEADLESS_DEVICE_CREDENTIAL_PATH:e.credential,HEADLESS_AUTH_SESSION_PATH:e.authSession,HEADLESS_DEVICE_REQUIRE_AUTH:"1",HEADLESS_PREWARM_DEFAULT_SESSION:"0",WORKSPACE:e.workspace,ANYDEV_UPDATE_ENABLED:"0"};r?n.HEADLESS_DEVICE_TOKEN=r:delete n.HEADLESS_DEVICE_TOKEN,delete n.CLIENT_UID,delete n.HEADLESS_DEVICE_FORCE_ENROLL;for(let t of Ge)delete n[t];if(e.edition==="coboot"){let t=i.join(e.current,"native");n.HEADLESS_BWRAP_EXECUTABLE=i.join(t,"bwrap"),n.HEADLESS_BWRAP_LOADER=i.join(t,"ld-musl"),n.HEADLESS_BWRAP_LIBRARY_DIR=i.join(t,"lib"),n.HEADLESS_BROWSER_RUNTIME="system-or-guided-install",n.PLAYWRIGHT_BROWSERS_PATH=B(e.home,o)}else delete n.HEADLESS_BROWSER_RUNTIME,delete n.HEADLESS_BROWSER_INSTALL_COMMAND;if(n.HEADLESS_ALLOW_ENDPOINT_OVERRIDE!=="1")for(let t of Je)delete n[t];return n}async function Ze(e,r=We){let o=Date.now()+r,n=!1;for(;Date.now()<o;){let t=R(i.join(e.run,"daemon.pid"));if(!h(t))throw new Error("daemon exited during client startup");let s=null;try{s=JSON.parse(m.readFileSync(i.join(e.run,"status.json"),"utf8"))}catch{}if(s?.client_ready===!0&&h(s.client_pid))return;if(s?.client_exit_code!=null&&!s?.client_pid){if(s.client_exit_code===Ve){n=!0,await O(100);continue}throw new Error(`client exited during startup with code ${s.client_exit_code}`)}await O(100)}throw n?new Error("another Headless Driver still holds the machine lock after startup retries"):new Error(`client did not become ready within ${r}ms`)}function T(e,r=""){let o=e.edition==="coboot"?"coboot-headless":"siboot-headless";if(!m.existsSync(e.credential)&&!r)throw new Error(`device credential is missing; run ${o} <device-token> first`);let n=m.existsSync(i.join(e.current,"daemon.mjs"))?i.join(e.current,"daemon.mjs"):i.join(e.scripts,"daemon.mjs"),t=m.existsSync(i.join(e.current,"entry.mjs")),s=m.existsSync(n);if(!t||!s)throw new Error(`runtime is not installed (entry=${t}, daemon=${s}); run ${o} <device-token> first`);return n}async function A(e,r="",o=process.env){P(e);let n=T(e,r),t=m.existsSync(e.credential)?"":r,s=i.join(e.logs,"daemon",`${new Date().toISOString().slice(0,10)}.log`),a=m.openSync(s,"a",384);q(process.execPath,[n],{detached:!0,env:ze(e,t,o),stdio:["ignore",a,a]}).unref(),m.closeSync(a),await O(500);let c=R(i.join(e.run,"daemon.pid"));if(!h(c))throw new Error(`daemon failed to start; check ${s}`);return await Ze(e),{pid:c,logFile:s}}function k(e){try{let r=JSON.parse(m.readFileSync(i.join(e.current,"manifest.json"),"utf8"));return typeof r.version=="string"?r.version:null}catch{return null}}async function qe(e,r={}){return L(r.nodeVersion),T(e),await b(e),A(e,"",r.baseEnv||process.env)}async function ne(e,r,o={}){L(o.nodeVersion);let n=o.token||"";if(!m.existsSync(r.credential)&&!n){let l=r.edition==="coboot"?"coboot-headless":"siboot-headless";throw new Error(`device credential is missing; run ${l} <device-token> first`)}let t=j(r),s=k(r),a=x(e,r,o.runtime||{platform:process.platform,arch:process.arch});await b(r),y(r,a.releaseDir);try{let l=await A(r,n,o.baseEnv||process.env);return{previousVersion:s,installed:a,started:l}}catch(l){if(await b(r).catch(()=>{}),!t)throw l;y(r,t);try{await A(r,"",o.baseEnv||process.env)}catch(c){throw new AggregateError([l,c],`headless update failed and the previous runtime could not be restarted: ${c instanceof Error?c.message:String(c)}`)}throw new Error(`headless update failed; restored ${s||"the previous version"}`,{cause:l})}}async function Qe(e,r,o,n={}){return ne(e,r,{...n,token:o})}async function er(e,r,o={}){return T(r),ne(e,r,o)}function rr(e){let r=R(i.join(e.run,"daemon.pid")),o=null;try{o=JSON.parse(m.readFileSync(i.join(e.run,"status.json"),"utf8"))}catch{}console.log(JSON.stringify({running:h(r),pid:r,current:m.existsSync(e.current),status:o},null,2))}async function nr(e){let r=i.join(e.logs,"daemon",`${new Date().toISOString().slice(0,10)}.log`);if(!m.existsSync(r))throw new Error(`log file not found: ${r}`);let o=q("tail",["-n","100","-f",r],{stdio:"inherit"});await new Promise((n,t)=>{o.once("error",t),o.once("exit",s=>s===0?n():t(new Error(`tail exited ${s}`)))})}function v(e,r=e.version){let o={...process.env};return e.browserRuntime&&(o.HEADLESS_BROWSER_INSTALL_COMMAND=Ke(e.packageName,r)),o}async function or(e,r,o,n){L();let t=j(o),s=k(o),a=x(e,o);await b(o);try{return await Y(o,async()=>{y(o,a.releaseDir);let l=await A(o,n,v(r));return{installed:a,started:l}},()=>b(o))}catch(l){if(!t)throw l;y(o,t);try{await A(o,"",v(r,s||r.version))}catch(c){throw new AggregateError([l,c],`force enrollment failed and the previous runtime could not be restarted: ${c instanceof Error?c.message:String(c)}`)}throw l}}async function tr(){let e=I(process.argv.slice(2)),r=i.basename(process.argv[1]||"siboot-headless"),o=i.dirname(Q),n=D(o),t=Ye(process.env.SIBOOT_DATA_ROOT,n.edition);if(e.command==="help")return F(r,t.root,!!n.browserRuntime);if(e.command==="install-browser"){if(!n.browserRuntime)throw new Error("install-browser is available in the Coboot headless npm package only");return J(o,t)}let s=n.edition==="coboot"?"Coboot":"Siboot";if(e.command==="status")return rr(t);if(e.command==="logs")return nr(t);if(e.command==="stop"){let c=await b(t);console.log(c?`${s} Headless stopped`:`${s} Headless is not running`);return}if(e.command==="start"){L(),T(t);let c=R(i.join(t.run,"daemon.pid")),E=[i.join(t.current,"daemon.mjs"),i.join(t.scripts,"daemon.mjs")];if(h(c)&&re(c,E)){console.log(`${s} Headless is already running (pid=${c})`);return}await b(t);let S=await A(t,"",v(n,k(t)||n.version));console.log(`${s} Headless started in background (pid=${S.pid})`),console.log(`Logs: ${S.logFile}`);return}if(e.command==="restart"){let c=await qe(t,{baseEnv:v(n,k(t)||n.version)});console.log(`${s} Headless restarted in background (pid=${c.pid})`),console.log(`Logs: ${c.logFile}`);return}if(e.command==="update"){let{previousVersion:c,installed:E,started:S}=await er(o,t,{baseEnv:v(n)});console.log(`${s} Headless updated ${c||"unknown"} -> ${E.version} (pid=${S.pid})`),console.log(`Logs: ${S.logFile}`);return}let{installed:a,started:l}=e.forceEnroll?await or(o,n,t,e.token):await Qe(o,t,e.token||"",{baseEnv:v(n)});console.log(`${s} Headless ${a.version} started in background (pid=${l.pid})`),e.forceEnroll&&console.log("Device credential replaced by force enrollment"),console.log(`Logs: ${l.logFile}`)}function C(e){try{return m.realpathSync(e)}catch{return i.resolve(e)}}process.argv[1]&&C(process.argv[1])===C(Q)&&tr().catch(e=>{let r=i.basename(process.argv[1]||"headless-client");console.error(`${r}: ${e instanceof Error?e.message:String(e)}`),process.exitCode=1});export{L as assertSupportedNodeVersion,ze as createDaemonEnv,Qe as installAndStartHeadlessRuntime,Fe as installHeadlessRuntime,h as isRunning,I as parseHeadlessCliArgs,D as readPackageMetadata,Ye as resolveHeadlessLayout,qe as restartHeadlessRuntime,A as startHeadlessRuntime,b as stopHeadlessRuntime,er as updateHeadlessRuntime,T as validateHeadlessRuntimeStart};
|
|
11
|
+
`)}`)}}function J(e){let r=w.join(e,"browser-runtime","playwright-core"),t=ae(w.join(e,"package.json"));return{driverRoot:r,driver:t(r)}}async function ge(e){let{driverRoot:r,driver:t}=J(e),n=w.join(r,"cli.js"),o=process.env.PLAYWRIGHT_BROWSERS_PATH;try{h.mkdirSync(o,{recursive:!0,mode:448})}catch(s){throw new Error(`\u65E0\u6CD5\u521B\u5EFA\u6D4F\u89C8\u5668\u76EE\u5F55 ${o}: ${_(s)}
|
|
12
|
+
\u8BF7\u786E\u8BA4\u5F53\u524D\u7528\u6237\u5BF9 SIBOOT_DATA_ROOT \u6709\u5199\u6743\u9650\uFF0C\u6216\u7528\u8BE5\u76EE\u5F55\u7684\u5C5E\u4E3B\u8D26\u53F7\u91CD\u65B0\u6267\u884C\u3002`)}return console.log(`Downloading Chromium into ${o}`),V(process.execPath,[n,"install","chromium","--no-shell"],{stdio:"inherit"}),t.chromium.executablePath()}async function be(e,r){let{driver:t}=J(e),n=await t.chromium.launch({executablePath:r,headless:!0,args:["--no-sandbox","--disable-gpu","--disable-dev-shm-usage"]});try{let o=await n.newPage();if(await o.setContent("<title>Headless browser ready</title>"),await o.title()!=="Headless browser ready")throw new Error("browser smoke test returned wrong title");return n.version()}finally{await n.close()}}function _e(e,r){let t=we(r),n=t.length?`\u7F3A\u5C11\u7CFB\u7EDF\u52A8\u6001\u5E93: ${t.join(", ")}`:"\u7F3A\u5C11\u7CFB\u7EDF\u4F9D\u8D56\u5E93";return new Error([`${e} \u65E0\u6CD5\u542F\u52A8\uFF0C${n}\u3002`,"\u8865\u9F50\u7CFB\u7EDF\u4F9D\u8D56\u9700\u8981 root \u6743\u9650\uFF0C\u8BF7\u7528 sudo \u91CD\u65B0\u6267\u884C\u540C\u4E00\u6761 install-browser \u547D\u4EE4\uFF0C\u6216\u8BA9\u7BA1\u7406\u5458\u5B89\u88C5\u4E0A\u8FF0\u5E93\u3002",`\u539F\u59CB\u9519\u8BEF: ${r}`].join(`
|
|
13
|
+
`))}async function N(e,r){console.log(`Browser executable: ${r}`);try{let t=await be(e,r);return console.log(`Browser smoke test passed: ${t}`),{executablePath:r,version:t}}catch(t){let n=_(t);throw pe(n)?_e(r,n):t}}function ye(e){try{let r=w.join(e.run,"daemon.pid"),t=Number.parseInt(h.readFileSync(r,"utf8").trim(),10);return!Number.isInteger(t)||t<=0?null:h.statSync(`/proc/${t}`).uid}catch{return null}}function ve(e,r){let t=ye(e),n=process.getuid?.();t===null||n===void 0||t===n||console.warn([`\u6CE8\u610F: daemon \u6B63\u4EE5 uid=${t} \u8FD0\u884C\uFF0C\u800C\u6D4F\u89C8\u5668\u88C5\u5728 uid=${n} \u540D\u4E0B\u7684 ${r}\u3002`,"daemon \u53EF\u80FD\u65E0\u6743\u8BFB\u53D6\u8BE5\u76EE\u5F55\u3002\u8BF7\u6539\u7528 daemon \u6240\u5C5E\u8D26\u53F7\u91CD\u65B0\u6267\u884C\u672C\u547D\u4EE4\uFF0C","\u6216\u4EE5 root \u6267\u884C\u8BA9\u5305\u7BA1\u7406\u5668\u628A\u6D4F\u89C8\u5668\u88C5\u5230\u7CFB\u7EDF\u8DEF\u5F84\u3002"].join(`
|
|
14
|
+
`))}async function G(e,r){if(process.platform!=="linux")throw new Error("install-browser currently supports Linux only");let t=P(r.home);process.env.PLAYWRIGHT_BROWSERS_PATH=t;let n=U();if(n)try{return await N(e,n)}catch(a){console.warn(`Existing browser failed the smoke test: ${_(a)}`)}if(process.getuid?.()===0)try{await Se();let a=U();if(a)return await N(e,a);console.warn("\u5305\u7BA1\u7406\u5668\u6267\u884C\u5B8C\u6210\u4F46\u6CA1\u6709\u627E\u5230\u6D4F\u89C8\u5668\uFF0C\u56DE\u9000\u5230\u4E0B\u8F7D\u5B89\u88C5")}catch(a){console.warn(`\u7CFB\u7EDF\u5305\u7BA1\u7406\u5668\u5B89\u88C5\u5931\u8D25\uFF0C\u56DE\u9000\u5230\u4E0B\u8F7D\u5B89\u88C5: ${_(a)}`)}let o=await ge(e),s=await N(e,o);return console.log(`\u6D4F\u89C8\u5668\u5DF2\u88C5\u5165 ${t}\uFF0Cdaemon \u542F\u52A8\u65F6\u4F1A\u6307\u5411\u540C\u4E00\u76EE\u5F55\uFF0C\u65E0\u9700\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF\u3002`),ve(r,t),n&&console.warn([`\u6CE8\u610F: ${n} \u4ECD\u7136\u5B58\u5728\u4E14\u4F1A\u88AB\u4F18\u5148\u4F7F\u7528\uFF0C\u4F46\u5B83\u65E0\u6CD5\u542F\u52A8\u3002`,`\u8BF7\u7528 HEADLESS_BROWSER_EXECUTABLE=${o} \u91CD\u65B0\u542F\u52A8 daemon\uFF0C\u6216\u4EE5 root \u4FEE\u590D\u8BE5\u7CFB\u7EDF\u6D4F\u89C8\u5668\u3002`].join(`
|
|
15
|
+
`)),s}import E from"fs";var Re=e=>new Promise(r=>setTimeout(r,e));function X(e){if(!Number.isInteger(e)||e<=0)return!1;try{return process.kill(e,0),!0}catch{return!1}}function Y(e){try{return JSON.parse(E.readFileSync(e,"utf8"))}catch{return null}}function Ae(e){let r=Y(e);return!!(r?.deviceId?.trim()&&r?.credentialId?.trim()&&r?.ownerUid?.trim()&&r?.deviceName?.trim()&&r?.deviceType>0&&r?.publicKey?.trim()&&r?.privateKeyPem?.trim())}function $e(e){return[e.credential,e.authSession]}function xe(e){for(let r of e.entries){try{E.unlinkSync(r.file)}catch{}r.existed&&E.renameSync(r.backup,r.file)}}function De(e){let r=`${process.pid}-${Date.now()}`,t=$e(e).map(o=>({file:o,backup:`${o}.force-enroll-${r}.bak`,existed:E.existsSync(o)})),n=[];try{for(let o of t)o.existed&&(E.renameSync(o.file,o.backup),n.push(o))}catch(o){for(let s of n.reverse())E.renameSync(s.backup,s.file);throw o}return{entries:t}}function je(e){for(let r of e.entries)r.existed&&E.unlinkSync(r.backup)}function Oe(e){xe(e)}async function ke(e,r){let t=Date.now()+r,n=0;for(;Date.now()<t;){let o=Y(`${e.run}/status.json`);if(Ae(e.credential)&&o?.client_ready===!0&&X(o.client_pid)){if(n||=Date.now(),Date.now()-n>=1e3)return}else n=0;let s=Number.parseInt(E.readFileSync(`${e.run}/daemon.pid`,"utf8"),10);if(o?.client_exit_code!=null&&!o?.client_pid)throw new Error(`client exited during force enrollment with code ${o.client_exit_code}`);if(s&&!X(s))throw new Error("daemon exited during force enrollment");await Re(100)}throw new Error(`force enrollment did not become ready within ${r}ms`)}async function K(e,r,t,n={}){let o=De(e);try{let s=await r();return await ke(e,n.timeoutMs||15e3),je(o),s}catch(s){await t().catch(()=>{});try{Oe(o)}catch(a){throw new AggregateError([s,a],"force enrollment and rollback failed")}throw s}}import{createHash as Le}from"crypto";import l from"fs";import c from"path";var Te=new Set(["siboot","coboot"]);function C(e){for(let r of[e.home,e.releases,e.scripts,e.run,e.workspace,c.join(e.logs,"daemon"),c.join(e.logs,"client"),c.join(e.logs,"update")])l.mkdirSync(r,{recursive:!0,mode:448})}function x(e){let r=JSON.parse(l.readFileSync(c.join(e,"package.json"),"utf8")),t=String(r.name||"").trim();if(!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(t))throw new Error("package name is missing or invalid");let n=String(r.version||"").trim();if(!n||!/^[0-9A-Za-z._-]+$/.test(n))throw new Error("package version is missing or invalid");let o=String(r.headlessEdition||"").trim();if(!Te.has(o))throw new Error("package headlessEdition must be siboot or coboot");let s=r.headlessLinuxSandbox??null;Ie(o,s);let a=r.headlessBrowserRuntime??null;return He(o,a),{packageName:t,version:n,edition:o,linuxSandbox:s,browserRuntime:a}}function Ie(e,r){if(e==="siboot"&&r)throw new Error("Siboot package must not include Linux sandbox metadata");if(e==="coboot"&&(r?.backend!=="bubblewrap"||!Array.isArray(r.architectures)||r.architectures.length===0))throw new Error("Coboot package is missing bundled Linux sandbox metadata")}function He(e,r){if(e==="siboot"&&r)throw new Error("Siboot package must not include guided browser runtime metadata");if(e==="coboot"&&(r?.driver!=="playwright-core"||!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(r.driverVersion||"")||r?.browser!=="system-or-guided-install"))throw new Error("Coboot package is missing valid guided browser runtime metadata")}function Ne(e,r){let t=c.relative(e.home,r),n=`${e.current}.next-${process.pid}`;l.rmSync(n,{force:!0}),l.symlinkSync(t,n),l.renameSync(n,e.current)}function j(e){try{let r=l.readlinkSync(e.current);return c.resolve(c.dirname(e.current),r)}catch{try{return l.realpathSync(e.current)}catch{return null}}}function g(e,r,t){let n=`${r}.next-${process.pid}`;l.copyFileSync(e,n),l.chmodSync(n,t),l.renameSync(n,r)}function Be(e,r,t){let n=`${e}.next-${process.pid}`;l.writeFileSync(n,r,{mode:t}),l.renameSync(n,e)}function Pe(e){return Le("sha256").update(l.readFileSync(e)).digest("hex")}function Ce(e,r){if(!l.existsSync(e)||Pe(e)!==r)throw new Error(`bundled Linux sandbox file failed integrity check: ${c.basename(e)}`)}function Fe(e,r,t,n){if(!t)return null;if(n.platform!=="linux")throw new Error(`Coboot Headless bundled sandbox supports Linux only: ${n.platform}`);if(!t.architectures.includes(n.arch))throw new Error(`Coboot Headless does not include Linux sandbox for arch: ${n.arch}`);let o=c.join(e,"native",`linux-${n.arch}`),s=c.join(o,"manifest.json");if(!l.existsSync(s))throw new Error(`package is missing Linux sandbox bundle for arch: ${n.arch}`);let a=JSON.parse(l.readFileSync(s,"utf8"));if(a.backend!==t.backend||a.backendVersion!==t.backendVersion||a.arch!==n.arch)throw new Error("bundled Linux sandbox manifest does not match package metadata");let d={bwrap:c.join(o,"bwrap"),loader:c.join(o,"ld-musl"),libcap:c.join(o,"lib","libcap.so.2")};for(let[f,p]of Object.entries(d))Ce(p,a.files?.[f]);let u=c.join(r,"native");return l.mkdirSync(c.join(u,"lib"),{recursive:!0,mode:448}),g(d.bwrap,c.join(u,"bwrap"),320),g(d.loader,c.join(u,"ld-musl"),320),g(d.libcap,c.join(u,"lib","libcap.so.2"),256),g(s,c.join(u,"manifest.json"),256),{backend:a.backend,version:a.backendVersion,arch:a.arch,bundled:!0}}function Me(e,r,t){let n=c.join(e,"browser-runtime","playwright-core"),o=c.join(n,"package.json");if(!l.existsSync(o))throw new Error("package is missing bundled playwright-core");let s=JSON.parse(l.readFileSync(o,"utf8"));if(s.name!==t.driver||s.version!==t.driverVersion)throw new Error("bundled playwright-core does not match package metadata");let a=c.join(r,"node_modules","playwright-core"),d=`${a}.next-${process.pid}`;return l.rmSync(d,{recursive:!0,force:!0}),l.mkdirSync(c.dirname(a),{recursive:!0,mode:448}),l.cpSync(n,d,{recursive:!0}),l.rmSync(a,{recursive:!0,force:!0}),l.renameSync(d,a),t}function O(e,r,t={platform:process.platform,arch:process.arch}){C(r);let{version:n,edition:o,linuxSandbox:s,browserRuntime:a}=x(e);if(o!==r.edition)throw new Error(`package edition ${o} does not match runtime layout ${r.edition}`);let d=c.join(r.releases,n);if(j(r)===d)return{version:n,releaseDir:d,reused:!0};let u=c.join(r.releases,`.staging-${n}-${process.pid}`);l.rmSync(u,{recursive:!0,force:!0}),l.mkdirSync(u,{recursive:!0,mode:448});try{Ue(e,u,{version:n,edition:o,linuxSandbox:s,browserRuntime:a},t),l.rmSync(d,{recursive:!0,force:!0}),l.renameSync(u,d)}catch(f){throw l.rmSync(u,{recursive:!0,force:!0}),f}return{version:n,releaseDir:d,reused:!1}}function Ue(e,r,t,n){let o=c.join(e,"entry.mjs"),s=c.join(e,"daemon.mjs");if(!l.existsSync(o)||!l.existsSync(s))throw new Error("package is missing entry.mjs or daemon.mjs");g(o,c.join(r,"entry.mjs"),320),g(s,c.join(r,"daemon.mjs"),320);let a=Fe(e,r,t.linuxSandbox,n),d=t.browserRuntime?Me(e,r,t.browserRuntime):"disabled";Be(c.join(r,"manifest.json"),`${JSON.stringify({type:"headless-npm-daemon",version:t.version,edition:t.edition,browserRuntime:d,linuxSandbox:a},null,2)}
|
|
16
|
+
`,256)}function y(e,r){Ne(e,r);let t=c.join(e.scripts,"daemon.mjs");l.existsSync(t)||g(c.join(r,"daemon.mjs"),t,320)}function We(e,r,t={platform:process.platform,arch:process.arch}){let n=O(e,r,t);return y(r,n.releaseDir),n}var ee=Je(import.meta.url),re={siboot:"/data/siboot",coboot:"/data/coboot"},Ge=15e3,z=7e3,Xe=73,Z=20,Ye=["BACKEND_URL","WS_URL","SIBOOT_URL","MCP_CREDENTIAL_API_BASE_URL","SKILL_CREDENTIAL_API_BASE_URL"],Ke=["HEADLESS_BWRAP_EXECUTABLE","HEADLESS_BWRAP_LOADER","HEADLESS_BWRAP_LIBRARY_DIR"];function ze(e,r){let t=e||re[r],n=i.resolve(t);if(!i.isAbsolute(t)||n===i.parse(n).root)throw new Error(`SIBOOT_DATA_ROOT must be a non-root absolute path: ${e}`);return n}function Ze(e=process.env.SIBOOT_DATA_ROOT,r="siboot"){if(!(r in re))throw new Error(`unsupported headless edition: ${r}`);let t=ze(e,r),n=i.join(t,".siboot");return{edition:r,root:t,home:n,releases:i.join(n,"releases"),current:i.join(n,"current"),scripts:i.join(n,"scripts"),run:i.join(n,"run"),logs:i.join(n,"logs"),workspace:i.join(t,"workspace"),credential:i.join(n,"headless-device-credential.json"),authSession:i.join(n,"headless-auth-session.json")}}function qe(e,r){return`npx -y '${e}@${r}' install-browser`}function R(e){try{let r=Number.parseInt(m.readFileSync(e,"utf8").trim(),10);return Number.isInteger(r)&&r>0?r:null}catch{return null}}function S(e,r=process.platform==="linux"?"/proc":""){if(!e)return!1;try{process.kill(e,0)}catch{return!1}if(!r)return!0;try{let t=m.readFileSync(i.join(r,String(e),"stat"),"utf8"),n=t.lastIndexOf(") ")+2;return n<2||t[n]!=="Z"}catch{return!0}}function ne(e,r){let t="";try{t=m.readFileSync(`/proc/${e}/cmdline`,"utf8").replaceAll("\0"," ").trim()}catch{}if(!t)try{t=Ve("ps",["-ww","-p",String(e),"-o","command="],{encoding:"utf8"}).trim()}catch{return!1}return r.some(n=>[n,F(n)].some(o=>t.includes(o)))}var k=e=>new Promise(r=>setTimeout(r,e));function T(e=process.versions.node){let r=Number.parseInt(String(e).split(".")[0],10);if(!Number.isInteger(r)||r<Z)throw new Error(`Node.js >=${Z} is required; current version is ${e}`)}async function q(e,r,t){let n=R(e);if(!S(n)){try{m.unlinkSync(e)}catch{}return!1}if(!ne(n,r))throw new Error(`refusing to stop unrelated process from stale ${i.basename(e)}: ${n}`);process.kill(n,"SIGTERM");let o=Date.now()+z;for(;Date.now()<o&&S(n);)await k(100);if(S(n))throw new Error(`${t} did not stop within ${z/1e3} seconds: ${n}`);return R(e)===n&&m.rmSync(e,{force:!0}),!0}async function b(e){let r=await q(i.join(e.run,"daemon.pid"),[i.join(e.current,"daemon.mjs"),i.join(e.scripts,"daemon.mjs")],"daemon"),t=await q(i.join(e.run,"node.pid"),[i.join(e.current,"entry.mjs")],"client");return r||t}function Qe(e,r,t=process.env){let n={...t,CU_EDITION:e.edition,NO_ELECTRON:"1",CLIENT_TYPE:"anydev",SIBOOT_BASE_DIR:e.home,SIBOOT_DATA_ROOT:e.root,HEADLESS_DATA_DIR:e.root,SIBOOT_DATA_DIR:e.root,HEADLESS_DEVICE_CREDENTIAL_PATH:e.credential,HEADLESS_AUTH_SESSION_PATH:e.authSession,HEADLESS_DEVICE_REQUIRE_AUTH:"1",HEADLESS_PREWARM_DEFAULT_SESSION:"0",WORKSPACE:e.workspace,ANYDEV_UPDATE_ENABLED:"0"};r?n.HEADLESS_DEVICE_TOKEN=r:delete n.HEADLESS_DEVICE_TOKEN,delete n.CLIENT_UID,delete n.HEADLESS_DEVICE_FORCE_ENROLL;for(let o of Ke)delete n[o];if(e.edition==="coboot"){let o=i.join(e.current,"native");n.HEADLESS_BWRAP_EXECUTABLE=i.join(o,"bwrap"),n.HEADLESS_BWRAP_LOADER=i.join(o,"ld-musl"),n.HEADLESS_BWRAP_LIBRARY_DIR=i.join(o,"lib"),n.HEADLESS_BROWSER_RUNTIME="system-or-guided-install",n.PLAYWRIGHT_BROWSERS_PATH=P(e.home,t)}else delete n.HEADLESS_BROWSER_RUNTIME,delete n.HEADLESS_BROWSER_INSTALL_COMMAND;if(n.HEADLESS_ALLOW_ENDPOINT_OVERRIDE!=="1")for(let o of Ye)delete n[o];return n}async function er(e,r=Ge){let t=Date.now()+r,n=!1;for(;Date.now()<t;){let o=R(i.join(e.run,"daemon.pid"));if(!S(o))throw new Error("daemon exited during client startup");let s=null;try{s=JSON.parse(m.readFileSync(i.join(e.run,"status.json"),"utf8"))}catch{}if(s?.client_ready===!0&&S(s.client_pid))return;if(s?.client_exit_code!=null&&!s?.client_pid){if(s.client_exit_code===Xe){n=!0,await k(100);continue}throw new Error(`client exited during startup with code ${s.client_exit_code}`)}await k(100)}throw n?new Error("another Headless Driver still holds the machine lock after startup retries"):new Error(`client did not become ready within ${r}ms`)}function I(e,r=""){let t=e.edition==="coboot"?"coboot-headless":"siboot-headless";if(!m.existsSync(e.credential)&&!r)throw new Error(`device credential is missing; run ${t} <device-token> first`);let n=m.existsSync(i.join(e.current,"daemon.mjs"))?i.join(e.current,"daemon.mjs"):i.join(e.scripts,"daemon.mjs"),o=m.existsSync(i.join(e.current,"entry.mjs")),s=m.existsSync(n);if(!o||!s)throw new Error(`runtime is not installed (entry=${o}, daemon=${s}); run ${t} <device-token> first`);return n}async function A(e,r="",t=process.env){C(e);let n=I(e,r),o=m.existsSync(e.credential)?"":r,s=i.join(e.logs,"daemon",`${new Date().toISOString().slice(0,10)}.log`),a=m.openSync(s,"a",384);Q(process.execPath,[n],{detached:!0,env:Qe(e,o,t),stdio:["ignore",a,a]}).unref(),m.closeSync(a),await k(500);let u=R(i.join(e.run,"daemon.pid"));if(!S(u))throw new Error(`daemon failed to start; check ${s}`);return await er(e),{pid:u,logFile:s}}function L(e){try{let r=JSON.parse(m.readFileSync(i.join(e.current,"manifest.json"),"utf8"));return typeof r.version=="string"?r.version:null}catch{return null}}async function rr(e,r={}){return T(r.nodeVersion),I(e),await b(e),A(e,"",r.baseEnv||process.env)}async function te(e,r,t={}){T(t.nodeVersion);let n=t.token||"";if(!m.existsSync(r.credential)&&!n){let d=r.edition==="coboot"?"coboot-headless":"siboot-headless";throw new Error(`device credential is missing; run ${d} <device-token> first`)}let o=j(r),s=L(r),a=O(e,r,t.runtime||{platform:process.platform,arch:process.arch});await b(r),y(r,a.releaseDir);try{let d=await A(r,n,t.baseEnv||process.env);return{previousVersion:s,installed:a,started:d}}catch(d){if(await b(r).catch(()=>{}),!o)throw d;y(r,o);try{await A(r,"",t.baseEnv||process.env)}catch(u){throw new AggregateError([d,u],`headless update failed and the previous runtime could not be restarted: ${u instanceof Error?u.message:String(u)}`)}throw new Error(`headless update failed; restored ${s||"the previous version"}`,{cause:d})}}async function nr(e,r,t,n={}){return oe(r)?se(e,r,t,n):{...await te(e,r,{...n,token:t}),identityReplaced:!1}}async function tr(e,r,t={}){return I(r),te(e,r,t)}function or(e){let r=R(i.join(e.run,"daemon.pid")),t=null;try{t=JSON.parse(m.readFileSync(i.join(e.run,"status.json"),"utf8"))}catch{}console.log(JSON.stringify({running:S(r),pid:r,current:m.existsSync(e.current),status:t},null,2))}async function sr(e){let r=i.join(e.logs,"daemon",`${new Date().toISOString().slice(0,10)}.log`);if(!m.existsSync(r))throw new Error(`log file not found: ${r}`);let t=Q("tail",["-n","100","-f",r],{stdio:"inherit"});await new Promise((n,o)=>{t.once("error",o),t.once("exit",s=>s===0?n():o(new Error(`tail exited ${s}`)))})}function v(e,r=e.version){let t={...process.env};return e.browserRuntime&&(t.HEADLESS_BROWSER_INSTALL_COMMAND=qe(e.packageName,r)),t}function oe(e){return m.existsSync(e.credential)||m.existsSync(e.authSession)}async function se(e,r,t,n={}){T(n.nodeVersion);let o=x(e),s=oe(r),a=j(r),d=L(r),u=O(e,r,n.runtime||{platform:process.platform,arch:process.arch});await b(r);try{return await K(r,async()=>{y(r,u.releaseDir);let f=await A(r,t,n.baseEnv||v(o));return{installed:u,started:f,identityReplaced:s}},()=>b(r))}catch(f){if(!a)throw f;y(r,a);try{await A(r,"",v(o,d||o.version))}catch(p){throw new AggregateError([f,p],`force enrollment failed and the previous runtime could not be restarted: ${p instanceof Error?p.message:String(p)}`)}throw f}}async function ir(){let e=H(process.argv.slice(2)),r=i.basename(process.argv[1]||"siboot-headless"),t=i.dirname(ee),n=x(t),o=Ze(process.env.SIBOOT_DATA_ROOT,n.edition);if(e.command==="help")return M(r,o.root,!!n.browserRuntime);if(e.command==="install-browser"){if(!n.browserRuntime)throw new Error("install-browser is available in the Coboot headless npm package only");return G(t,o)}let s=n.edition==="coboot"?"Coboot":"Siboot";if(e.command==="status")return or(o);if(e.command==="logs")return sr(o);if(e.command==="stop"){let f=await b(o);console.log(f?`${s} Headless stopped`:`${s} Headless is not running`);return}if(e.command==="start"){T(),I(o);let f=R(i.join(o.run,"daemon.pid")),p=[i.join(o.current,"daemon.mjs"),i.join(o.scripts,"daemon.mjs")];if(S(f)&&ne(f,p)){console.log(`${s} Headless is already running (pid=${f})`);return}await b(o);let $=await A(o,"",v(n,L(o)||n.version));console.log(`${s} Headless started in background (pid=${$.pid})`),console.log(`Logs: ${$.logFile}`);return}if(e.command==="restart"){let f=await rr(o,{baseEnv:v(n,L(o)||n.version)});console.log(`${s} Headless restarted in background (pid=${f.pid})`),console.log(`Logs: ${f.logFile}`);return}if(e.command==="update"){let{previousVersion:f,installed:p,started:$}=await tr(t,o,{baseEnv:v(n)});console.log(`${s} Headless updated ${f||"unknown"} -> ${p.version} (pid=${$.pid})`),console.log(`Logs: ${$.logFile}`);return}let{installed:a,started:d,identityReplaced:u}=e.forceEnroll?await se(t,o,e.token):await nr(t,o,e.token||"",{baseEnv:v(n)});console.log(`${s} Headless ${a.version} started in background (pid=${d.pid})`),u?console.log("Existing device credential replaced by new enrollment"):e.forceEnroll&&console.log("Device credential created by force enrollment"),console.log(`Logs: ${d.logFile}`)}function F(e){try{return m.realpathSync(e)}catch{return i.resolve(e)}}process.argv[1]&&F(process.argv[1])===F(ee)&&ir().catch(e=>{let r=i.basename(process.argv[1]||"headless-client");console.error(`${r}: ${e instanceof Error?e.message:String(e)}`),process.exitCode=1});export{T as assertSupportedNodeVersion,Qe as createDaemonEnv,nr as installAndStartHeadlessRuntime,We as installHeadlessRuntime,S as isRunning,H as parseHeadlessCliArgs,x as readPackageMetadata,Ze as resolveHeadlessLayout,rr as restartHeadlessRuntime,A as startHeadlessRuntime,b as stopHeadlessRuntime,tr as updateHeadlessRuntime,I as validateHeadlessRuntimeStart};
|
package/daemon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
`;
|
|
4
|
-
`),process.exit(
|
|
2
|
+
import o from"fs";import W from"net";import q from"os";import r from"path";import{spawn as h}from"child_process";var l=process.env.SIBOOT_BASE_DIR,_=r.join(l,"current"),N=process.env.WORKSPACE||"/data/siboot/workspace",f=r.join(l,"run"),p=r.join(l,"logs"),m=r.join(_,"entry.mjs"),A=r.join(l,"electron-headless-host"),B=r.join(l,"scripts","fetch-electron-headless-host.sh"),O=r.join(l,"scripts","updater.mjs"),F=r.join(f,"daemon.pid"),H=r.join(f,"node.pid"),b=r.join(f,"updater.pid"),Q=r.join(f,"status.json"),k=73,z=5e3,Z=3e4,V=Number.parseInt(process.env.ANYDEV_CLIENT_RESTART_DELAY_MS||"1000",10),ee=Number.parseInt(process.env.ANYDEV_UPDATER_RESTART_DELAY_MS||"10000",10),L=Number.parseInt(process.env.ANYDEV_LOG_RETENTION_DAYS||"4",10),a=null,u=null,T=null,E=!1,w=0,g=0,R=!1,D=null,y=null,S=0,P=null,ne=new Date().toISOString();async function te(){let e=W.createServer(t=>t.destroy()),n=process.platform==="linux"?{path:"\0siboot-anydev-daemon-v1"}:{host:"127.0.0.1",port:43880,exclusive:!0};await new Promise((t,s)=>{e.once("error",s),e.listen(n,()=>{e.removeListener("error",s),t()})})}function re(){for(let e of[f,N,r.join(p,"daemon"),r.join(p,"client"),r.join(p,"update")])o.mkdirSync(e,{recursive:!0})}function oe(){return new Date().toISOString().slice(0,10)}function G(e){return r.join(p,e,`${oe()}.log`)}function i(e,n){let t=`${new Date().toISOString()} [${e}] ${n}
|
|
3
|
+
`;o.appendFile(G(e),t,()=>{}),e==="daemon"&&process.stdout.write(t)}function K(e){try{return JSON.parse(o.readFileSync(e,"utf8"))}catch{return null}}function ie(e,n){o.mkdirSync(r.dirname(e),{recursive:!0}),o.writeFileSync(e,JSON.stringify(n,null,2),"utf8")}function se(e){try{o.unlinkSync(e)}catch{}}function $(e,n){if(n)try{o.readFileSync(e,"utf8").trim()===String(n)&&o.unlinkSync(e)}catch(t){t.code!=="ENOENT"&&i("daemon",`pid cleanup failed file=${e} error=${t.message}`)}}function I(){let e=K(r.join(_,"manifest.json"));if(typeof e?.version=="string"&&e.version.trim())return e.version.trim();try{return r.basename(o.realpathSync(_))}catch{return process.env.APP_VERSION||"unknown"}}function ce(){let e=process.env.HEADLESS_DEVICE_CREDENTIAL_PATH||process.env.SIBOOT_DEVICE_CREDENTIAL_PATH;if(e&&e.trim())return r.resolve(e.trim());let n=process.env.HEADLESS_DATA_DIR||process.env.SIBOOT_DATA_DIR||r.join(q.homedir(),".siboot");return r.join(n,"headless-device-credential.json")}function ae(){try{return o.existsSync(ce())}catch{return!1}}function d(e={}){ie(Q,{pid:process.pid,client_pid:a?.pid||null,updater_pid:u?.pid||null,client_ready:R,client_ready_at:D,client_version:y,client_lock_conflict_count:S,client_retry_at:P,client_start_count:w,updater_start_count:g,version:I(),current_dir:_,state:E?"stopping":S>0?"client_lock_conflict":"running",started_at:ne,updated_at:new Date().toISOString(),...e})}function x(e,n){let t=s=>{let c=Buffer.from(s);o.appendFile(G(n),c,()=>{})};e.stdout?.on("data",t),e.stderr?.on("data",t)}function U(){if(!Number.isFinite(L)||L<=0)return;let e=Date.now();for(let n of["daemon","client","update"]){let t=r.join(p,n);if(o.existsSync(t))for(let s of o.readdirSync(t)){let c=r.join(t,s),C=null;try{C=o.statSync(c)}catch{continue}if(!C.isFile())continue;e-C.mtimeMs>L*24*60*60*1e3&&se(c)}}}function J(){let e=r.join(A,"electron-headless-host"),n=o.existsSync(r.join(e,"electron-host-app","main","entry.js"))?e:_,t=r.join(n,"electron-host-app","main","entry.js"),s=r.join(n,"patches","electron","v32.3.3","runtime.json"),c={CU_ELECTRON_RUNTIME_CACHE:process.env.CU_ELECTRON_RUNTIME_CACHE||r.join(l,"electron-runtime"),HEADLESS:process.env.HEADLESS||"true"};return process.platform==="linux"&&(process.getuid?.()===0||process.env.ELECTRON_NO_SANDBOX==="1")&&(c.ELECTRON_NO_SANDBOX="1"),o.existsSync(s)&&(c.CU_APP_DESKTOP_ROOT=n),o.existsSync(t)&&(c.ELECTRON_HEADLESS_APP_PATH=t),c}function le(){let e=K(r.join(_,"manifest.json"));return String(process.env.ELECTRON_HOST_CDN_BASE||e?.electron_host_cdn_base||"").trim()}function M(){return o.existsSync(r.join(A,"electron-headless-host","electron-host-app","main","entry.js"))&&o.existsSync(r.join(A,"electron-headless-host","patches","electron","v32.3.3","runtime.json"))}async function ue(){if(M())return!0;let e=le();return!e||!o.existsSync(B)?(i("daemon","electron host bundle is not configured; browser tools may be unavailable"),!1):(i("daemon",`downloading electron host bundle base=${e}`),await new Promise((n,t)=>{let s=h("bash",[B],{env:{...process.env,ELECTRON_HOST_CDN_BASE:e,ELECTRON_HOST_FETCH_DIR:A},stdio:["ignore","pipe","pipe"]});x(s,"electron-host"),s.once("error",t),s.once("exit",c=>{c===0?n():t(new Error(`electron host bundle fetch exited with code=${c}`))})}),M())}function de(e){T||process.env.HEADLESS_PREWARM_ELECTRON_RUNTIME==="0"||!o.existsSync(m)||!e.ELECTRON_HEADLESS_APP_PATH||e.CU_APP_DESKTOP_ROOT&&(i("daemon",`prewarming patched electron runtime entry=${m}`),T=h(process.execPath,[m,"--prewarm-electron-runtime"],{cwd:N,env:e,stdio:["ignore","pipe","pipe"]}),x(T,"client"),T.on("exit",(n,t)=>{i("daemon",`electron runtime prewarm exited code=${n} signal=${t}`),T=null}))}function j(){if(E||a)return;if(!o.existsSync(m)){i("daemon",`client entry missing entry=${m}`),d({state:"client_entry_missing"}),setTimeout(j,V).unref?.();return}w+=1,R=!1,D=null,y=I(),P=null;let e={...process.env,...J(),APP_VERSION:y,CLIENT_TYPE:process.env.CLIENT_TYPE||"anydev",SIBOOT_BASE_DIR:l,SIBOOT_CURRENT_DIR:_,SIBOOT_RUN_DIR:f,SIBOOT_LOG_DIR:p};ae()&&(delete e.HEADLESS_DEVICE_TOKEN,delete process.env.HEADLESS_DEVICE_TOKEN),i("daemon",`starting client entry=${m} cwd=${N} version=${I()} start_count=${w}`),a=h(process.execPath,[m],{cwd:N,env:e,stdio:["ignore","pipe","pipe","ipc"]});let n=a;o.writeFileSync(H,String(a.pid)),x(a,"client"),d(),a.on("message",t=>{t?.type!=="headless-client-ready"||a!==n||(S>0&&i("daemon",`client lock recovered conflicts=${S} pid=${n.pid}`),S=0,R=!0,D=new Date().toISOString(),d(),i("daemon",`client ready pid=${a?.pid||""}`))}),a.on("error",t=>{i("daemon",`client spawn error error=${t.message}`)}),a.on("exit",(t,s)=>{i("daemon",`client exited pid=${a?.pid||""} code=${t} signal=${s}`),a=null,R=!1,D=null,y=null,$(H,n.pid);let c=_e(t);P=E?null:new Date(Date.now()+c).toISOString(),d({client_exit_code:t,client_exit_signal:s}),E||setTimeout(j,c).unref?.()})}function _e(e){return e!==k?V:(S+=1,S===1&&i("daemon","client lock conflict; retrying with bounded backoff"),Math.min(z*2**Math.min(S-1,3),Z))}function X(){if(E||u||process.env.ANYDEV_UPDATE_ENABLED==="0")return;if(!o.existsSync(O)){i("daemon",`updater entry missing entry=${O}`);return}g+=1,i("daemon",`starting updater entry=${O} start_count=${g}`),u=h(process.execPath,[O],{cwd:l,env:{...process.env,CLIENT_TYPE:"anydev",SIBOOT_BASE_DIR:l,SIBOOT_CURRENT_DIR:_,SIBOOT_RUN_DIR:f,SIBOOT_LOG_DIR:p},stdio:["ignore","inherit","inherit"]});let e=u;o.writeFileSync(b,String(u.pid)),d(),u.on("error",n=>{i("daemon",`updater spawn error error=${n.message}`)}),u.on("exit",(n,t)=>{i("daemon",`updater exited pid=${u?.pid||""} code=${n} signal=${t}`),u=null,$(b,e.pid),d({updater_exit_code:n,updater_exit_signal:t}),E||setTimeout(X,ee).unref?.()})}function v(e,n){return e?.pid?(i("daemon",`stopping ${n} pid=${e.pid}`),new Promise(t=>{let s=!1,c=()=>{s||(s=!0,t())};e.once("exit",c);try{e.kill("SIGTERM")}catch{c();return}setTimeout(()=>{if(!s){try{e.kill("SIGKILL")}catch{}c()}},5e3).unref?.()})):Promise.resolve()}async function Y(e){E||(E=!0,i("daemon",`shutdown signal=${e}`),d({state:"stopping",signal:e}),await Promise.all([v(u,"updater"),v(a,"client"),v(T,"electron-runtime-prewarm")]),$(F,process.pid),d({state:"stopped",signal:e}),process.exit(0))}async function pe(){await te(),re(),o.writeFileSync(F,String(process.pid)),process.on("SIGTERM",()=>{Y("SIGTERM")}),process.on("SIGINT",()=>{Y("SIGINT")}),i("daemon",`started base=${l} version=${I()}`),d({state:"starting"}),U();try{await ue()||i("daemon","electron host bundle is not ready; continuing without browser host")}catch(n){i("daemon",`electron host bundle fetch failed: ${n.message}`)}let e={...process.env,...J(),APP_VERSION:I(),CLIENT_TYPE:process.env.CLIENT_TYPE||"anydev",SIBOOT_BASE_DIR:l,SIBOOT_CURRENT_DIR:_,SIBOOT_RUN_DIR:f,SIBOOT_LOG_DIR:p};de(e),j(),X(),setInterval(U,3600*1e3)}pe().catch(e=>{let n=e.code==="EADDRINUSE";process.stderr.write(`[daemon] ${n?"another daemon is already running":e.message}
|
|
4
|
+
`),process.exit(n?k:1)});
|