everything-dev 0.2.0 → 0.3.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 +80 -79
- package/src/cli.ts +1491 -1198
- package/src/components/monitor-view.tsx +423 -419
- package/src/config.ts +530 -243
- package/src/contract.ts +381 -364
- package/src/lib/env.ts +83 -65
- package/src/lib/nova.ts +207 -195
- package/src/lib/orchestrator.ts +232 -199
- package/src/lib/process-registry.ts +141 -132
- package/src/lib/process.ts +499 -409
- package/src/lib/resource-monitor/diff.ts +27 -9
- package/src/lib/resource-monitor/platform/darwin.ts +31 -18
- package/src/lib/resource-monitor/snapshot.ts +164 -151
- package/src/plugin.ts +2281 -1841
- package/src/types.ts +182 -83
- package/src/ui/head.ts +37 -26
- package/src/utils/banner.ts +7 -9
- package/src/utils/run.ts +27 -16
- package/src/lib/secrets.ts +0 -29
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { PortInfo, Snapshot, SnapshotDiff } from "./types";
|
|
1
|
+
import type { PortInfo, ProcessInfo, Snapshot, SnapshotDiff } from "./types";
|
|
3
2
|
|
|
4
3
|
export const diffSnapshots = (from: Snapshot, to: Snapshot): SnapshotDiff => {
|
|
5
4
|
const fromPids = new Set(from.processes.map((p) => p.pid));
|
|
@@ -21,13 +20,7 @@ export const diffSnapshots = (from: Snapshot, to: Snapshot): SnapshotDiff => {
|
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
const orphanedProcesses = from
|
|
25
|
-
const wasInBaseline = fromPids.has(p.pid);
|
|
26
|
-
const notInAfter = !toPids.has(p.pid);
|
|
27
|
-
const stillAlive = isProcessAliveSync(p.pid);
|
|
28
|
-
|
|
29
|
-
return wasInBaseline && notInAfter && stillAlive;
|
|
30
|
-
});
|
|
23
|
+
const orphanedProcesses = findOrphanedProcesses(from, to, fromPids, toPids);
|
|
31
24
|
|
|
32
25
|
const newProcesses = to.processes.filter((p) => !fromPids.has(p.pid));
|
|
33
26
|
const killedProcesses = from.processes.filter((p) => !toPids.has(p.pid));
|
|
@@ -46,6 +39,31 @@ export const diffSnapshots = (from: Snapshot, to: Snapshot): SnapshotDiff => {
|
|
|
46
39
|
};
|
|
47
40
|
};
|
|
48
41
|
|
|
42
|
+
const findOrphanedProcesses = (
|
|
43
|
+
from: Snapshot,
|
|
44
|
+
to: Snapshot,
|
|
45
|
+
fromPids: Set<number>,
|
|
46
|
+
toPids: Set<number>
|
|
47
|
+
): ProcessInfo[] => {
|
|
48
|
+
const orphaned: ProcessInfo[] = [];
|
|
49
|
+
|
|
50
|
+
for (const toProc of to.processes) {
|
|
51
|
+
const parentPid = toProc.ppid;
|
|
52
|
+
|
|
53
|
+
if (parentPid <= 1) continue;
|
|
54
|
+
|
|
55
|
+
const parentWasTracked = fromPids.has(parentPid);
|
|
56
|
+
const parentIsGone = !toPids.has(parentPid);
|
|
57
|
+
const childStillAlive = toPids.has(toProc.pid);
|
|
58
|
+
|
|
59
|
+
if (parentWasTracked && parentIsGone && childStillAlive) {
|
|
60
|
+
orphaned.push(toProc);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return orphaned;
|
|
65
|
+
};
|
|
66
|
+
|
|
49
67
|
export const hasLeaks = (diff: SnapshotDiff): boolean => {
|
|
50
68
|
return diff.orphanedProcesses.length > 0 || diff.stillBoundPorts.length > 0;
|
|
51
69
|
};
|
|
@@ -172,46 +172,59 @@ const getMemoryInfo = (): Effect.Effect<MemoryInfo, never> =>
|
|
|
172
172
|
Effect.gen(function* () {
|
|
173
173
|
yield* Effect.logDebug("[darwin] Getting memory info");
|
|
174
174
|
|
|
175
|
+
const sysctlMem = yield* execShellSafe(
|
|
176
|
+
"sysctl -n hw.memsize 2>/dev/null || echo 0"
|
|
177
|
+
);
|
|
178
|
+
const total = parseInt(sysctlMem, 10) || 16 * 1024 * 1024 * 1024;
|
|
179
|
+
|
|
180
|
+
const pageSizeOutput = yield* execShellSafe(
|
|
181
|
+
"sysctl -n hw.pagesize 2>/dev/null || echo 16384"
|
|
182
|
+
);
|
|
183
|
+
const pageSize = parseInt(pageSizeOutput.trim(), 10) || 16384;
|
|
184
|
+
|
|
175
185
|
const vmStat = yield* execShellSafe("vm_stat 2>/dev/null || true");
|
|
176
|
-
const pageSize = 16384;
|
|
177
186
|
|
|
178
|
-
let
|
|
179
|
-
let
|
|
180
|
-
let
|
|
181
|
-
let
|
|
182
|
-
let
|
|
187
|
+
let freePages = 0;
|
|
188
|
+
let activePages = 0;
|
|
189
|
+
let inactivePages = 0;
|
|
190
|
+
let wiredPages = 0;
|
|
191
|
+
let speculativePages = 0;
|
|
183
192
|
|
|
184
193
|
for (const line of vmStat.split("\n")) {
|
|
185
|
-
const match = line.match(/^(
|
|
194
|
+
const match = line.match(/^(.+?):\s+(\d+)/);
|
|
186
195
|
if (!match) continue;
|
|
187
196
|
|
|
188
197
|
const [, key, value] = match;
|
|
189
198
|
const pages = parseInt(value, 10);
|
|
190
199
|
|
|
191
|
-
if (key.includes("free"))
|
|
192
|
-
else if (key.includes("active"))
|
|
193
|
-
else if (key.includes("inactive"))
|
|
194
|
-
else if (key.includes("wired"))
|
|
195
|
-
else if (key.includes("speculative"))
|
|
200
|
+
if (key.includes("Pages free")) freePages = pages;
|
|
201
|
+
else if (key.includes("Pages active")) activePages = pages;
|
|
202
|
+
else if (key.includes("Pages inactive")) inactivePages = pages;
|
|
203
|
+
else if (key.includes("Pages wired")) wiredPages = pages;
|
|
204
|
+
else if (key.includes("Pages speculative")) speculativePages = pages;
|
|
196
205
|
}
|
|
197
206
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
207
|
+
const free = freePages * pageSize;
|
|
208
|
+
const active = activePages * pageSize;
|
|
209
|
+
const inactive = inactivePages * pageSize;
|
|
210
|
+
const wired = wiredPages * pageSize;
|
|
211
|
+
const speculative = speculativePages * pageSize;
|
|
202
212
|
|
|
203
213
|
const used = active + inactive + wired + speculative;
|
|
204
214
|
|
|
215
|
+
const effectiveFree = free > 0 ? free : Math.max(0, total - used);
|
|
216
|
+
|
|
205
217
|
const totalMB = (total / 1024 / 1024).toFixed(0);
|
|
206
218
|
const usedMB = (used / 1024 / 1024).toFixed(0);
|
|
219
|
+
const freeMB = (effectiveFree / 1024 / 1024).toFixed(0);
|
|
207
220
|
yield* Effect.logDebug(
|
|
208
|
-
`[darwin] Memory: ${usedMB}MB used / ${totalMB}MB total`
|
|
221
|
+
`[darwin] Memory: ${usedMB}MB used / ${freeMB}MB free / ${totalMB}MB total`
|
|
209
222
|
);
|
|
210
223
|
|
|
211
224
|
return {
|
|
212
225
|
total,
|
|
213
226
|
used,
|
|
214
|
-
free:
|
|
227
|
+
free: effectiveFree,
|
|
215
228
|
processRss: 0,
|
|
216
229
|
};
|
|
217
230
|
});
|
|
@@ -1,204 +1,217 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import {
|
|
2
|
+
import { loadConfig } from "../../config";
|
|
3
3
|
import { PlatformService, withPlatform } from "./platform";
|
|
4
4
|
import type { MonitorConfig, ProcessInfo, Snapshot } from "./types";
|
|
5
5
|
|
|
6
6
|
export const getPortsToMonitor = (
|
|
7
|
-
|
|
7
|
+
config?: MonitorConfig,
|
|
8
8
|
): Effect.Effect<number[]> =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
9
|
+
Effect.gen(function* () {
|
|
10
|
+
if (config?.ports && config.ports.length > 0) {
|
|
11
|
+
yield* Effect.logDebug(
|
|
12
|
+
`Using configured ports: ${config.ports.join(", ")}`,
|
|
13
|
+
);
|
|
14
|
+
return config.ports;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Load config and get ports from runtime config
|
|
18
|
+
const result = yield* Effect.tryPromise({
|
|
19
|
+
try: () => loadConfig({ path: config?.configPath }),
|
|
20
|
+
catch: () => new Error("Config not found"),
|
|
21
|
+
}).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
22
|
+
|
|
23
|
+
if (result?.runtime) {
|
|
24
|
+
const ports = [
|
|
25
|
+
result.runtime.hostUrl,
|
|
26
|
+
result.runtime.ui.url,
|
|
27
|
+
result.runtime.api.url,
|
|
28
|
+
]
|
|
29
|
+
.map((url) => {
|
|
30
|
+
try {
|
|
31
|
+
return parseInt(new URL(url).port || "0");
|
|
32
|
+
} catch {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
.filter((p) => p > 0);
|
|
37
|
+
return ports;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return [3000, 3002, 3014];
|
|
41
|
+
});
|
|
29
42
|
|
|
30
43
|
export const getConfigPathSafe = (
|
|
31
|
-
|
|
44
|
+
config?: MonitorConfig,
|
|
32
45
|
): Effect.Effect<string | null> =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
Effect.tryPromise({
|
|
47
|
+
try: async () => {
|
|
48
|
+
const result = await loadConfig({ path: config?.configPath });
|
|
49
|
+
return result?.source.path ?? null;
|
|
50
|
+
},
|
|
51
|
+
catch: () => new Error("Config not found"),
|
|
52
|
+
}).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
40
53
|
|
|
41
54
|
export const createSnapshot = (
|
|
42
|
-
|
|
55
|
+
config?: MonitorConfig,
|
|
43
56
|
): Effect.Effect<Snapshot, never, PlatformService> =>
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
Effect.gen(function* () {
|
|
58
|
+
yield* Effect.logInfo("Creating system snapshot");
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
const platform = yield* PlatformService;
|
|
61
|
+
const ports = yield* getPortsToMonitor(config);
|
|
62
|
+
const configPath = yield* getConfigPathSafe(config);
|
|
50
63
|
|
|
51
|
-
|
|
64
|
+
yield* Effect.logDebug(`Monitoring ports: ${ports.join(", ")}`);
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
const portInfo = yield* platform.getPortInfo(ports);
|
|
67
|
+
const boundPorts = Object.values(portInfo).filter(
|
|
68
|
+
(p) => p.state !== "FREE",
|
|
69
|
+
);
|
|
57
70
|
|
|
58
|
-
|
|
71
|
+
yield* Effect.logInfo(`Found ${boundPorts.length} bound ports`);
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
const rootPids = boundPorts
|
|
74
|
+
.map((p) => p.pid)
|
|
75
|
+
.filter((pid): pid is number => pid !== null);
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
const processes =
|
|
78
|
+
rootPids.length > 0 ? yield* platform.getProcessTree(rootPids) : [];
|
|
66
79
|
|
|
67
|
-
|
|
80
|
+
yield* Effect.logInfo(`Tracked ${processes.length} processes in tree`);
|
|
68
81
|
|
|
69
|
-
|
|
82
|
+
const memory = yield* platform.getMemoryInfo();
|
|
70
83
|
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
const totalRss = processes.reduce((sum, p) => sum + p.rss, 0);
|
|
85
|
+
memory.processRss = totalRss;
|
|
73
86
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
yield* Effect.logDebug(
|
|
88
|
+
`Total process RSS: ${(totalRss / 1024 / 1024).toFixed(1)}MB`,
|
|
89
|
+
);
|
|
77
90
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
const snapshot: Snapshot = {
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
configPath,
|
|
94
|
+
ports: portInfo,
|
|
95
|
+
processes,
|
|
96
|
+
memory,
|
|
97
|
+
platform: process.platform,
|
|
98
|
+
};
|
|
86
99
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
yield* Effect.logInfo(
|
|
101
|
+
`Snapshot created at ${new Date(snapshot.timestamp).toISOString()}`,
|
|
102
|
+
);
|
|
90
103
|
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
return snapshot;
|
|
105
|
+
});
|
|
93
106
|
|
|
94
107
|
export const createSnapshotWithPlatform = (
|
|
95
|
-
|
|
108
|
+
config?: MonitorConfig,
|
|
96
109
|
): Effect.Effect<Snapshot> => withPlatform(createSnapshot(config));
|
|
97
110
|
|
|
98
111
|
export const findProcessesByPattern = (
|
|
99
|
-
|
|
112
|
+
patterns: string[],
|
|
100
113
|
): Effect.Effect<ProcessInfo[], never, PlatformService> =>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
Effect.gen(function* () {
|
|
115
|
+
yield* Effect.logDebug(
|
|
116
|
+
`Finding processes matching: ${patterns.join(", ")}`,
|
|
117
|
+
);
|
|
105
118
|
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
const platform = yield* PlatformService;
|
|
120
|
+
const allProcesses = yield* platform.getAllProcesses();
|
|
108
121
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
const matched = allProcesses.filter((proc) =>
|
|
123
|
+
patterns.some((pattern) =>
|
|
124
|
+
proc.command.toLowerCase().includes(pattern.toLowerCase()),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
114
127
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
128
|
+
yield* Effect.logInfo(
|
|
129
|
+
`Found ${matched.length} processes matching patterns`,
|
|
130
|
+
);
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
132
|
+
return matched;
|
|
133
|
+
});
|
|
121
134
|
|
|
122
135
|
export const findBosProcesses = (): Effect.Effect<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
ProcessInfo[],
|
|
137
|
+
never,
|
|
138
|
+
PlatformService
|
|
126
139
|
> => {
|
|
127
|
-
|
|
128
|
-
|
|
140
|
+
const patterns = ["bun", "rspack", "rsbuild", "esbuild", "webpack", "node"];
|
|
141
|
+
return findProcessesByPattern(patterns);
|
|
129
142
|
};
|
|
130
143
|
|
|
131
144
|
export const isProcessAliveSync = (pid: number): boolean => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
try {
|
|
146
|
+
process.kill(pid, 0);
|
|
147
|
+
return true;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
138
151
|
};
|
|
139
152
|
|
|
140
153
|
export const isProcessAlive = (pid: number): Effect.Effect<boolean> =>
|
|
141
|
-
|
|
154
|
+
Effect.sync(() => isProcessAliveSync(pid));
|
|
142
155
|
|
|
143
156
|
export const waitForProcessDeath = (
|
|
144
|
-
|
|
145
|
-
|
|
157
|
+
pid: number,
|
|
158
|
+
timeoutMs = 5000,
|
|
146
159
|
): Effect.Effect<boolean> =>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
160
|
+
Effect.gen(function* () {
|
|
161
|
+
yield* Effect.logDebug(
|
|
162
|
+
`Waiting for PID ${pid} to die (timeout: ${timeoutMs}ms)`,
|
|
163
|
+
);
|
|
164
|
+
const start = Date.now();
|
|
165
|
+
|
|
166
|
+
while (Date.now() - start < timeoutMs) {
|
|
167
|
+
const alive = yield* isProcessAlive(pid);
|
|
168
|
+
if (!alive) {
|
|
169
|
+
yield* Effect.logDebug(`PID ${pid} is dead`);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
yield* Effect.sleep("100 millis");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const finalAlive = yield* isProcessAlive(pid);
|
|
176
|
+
if (finalAlive) {
|
|
177
|
+
yield* Effect.logWarning(`PID ${pid} still alive after ${timeoutMs}ms`);
|
|
178
|
+
}
|
|
179
|
+
return !finalAlive;
|
|
180
|
+
});
|
|
168
181
|
|
|
169
182
|
export const waitForPortFree = (
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
port: number,
|
|
184
|
+
timeoutMs = 5000,
|
|
172
185
|
): Effect.Effect<boolean, never, PlatformService> =>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
186
|
+
Effect.gen(function* () {
|
|
187
|
+
yield* Effect.logDebug(
|
|
188
|
+
`Waiting for port :${port} to be free (timeout: ${timeoutMs}ms)`,
|
|
189
|
+
);
|
|
190
|
+
const platform = yield* PlatformService;
|
|
191
|
+
const start = Date.now();
|
|
192
|
+
|
|
193
|
+
while (Date.now() - start < timeoutMs) {
|
|
194
|
+
const portInfo = yield* platform.getPortInfo([port]);
|
|
195
|
+
if (portInfo[port].state === "FREE") {
|
|
196
|
+
yield* Effect.logDebug(`Port :${port} is now free`);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
yield* Effect.sleep("100 millis");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const finalPortInfo = yield* platform.getPortInfo([port]);
|
|
203
|
+
const isFree = finalPortInfo[port].state === "FREE";
|
|
204
|
+
|
|
205
|
+
if (!isFree) {
|
|
206
|
+
yield* Effect.logWarning(
|
|
207
|
+
`Port :${port} still bound after ${timeoutMs}ms`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return isFree;
|
|
212
|
+
});
|
|
200
213
|
|
|
201
214
|
export const waitForPortFreeWithPlatform = (
|
|
202
|
-
|
|
203
|
-
|
|
215
|
+
port: number,
|
|
216
|
+
timeoutMs = 5000,
|
|
204
217
|
): Effect.Effect<boolean> => withPlatform(waitForPortFree(port, timeoutMs));
|