@coboot/headless 0.2.2-rc.5 → 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 +12 -12
- package/daemon.mjs +3 -3
- package/entry.mjs +571 -343
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ 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`,在客户端鉴权和运行时初始化完成后启动
|
|
@@ -18,10 +18,10 @@ detached daemon 并退出。不需要设置 `BACKEND_URL`、`CLIENT_UID`。终
|
|
|
18
18
|
并重新启动旧 client,不会让两个 Headless 进程争抢同一台物理机的 Driver 锁。
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
22
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
23
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
24
|
-
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
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
更新到 npm `latest` 并重启:
|
|
@@ -45,13 +45,13 @@ Coboot daemon 默认禁用 updater 和浏览器预热,第一次调用 `browser
|
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
HEADLESS_BROWSER_EXECUTABLE=/absolute/path/to/chrome \
|
|
48
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
48
|
+
npx -y @coboot/headless@0.2.2-rc.6 '<device-token>'
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
需要安装浏览器时,由用户执行:
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
54
|
+
npx -y @coboot/headless@0.2.2-rc.6 install-browser
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
该命令不需要设备 token,也不要求 root。它会先检查现有浏览器并执行启动验证;浏览器已经可用时
|
|
@@ -89,7 +89,7 @@ dnf/yum 先试仓库中的 `chromium`,x86_64 回退到官方 RPM 并关闭 wea
|
|
|
89
89
|
自动化或排障需要显式表达重新注册时,也可以使用:
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
92
|
+
npx -y @coboot/headless@0.2.2-rc.6 --force-enroll '<device-token>'
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
当本地已经存在设备身份时,该命令与再次传入 positional token 使用同一套事务式替换流程。
|
|
@@ -126,7 +126,7 @@ HTTP 与 WebSocket 必须同域并与设备 token 匹配:
|
|
|
126
126
|
HEADLESS_ALLOW_ENDPOINT_OVERRIDE=1 \
|
|
127
127
|
BACKEND_URL=https://example.com \
|
|
128
128
|
WS_URL=wss://example.com/wsschat \
|
|
129
|
-
npx -y @coboot/headless@0.2.2-rc.
|
|
129
|
+
npx -y @coboot/headless@0.2.2-rc.6 '<device-token>'
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
WebSocket 覆盖变量名是 `WS_URL`,不是 `WSS_URL`。
|
|
@@ -140,7 +140,7 @@ endpoint 覆盖同样不会写入配置;后续执行 `restart` 时必须再次
|
|
|
140
140
|
CU_EDITION=coboot \
|
|
141
141
|
PKG_NAME=@coboot/headless \
|
|
142
142
|
PKG_BIN_NAME=coboot-headless \
|
|
143
|
-
PKG_VERSION=0.2.2-rc.
|
|
143
|
+
PKG_VERSION=0.2.2-rc.6 \
|
|
144
144
|
pnpm build:headless:npm
|
|
145
145
|
```
|
|
146
146
|
|
|
@@ -148,8 +148,8 @@ pnpm build:headless:npm
|
|
|
148
148
|
|
|
149
149
|
```bash
|
|
150
150
|
npm pack --dry-run --json ./dist-headless
|
|
151
|
-
tar -xOf dist-headless/coboot-headless-0.2.2-rc.
|
|
152
|
-
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
|
|
153
153
|
```
|
|
154
154
|
|
|
155
155
|
|
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)});
|