everything-dev 0.2.1 → 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.
@@ -1,5 +1,4 @@
1
- import { isProcessAliveSync } from "./snapshot";
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.processes.filter((p) => {
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 free = 0;
179
- let active = 0;
180
- let inactive = 0;
181
- let wired = 0;
182
- let speculative = 0;
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(/^(.+):\s+(\d+)/);
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")) free = pages * pageSize;
192
- else if (key.includes("active")) active = pages * pageSize;
193
- else if (key.includes("inactive")) inactive = pages * pageSize;
194
- else if (key.includes("wired")) wired = pages * pageSize;
195
- else if (key.includes("speculative")) speculative = pages * pageSize;
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 sysctlMem = yield* execShellSafe(
199
- "sysctl -n hw.memsize 2>/dev/null || echo 0"
200
- );
201
- const total = parseInt(sysctlMem, 10) || 16 * 1024 * 1024 * 1024;
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: Math.max(0, total - used),
227
+ free: effectiveFree,
215
228
  processRss: 0,
216
229
  };
217
230
  });
@@ -1,204 +1,217 @@
1
1
  import { Effect } from "effect";
2
- import { getConfigPath, getPortsFromConfig, loadConfig } from "../../config";
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
- config?: MonitorConfig
7
+ config?: MonitorConfig,
8
8
  ): Effect.Effect<number[]> =>
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
- return yield* Effect.try({
18
- try: () => {
19
- loadConfig(config?.configPath);
20
- const portConfig = getPortsFromConfig();
21
- const ports = [portConfig.host, portConfig.ui, portConfig.api].filter(
22
- (p) => p > 0
23
- );
24
- return ports;
25
- },
26
- catch: () => new Error("Config not found"),
27
- }).pipe(Effect.catchAll(() => Effect.succeed([3000, 3002, 3014])));
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
- config?: MonitorConfig
44
+ config?: MonitorConfig,
32
45
  ): Effect.Effect<string | null> =>
33
- Effect.try({
34
- try: () => {
35
- loadConfig(config?.configPath);
36
- return getConfigPath();
37
- },
38
- catch: () => new Error("Config not found"),
39
- }).pipe(Effect.catchAll(() => Effect.succeed(null)));
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
- config?: MonitorConfig
55
+ config?: MonitorConfig,
43
56
  ): Effect.Effect<Snapshot, never, PlatformService> =>
44
- Effect.gen(function* () {
45
- yield* Effect.logInfo("Creating system snapshot");
57
+ Effect.gen(function* () {
58
+ yield* Effect.logInfo("Creating system snapshot");
46
59
 
47
- const platform = yield* PlatformService;
48
- const ports = yield* getPortsToMonitor(config);
49
- const configPath = yield* getConfigPathSafe(config);
60
+ const platform = yield* PlatformService;
61
+ const ports = yield* getPortsToMonitor(config);
62
+ const configPath = yield* getConfigPathSafe(config);
50
63
 
51
- yield* Effect.logDebug(`Monitoring ports: ${ports.join(", ")}`);
64
+ yield* Effect.logDebug(`Monitoring ports: ${ports.join(", ")}`);
52
65
 
53
- const portInfo = yield* platform.getPortInfo(ports);
54
- const boundPorts = Object.values(portInfo).filter(
55
- (p) => p.state !== "FREE"
56
- );
66
+ const portInfo = yield* platform.getPortInfo(ports);
67
+ const boundPorts = Object.values(portInfo).filter(
68
+ (p) => p.state !== "FREE",
69
+ );
57
70
 
58
- yield* Effect.logInfo(`Found ${boundPorts.length} bound ports`);
71
+ yield* Effect.logInfo(`Found ${boundPorts.length} bound ports`);
59
72
 
60
- const rootPids = boundPorts
61
- .map((p) => p.pid)
62
- .filter((pid): pid is number => pid !== null);
73
+ const rootPids = boundPorts
74
+ .map((p) => p.pid)
75
+ .filter((pid): pid is number => pid !== null);
63
76
 
64
- const processes =
65
- rootPids.length > 0 ? yield* platform.getProcessTree(rootPids) : [];
77
+ const processes =
78
+ rootPids.length > 0 ? yield* platform.getProcessTree(rootPids) : [];
66
79
 
67
- yield* Effect.logInfo(`Tracked ${processes.length} processes in tree`);
80
+ yield* Effect.logInfo(`Tracked ${processes.length} processes in tree`);
68
81
 
69
- const memory = yield* platform.getMemoryInfo();
82
+ const memory = yield* platform.getMemoryInfo();
70
83
 
71
- const totalRss = processes.reduce((sum, p) => sum + p.rss, 0);
72
- memory.processRss = totalRss;
84
+ const totalRss = processes.reduce((sum, p) => sum + p.rss, 0);
85
+ memory.processRss = totalRss;
73
86
 
74
- yield* Effect.logDebug(
75
- `Total process RSS: ${(totalRss / 1024 / 1024).toFixed(1)}MB`
76
- );
87
+ yield* Effect.logDebug(
88
+ `Total process RSS: ${(totalRss / 1024 / 1024).toFixed(1)}MB`,
89
+ );
77
90
 
78
- const snapshot: Snapshot = {
79
- timestamp: Date.now(),
80
- configPath,
81
- ports: portInfo,
82
- processes,
83
- memory,
84
- platform: process.platform,
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
- yield* Effect.logInfo(
88
- `Snapshot created at ${new Date(snapshot.timestamp).toISOString()}`
89
- );
100
+ yield* Effect.logInfo(
101
+ `Snapshot created at ${new Date(snapshot.timestamp).toISOString()}`,
102
+ );
90
103
 
91
- return snapshot;
92
- });
104
+ return snapshot;
105
+ });
93
106
 
94
107
  export const createSnapshotWithPlatform = (
95
- config?: MonitorConfig
108
+ config?: MonitorConfig,
96
109
  ): Effect.Effect<Snapshot> => withPlatform(createSnapshot(config));
97
110
 
98
111
  export const findProcessesByPattern = (
99
- patterns: string[]
112
+ patterns: string[],
100
113
  ): Effect.Effect<ProcessInfo[], never, PlatformService> =>
101
- Effect.gen(function* () {
102
- yield* Effect.logDebug(
103
- `Finding processes matching: ${patterns.join(", ")}`
104
- );
114
+ Effect.gen(function* () {
115
+ yield* Effect.logDebug(
116
+ `Finding processes matching: ${patterns.join(", ")}`,
117
+ );
105
118
 
106
- const platform = yield* PlatformService;
107
- const allProcesses = yield* platform.getAllProcesses();
119
+ const platform = yield* PlatformService;
120
+ const allProcesses = yield* platform.getAllProcesses();
108
121
 
109
- const matched = allProcesses.filter((proc) =>
110
- patterns.some((pattern) =>
111
- proc.command.toLowerCase().includes(pattern.toLowerCase())
112
- )
113
- );
122
+ const matched = allProcesses.filter((proc) =>
123
+ patterns.some((pattern) =>
124
+ proc.command.toLowerCase().includes(pattern.toLowerCase()),
125
+ ),
126
+ );
114
127
 
115
- yield* Effect.logInfo(
116
- `Found ${matched.length} processes matching patterns`
117
- );
128
+ yield* Effect.logInfo(
129
+ `Found ${matched.length} processes matching patterns`,
130
+ );
118
131
 
119
- return matched;
120
- });
132
+ return matched;
133
+ });
121
134
 
122
135
  export const findBosProcesses = (): Effect.Effect<
123
- ProcessInfo[],
124
- never,
125
- PlatformService
136
+ ProcessInfo[],
137
+ never,
138
+ PlatformService
126
139
  > => {
127
- const patterns = ["bun", "rspack", "rsbuild", "esbuild", "webpack", "node"];
128
- return findProcessesByPattern(patterns);
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
- try {
133
- process.kill(pid, 0);
134
- return true;
135
- } catch {
136
- return false;
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
- Effect.sync(() => isProcessAliveSync(pid));
154
+ Effect.sync(() => isProcessAliveSync(pid));
142
155
 
143
156
  export const waitForProcessDeath = (
144
- pid: number,
145
- timeoutMs = 5000
157
+ pid: number,
158
+ timeoutMs = 5000,
146
159
  ): Effect.Effect<boolean> =>
147
- Effect.gen(function* () {
148
- yield* Effect.logDebug(
149
- `Waiting for PID ${pid} to die (timeout: ${timeoutMs}ms)`
150
- );
151
- const start = Date.now();
152
-
153
- while (Date.now() - start < timeoutMs) {
154
- const alive = yield* isProcessAlive(pid);
155
- if (!alive) {
156
- yield* Effect.logDebug(`PID ${pid} is dead`);
157
- return true;
158
- }
159
- yield* Effect.sleep("100 millis");
160
- }
161
-
162
- const finalAlive = yield* isProcessAlive(pid);
163
- if (finalAlive) {
164
- yield* Effect.logWarning(`PID ${pid} still alive after ${timeoutMs}ms`);
165
- }
166
- return !finalAlive;
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
- port: number,
171
- timeoutMs = 5000
183
+ port: number,
184
+ timeoutMs = 5000,
172
185
  ): Effect.Effect<boolean, never, PlatformService> =>
173
- Effect.gen(function* () {
174
- yield* Effect.logDebug(
175
- `Waiting for port :${port} to be free (timeout: ${timeoutMs}ms)`
176
- );
177
- const platform = yield* PlatformService;
178
- const start = Date.now();
179
-
180
- while (Date.now() - start < timeoutMs) {
181
- const portInfo = yield* platform.getPortInfo([port]);
182
- if (portInfo[port].state === "FREE") {
183
- yield* Effect.logDebug(`Port :${port} is now free`);
184
- return true;
185
- }
186
- yield* Effect.sleep("100 millis");
187
- }
188
-
189
- const finalPortInfo = yield* platform.getPortInfo([port]);
190
- const isFree = finalPortInfo[port].state === "FREE";
191
-
192
- if (!isFree) {
193
- yield* Effect.logWarning(
194
- `Port :${port} still bound after ${timeoutMs}ms`
195
- );
196
- }
197
-
198
- return isFree;
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
- port: number,
203
- timeoutMs = 5000
215
+ port: number,
216
+ timeoutMs = 5000,
204
217
  ): Effect.Effect<boolean> => withPlatform(waitForPortFree(port, timeoutMs));