@leynier/ccst 0.4.0 → 0.5.0
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/package.json +1 -1
- package/src/commands/ccs/start.ts +11 -14
- package/src/commands/ccs/stop.ts +18 -20
- package/src/utils/daemon.ts +70 -0
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getLogPath,
|
|
8
8
|
getRunningDaemonPid,
|
|
9
9
|
isProcessRunning,
|
|
10
|
+
killProcessTree,
|
|
10
11
|
writePid,
|
|
11
12
|
} from "../../utils/daemon.js";
|
|
12
13
|
|
|
@@ -29,19 +30,15 @@ export const ccsStartCommand = async (
|
|
|
29
30
|
// If force and running, stop first
|
|
30
31
|
if (existingPid !== null && options?.force) {
|
|
31
32
|
console.log(pc.dim(`Stopping existing daemon (PID: ${existingPid})...`));
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
break;
|
|
40
|
-
}
|
|
41
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
33
|
+
await killProcessTree(existingPid, true);
|
|
34
|
+
// Wait for process to terminate
|
|
35
|
+
const maxWait = 3000;
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
while (Date.now() - startTime < maxWait) {
|
|
38
|
+
if (!isProcessRunning(existingPid)) {
|
|
39
|
+
break;
|
|
42
40
|
}
|
|
43
|
-
|
|
44
|
-
// Process may have already exited
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
45
42
|
}
|
|
46
43
|
}
|
|
47
44
|
ensureDaemonDir();
|
|
@@ -53,8 +50,8 @@ export const ccsStartCommand = async (
|
|
|
53
50
|
const child = spawn(ccsPath, ["config"], {
|
|
54
51
|
detached: true,
|
|
55
52
|
stdio: ["ignore", logFd, logFd],
|
|
56
|
-
// On Windows, need shell: true for proper detachment
|
|
57
|
-
...(process.platform === "win32" ? { shell: true } : {}),
|
|
53
|
+
// On Windows, need shell: true for proper detachment and windowsHide to hide console
|
|
54
|
+
...(process.platform === "win32" ? { shell: true, windowsHide: true } : {}),
|
|
58
55
|
});
|
|
59
56
|
if (!child.pid) {
|
|
60
57
|
console.log(pc.red("Failed to start CCS config daemon"));
|
package/src/commands/ccs/stop.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import {
|
|
3
|
+
CCS_PORTS,
|
|
3
4
|
getRunningDaemonPid,
|
|
4
5
|
isProcessRunning,
|
|
6
|
+
killProcessByPort,
|
|
7
|
+
killProcessTree,
|
|
5
8
|
removePid,
|
|
6
9
|
} from "../../utils/daemon.js";
|
|
7
10
|
|
|
@@ -11,14 +14,10 @@ export type StopOptions = {
|
|
|
11
14
|
|
|
12
15
|
export const ccsStopCommand = async (options?: StopOptions): Promise<void> => {
|
|
13
16
|
const pid = await getRunningDaemonPid();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
// Send SIGTERM for graceful shutdown, SIGKILL if --force
|
|
20
|
-
const signal = options?.force ? "SIGKILL" : "SIGTERM";
|
|
21
|
-
process.kill(pid, signal);
|
|
17
|
+
let stopped = false;
|
|
18
|
+
// Phase 1: Kill by PID if exists
|
|
19
|
+
if (pid !== null) {
|
|
20
|
+
await killProcessTree(pid, options?.force);
|
|
22
21
|
// Wait for process to terminate (with timeout)
|
|
23
22
|
const maxWait = options?.force ? 1000 : 5000;
|
|
24
23
|
const startTime = Date.now();
|
|
@@ -30,18 +29,17 @@ export const ccsStopCommand = async (options?: StopOptions): Promise<void> => {
|
|
|
30
29
|
}
|
|
31
30
|
removePid();
|
|
32
31
|
console.log(pc.green(`CCS config daemon stopped (PID: ${pid})`));
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
pc.red("Permission denied. Try running with elevated privileges."),
|
|
42
|
-
);
|
|
43
|
-
} else {
|
|
44
|
-
console.log(pc.red(`Failed to stop daemon: ${err.message}`));
|
|
32
|
+
stopped = true;
|
|
33
|
+
}
|
|
34
|
+
// Phase 2: Kill processes by port (especially important on Windows)
|
|
35
|
+
for (const port of CCS_PORTS) {
|
|
36
|
+
const killed = await killProcessByPort(port, options?.force ?? true);
|
|
37
|
+
if (killed) {
|
|
38
|
+
console.log(pc.dim(`Cleaned up process on port ${port}`));
|
|
39
|
+
stopped = true;
|
|
45
40
|
}
|
|
46
41
|
}
|
|
42
|
+
if (!stopped) {
|
|
43
|
+
console.log(pc.yellow("CCS config daemon is not running"));
|
|
44
|
+
}
|
|
47
45
|
};
|
package/src/utils/daemon.ts
CHANGED
|
@@ -81,3 +81,73 @@ export const getCcsExecutable = (): string => {
|
|
|
81
81
|
// Use 'ccs' from PATH - it should be globally installed
|
|
82
82
|
return "ccs";
|
|
83
83
|
};
|
|
84
|
+
|
|
85
|
+
// Known CCS daemon ports
|
|
86
|
+
export const CCS_PORTS = [3000, 8317];
|
|
87
|
+
|
|
88
|
+
// Kill process tree (on Windows, kills all child processes)
|
|
89
|
+
export const killProcessTree = async (
|
|
90
|
+
pid: number,
|
|
91
|
+
force?: boolean,
|
|
92
|
+
): Promise<boolean> => {
|
|
93
|
+
if (process.platform === "win32") {
|
|
94
|
+
const args = ["/PID", String(pid), "/T"];
|
|
95
|
+
if (force) args.push("/F");
|
|
96
|
+
const proc = Bun.spawn(["taskkill", ...args], {
|
|
97
|
+
stdout: "ignore",
|
|
98
|
+
stderr: "ignore",
|
|
99
|
+
});
|
|
100
|
+
await proc.exited;
|
|
101
|
+
return proc.exitCode === 0;
|
|
102
|
+
}
|
|
103
|
+
const signal = force ? "SIGKILL" : "SIGTERM";
|
|
104
|
+
try {
|
|
105
|
+
process.kill(pid, signal);
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Get PID of process listening on a port
|
|
113
|
+
export const getProcessByPort = async (
|
|
114
|
+
port: number,
|
|
115
|
+
): Promise<number | null> => {
|
|
116
|
+
if (process.platform === "win32") {
|
|
117
|
+
const proc = Bun.spawn(["cmd", "/c", `netstat -ano | findstr :${port}`], {
|
|
118
|
+
stdout: "pipe",
|
|
119
|
+
stderr: "ignore",
|
|
120
|
+
});
|
|
121
|
+
const output = await new Response(proc.stdout).text();
|
|
122
|
+
await proc.exited;
|
|
123
|
+
const lines = output.trim().split("\n");
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
if (line.includes("LISTENING") || line.includes("ESTABLISHED")) {
|
|
126
|
+
const parts = line.trim().split(/\s+/);
|
|
127
|
+
const pid = Number.parseInt(parts[parts.length - 1], 10);
|
|
128
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
129
|
+
return pid;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const proc = Bun.spawn(["lsof", "-ti", `:${port}`], {
|
|
136
|
+
stdout: "pipe",
|
|
137
|
+
stderr: "ignore",
|
|
138
|
+
});
|
|
139
|
+
const output = await new Response(proc.stdout).text();
|
|
140
|
+
await proc.exited;
|
|
141
|
+
const pid = Number.parseInt(output.trim(), 10);
|
|
142
|
+
return Number.isFinite(pid) ? pid : null;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Kill process by port
|
|
146
|
+
export const killProcessByPort = async (
|
|
147
|
+
port: number,
|
|
148
|
+
force?: boolean,
|
|
149
|
+
): Promise<boolean> => {
|
|
150
|
+
const pid = await getProcessByPort(port);
|
|
151
|
+
if (pid === null) return false;
|
|
152
|
+
return killProcessTree(pid, force);
|
|
153
|
+
};
|