@juejin-opensource/jusage 0.1.1-beta.8 → 0.1.1

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/bin/jusage.js CHANGED
@@ -1,2 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import '../dist/index.js';
2
+ if (process.argv[2] === 'service' && process.argv[3] === 'start') {
3
+ process.stdout.write('正在等待检测进程与面板 /health …\n');
4
+ }
5
+ await import('../dist/index.js');
package/dist/daemon.d.ts CHANGED
@@ -4,7 +4,16 @@ export declare function daemonLogPath(dataDir?: string): string;
4
4
  export declare function ensureDaemonLogDir(dataDir?: string): Promise<string>;
5
5
  /** Soft then hard kill. Returns true if the process is gone. */
6
6
  export declare function stopPid(pid: number, timeoutMs?: number): Promise<boolean>;
7
+ /** True when local-api `/health` returns `{ ok: true }`. */
8
+ export declare function probeLocalHealth(port: number): Promise<boolean>;
7
9
  export declare function waitForPid(dataDir?: string, timeoutMs?: number): Promise<number | null>;
10
+ export interface ServiceReady {
11
+ pid: number | null;
12
+ health: boolean;
13
+ }
14
+ /** Wait until the pid file is live, or `/health` is up as a fallback. */
15
+ export declare function waitForServiceReady(dataDir: string, port: number, timeoutMs?: number): Promise<ServiceReady>;
16
+ export declare function readDaemonLogTail(dataDir?: string, maxChars?: number): Promise<string>;
8
17
  export declare function resolveServiceCommand(cliBinPath: string): {
9
18
  nodePath: string;
10
19
  args: string[];
package/dist/daemon.js CHANGED
@@ -1,6 +1,8 @@
1
- import { mkdir } from 'node:fs/promises';
1
+ import { spawnSync } from 'node:child_process';
2
+ import { realpathSync } from 'node:fs';
3
+ import { mkdir, readFile } from 'node:fs/promises';
2
4
  import { join } from 'node:path';
3
- import { DEFAULT_DATA_DIR, logsDir, clearPid, getRunningOwner, getRunningPid, isPidAlive, pidFilePath, readPid, readRuntimeOwner, writePid, writeRuntimeOwner, } from '@juejin-opensource/jusage-core';
5
+ import { DEFAULT_DATA_DIR, argsMatchRuntimeKind, logsDir, clearPid, getRunningOwner, getRunningPid, isPidAlive, pidFilePath, readPid, readProcessArgs, readRuntimeOwner, writePid, writeRuntimeOwner, } from '@juejin-opensource/jusage-core';
4
6
  export { clearPid, getRunningOwner, getRunningPid, isPidAlive, pidFilePath, readPid, readRuntimeOwner, writePid, writeRuntimeOwner, };
5
7
  export function daemonLogPath(dataDir = DEFAULT_DATA_DIR) {
6
8
  return join(logsDir(dataDir), 'daemon.log');
@@ -43,22 +45,124 @@ export async function stopPid(pid, timeoutMs = 5000) {
43
45
  await new Promise((r) => setTimeout(r, 200));
44
46
  return !isPidAlive(pid);
45
47
  }
48
+ async function readLivePid(dataDir) {
49
+ // Do not go through getRunningOwner: a locale-mismatched startedAt would
50
+ // delete the pid file the daemon just wrote, and this wait would never see it.
51
+ const owner = await readRuntimeOwner(dataDir);
52
+ if (owner != null && isPidAlive(owner.pid))
53
+ return owner.pid;
54
+ return null;
55
+ }
56
+ /** True when local-api `/health` returns `{ ok: true }`. */
57
+ export async function probeLocalHealth(port) {
58
+ try {
59
+ const res = await fetch(`http://127.0.0.1:${port}/health`, {
60
+ signal: AbortSignal.timeout(400),
61
+ });
62
+ if (!res.ok)
63
+ return false;
64
+ const body = (await res.json());
65
+ return body.ok === true;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ function readListeningPid(port) {
72
+ try {
73
+ if (process.platform === 'win32') {
74
+ const result = spawnSync('powershell.exe', [
75
+ '-NoProfile',
76
+ '-NonInteractive',
77
+ '-ExecutionPolicy',
78
+ 'Bypass',
79
+ '-Command',
80
+ `(Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty OwningProcess)`,
81
+ ], { encoding: 'utf8', windowsHide: true, timeout: 3_000 });
82
+ const pid = Number((result.stdout ?? '').trim());
83
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
84
+ }
85
+ const result = spawnSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'], { encoding: 'utf8', timeout: 2_000 });
86
+ const pid = Number((result.stdout ?? '').trim().split('\n')[0]);
87
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ async function recoverPidFromHealth(dataDir, port) {
94
+ const listenPid = readListeningPid(port);
95
+ if (listenPid == null || !isPidAlive(listenPid))
96
+ return null;
97
+ const args = readProcessArgs(listenPid) ?? '';
98
+ const kind = argsMatchRuntimeKind(args, 'desktop')
99
+ ? 'desktop'
100
+ : argsMatchRuntimeKind(args, 'cli')
101
+ ? 'cli'
102
+ : null;
103
+ if (!kind)
104
+ return null;
105
+ await writePid(listenPid, dataDir, kind);
106
+ return listenPid;
107
+ }
46
108
  export async function waitForPid(dataDir = DEFAULT_DATA_DIR, timeoutMs = 15_000) {
47
109
  const deadline = Date.now() + timeoutMs;
48
110
  while (Date.now() < deadline) {
49
- const pid = await getRunningPid(dataDir);
111
+ const pid = await readLivePid(dataDir);
50
112
  if (pid != null)
51
113
  return pid;
52
114
  await new Promise((r) => setTimeout(r, 200));
53
115
  }
54
116
  return getRunningPid(dataDir);
55
117
  }
118
+ /** Wait until the pid file is live, or `/health` is up as a fallback. */
119
+ export async function waitForServiceReady(dataDir, port, timeoutMs = 15_000) {
120
+ const deadline = Date.now() + timeoutMs;
121
+ while (Date.now() < deadline) {
122
+ const pid = await readLivePid(dataDir);
123
+ if (pid != null)
124
+ return { pid, health: await probeLocalHealth(port) };
125
+ if (await probeLocalHealth(port)) {
126
+ return { pid: await recoverPidFromHealth(dataDir, port), health: true };
127
+ }
128
+ await new Promise((r) => setTimeout(r, 200));
129
+ }
130
+ const pid = (await readLivePid(dataDir)) ?? (await getRunningPid(dataDir));
131
+ if (pid != null)
132
+ return { pid, health: await probeLocalHealth(port) };
133
+ if (await probeLocalHealth(port)) {
134
+ return { pid: await recoverPidFromHealth(dataDir, port), health: true };
135
+ }
136
+ return { pid: null, health: false };
137
+ }
138
+ export async function readDaemonLogTail(dataDir = DEFAULT_DATA_DIR, maxChars = 2000) {
139
+ try {
140
+ const raw = await readFile(daemonLogPath(dataDir), 'utf8');
141
+ const trimmed = raw.trimEnd();
142
+ if (!trimmed)
143
+ return '';
144
+ return trimmed.length <= maxChars ? trimmed : trimmed.slice(-maxChars);
145
+ }
146
+ catch {
147
+ return '';
148
+ }
149
+ }
150
+ function resolveExistingPath(path) {
151
+ try {
152
+ return realpathSync(path);
153
+ }
154
+ catch {
155
+ return path;
156
+ }
157
+ }
56
158
  export function resolveServiceCommand(cliBinPath) {
57
- const nodePath = process.execPath;
58
- const args = [cliBinPath, 'start'];
159
+ // fnm/nvm shims live in session-specific dirs; persist the real binary.
160
+ const nodePath = resolveExistingPath(process.execPath);
161
+ const binPath = resolveExistingPath(cliBinPath);
162
+ const args = [binPath, 'start'];
59
163
  const commandLine = process.platform === 'win32'
60
- ? `"${nodePath}" "${cliBinPath}" start`
61
- : `${shellQuote(nodePath)} ${shellQuote(cliBinPath)} start`;
164
+ ? `"${nodePath}" "${binPath}" start`
165
+ : `${shellQuote(nodePath)} ${shellQuote(binPath)} start`;
62
166
  return { nodePath, args, commandLine };
63
167
  }
64
168
  function shellQuote(value) {
@@ -0,0 +1 @@
1
+ import{t as p,R as l,j as t,c7 as v,r as s,c8 as g,a9 as c,c2 as f,c3 as x,a7 as h,c1 as w,c9 as u,ca as m,a$ as $}from"./index-DKJwDJt7.js";const j=p({slots:{base:"popover",dialog:"popover__dialog",heading:"popover__heading",trigger:"popover__trigger"}}),n=s.createContext({}),d=({children:o,...a})=>{const r=l.useMemo(()=>j(),[]);return t.jsx(n,{value:{slots:r},children:t.jsx(v,{"data-slot":"popover-root",...a,children:o})})},b=({children:o,className:a,...r})=>{const{slots:e}=s.use(n);return t.jsx(n,{value:{slots:e},children:t.jsx(f,{value:{variant:"default"},children:t.jsx(x,{...r,className:h(a,e==null?void 0:e.base()),children:o})})})},C=({children:o,className:a,...r})=>{const e=t.jsx("svg",{"data-slot":"popover-overlay-arrow",fill:"none",height:"12",viewBox:"0 0 12 12",width:"12",xmlns:"http://www.w3.org/2000/svg",children:t.jsx("path",{d:"M0 0C5.48483 8 6.5 8 12 0Z"})}),i=l.isValidElement(o)?l.cloneElement(o,{"data-slot":"popover-overlay-arrow"}):e;return t.jsx(w,{"data-slot":"popover-overlay-arrow-group",...r,className:a,children:i})},P=({children:o,className:a,...r})=>{const{slots:e}=s.use(n);return t.jsx(u,{"data-slot":"popover-dialog",...r,className:c(e==null?void 0:e.dialog,a),children:o})},R=({children:o,className:a,...r})=>{const{slots:e}=s.use(n);return t.jsx(m,{children:t.jsx($.div,{className:c(e==null?void 0:e.trigger,a),"data-slot":"popover-trigger",role:"button",...r,children:o})})},E=({children:o,className:a,...r})=>{const{slots:e}=s.use(n);return t.jsx(g,{slot:"title",...r,className:c(e==null?void 0:e.heading,a),children:o})},_=Object.assign(d,{Root:d,Trigger:R,Dialog:P,Arrow:C,Content:b,Heading:E}),A=o=>s.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",width:16,height:16,fill:"none",viewBox:"0 0 16 16"},o),s.createElement("path",{fill:"currentColor",fillRule:"evenodd",d:"M13.488 3.43a.75.75 0 0 1 .081 1.058l-6 7a.75.75 0 0 1-1.1.042l-3.5-3.5A.75.75 0 0 1 4.03 6.97l2.928 2.927 5.473-6.385a.75.75 0 0 1 1.057-.081",clipRule:"evenodd"})),M=o=>s.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",width:16,height:16,fill:"none",viewBox:"0 0 16 16"},o),s.createElement("path",{fill:"currentColor",fillRule:"evenodd",d:"M2.97 5.47a.75.75 0 0 1 1.06 0L8 9.44l3.97-3.97a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06 0l-4.5-4.5a.75.75 0 0 1 0-1.06",clipRule:"evenodd"}));export{M as C,_ as P,A as a};