everything-dev 0.1.3 → 0.1.4
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 +11 -7
- package/src/cli.ts +109 -1
- package/src/components/monitor-view.tsx +471 -0
- package/src/contract.ts +94 -0
- package/src/lib/orchestrator.ts +13 -2
- package/src/lib/resource-monitor/assertions.ts +234 -0
- package/src/lib/resource-monitor/command.ts +283 -0
- package/src/lib/resource-monitor/diff.ts +143 -0
- package/src/lib/resource-monitor/errors.ts +127 -0
- package/src/lib/resource-monitor/index.ts +305 -0
- package/src/lib/resource-monitor/platform/darwin.ts +293 -0
- package/src/lib/resource-monitor/platform/index.ts +35 -0
- package/src/lib/resource-monitor/platform/linux.ts +332 -0
- package/src/lib/resource-monitor/platform/windows.ts +298 -0
- package/src/lib/resource-monitor/snapshot.ts +204 -0
- package/src/lib/resource-monitor/types.ts +74 -0
- package/src/lib/session-recorder/errors.ts +102 -0
- package/src/lib/session-recorder/flows/login.ts +210 -0
- package/src/lib/session-recorder/index.ts +361 -0
- package/src/lib/session-recorder/playwright.ts +257 -0
- package/src/lib/session-recorder/report.ts +353 -0
- package/src/lib/session-recorder/server.ts +267 -0
- package/src/lib/session-recorder/types.ts +115 -0
- package/src/plugin.ts +154 -15
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
import { execShellSafe, powershellSafe } from "../command";
|
|
3
|
+
import type {
|
|
4
|
+
MemoryInfo,
|
|
5
|
+
PlatformOperations,
|
|
6
|
+
PortInfo,
|
|
7
|
+
ProcessInfo,
|
|
8
|
+
} from "../types";
|
|
9
|
+
import { PlatformService } from "../types";
|
|
10
|
+
|
|
11
|
+
const getPortInfo = (
|
|
12
|
+
ports: number[]
|
|
13
|
+
): Effect.Effect<Record<number, PortInfo>, never> =>
|
|
14
|
+
Effect.gen(function* () {
|
|
15
|
+
yield* Effect.logInfo(`[windows] Checking ${ports.length} ports`);
|
|
16
|
+
|
|
17
|
+
const result: Record<number, PortInfo> = {};
|
|
18
|
+
for (const port of ports) {
|
|
19
|
+
result[port] = { port, pid: null, command: null, state: "FREE" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (ports.length === 0) return result;
|
|
23
|
+
|
|
24
|
+
const output = yield* execShellSafe("netstat -ano -p TCP");
|
|
25
|
+
if (!output) {
|
|
26
|
+
yield* Effect.logDebug(
|
|
27
|
+
"[windows] No netstat output, all ports appear free"
|
|
28
|
+
);
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const lines = output.split("\n").filter(Boolean);
|
|
33
|
+
yield* Effect.logDebug(`[windows] Parsing ${lines.length} netstat lines`);
|
|
34
|
+
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
if (!line.includes("LISTENING")) continue;
|
|
37
|
+
|
|
38
|
+
const parts = line.trim().split(/\s+/);
|
|
39
|
+
if (parts.length < 5) continue;
|
|
40
|
+
|
|
41
|
+
const localAddr = parts[1];
|
|
42
|
+
const portMatch = localAddr.match(/:(\d+)$/);
|
|
43
|
+
if (!portMatch) continue;
|
|
44
|
+
|
|
45
|
+
const port = parseInt(portMatch[1], 10);
|
|
46
|
+
if (!ports.includes(port)) continue;
|
|
47
|
+
|
|
48
|
+
const pid = parseInt(parts[4], 10);
|
|
49
|
+
|
|
50
|
+
let command: string | null = null;
|
|
51
|
+
if (pid) {
|
|
52
|
+
const cmdOutput = yield* execShellSafe(
|
|
53
|
+
`wmic process where ProcessId=${pid} get Name /format:list`
|
|
54
|
+
);
|
|
55
|
+
const nameMatch = cmdOutput.match(/Name=(.+)/);
|
|
56
|
+
command = nameMatch ? nameMatch[1].trim() : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
result[port] = {
|
|
60
|
+
port,
|
|
61
|
+
pid,
|
|
62
|
+
command,
|
|
63
|
+
state: "LISTEN",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
yield* Effect.logDebug(
|
|
67
|
+
`[windows] Port :${port} bound to PID ${pid} (${command})`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const boundCount = Object.values(result).filter(
|
|
72
|
+
(p) => p.state === "LISTEN"
|
|
73
|
+
).length;
|
|
74
|
+
yield* Effect.logInfo(
|
|
75
|
+
`[windows] Found ${boundCount}/${ports.length} ports in use`
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const getProcessTree = (
|
|
82
|
+
rootPids: number[]
|
|
83
|
+
): Effect.Effect<ProcessInfo[], never> =>
|
|
84
|
+
Effect.gen(function* () {
|
|
85
|
+
yield* Effect.logInfo(
|
|
86
|
+
`[windows] Building process tree for ${rootPids.length} root PIDs`
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const processes: ProcessInfo[] = [];
|
|
90
|
+
const visited = new Set<number>();
|
|
91
|
+
|
|
92
|
+
const getProcess = (
|
|
93
|
+
pid: number
|
|
94
|
+
): Effect.Effect<ProcessInfo | null, never> =>
|
|
95
|
+
Effect.gen(function* () {
|
|
96
|
+
if (visited.has(pid)) return null;
|
|
97
|
+
visited.add(pid);
|
|
98
|
+
|
|
99
|
+
const output = yield* execShellSafe(
|
|
100
|
+
`wmic process where ProcessId=${pid} get ProcessId,ParentProcessId,WorkingSetSize,Name,CommandLine /format:csv`
|
|
101
|
+
);
|
|
102
|
+
const lines = output
|
|
103
|
+
.split("\n")
|
|
104
|
+
.filter((l) => l.trim() && !l.startsWith("Node"));
|
|
105
|
+
if (lines.length === 0) return null;
|
|
106
|
+
|
|
107
|
+
const parts = lines[0].split(",");
|
|
108
|
+
if (parts.length < 5) return null;
|
|
109
|
+
|
|
110
|
+
const cmdLine = parts[1] || "";
|
|
111
|
+
const name = parts[2] || "";
|
|
112
|
+
const ppid = parseInt(parts[3], 10);
|
|
113
|
+
const procId = parseInt(parts[4], 10);
|
|
114
|
+
const rss = parseInt(parts[5], 10) || 0;
|
|
115
|
+
|
|
116
|
+
yield* Effect.logDebug(
|
|
117
|
+
`[windows] Process ${pid}: ${name} (RSS: ${(rss / 1024).toFixed(0)}KB)`
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
pid: procId,
|
|
122
|
+
ppid,
|
|
123
|
+
command: name,
|
|
124
|
+
args: cmdLine.split(/\s+/).slice(1),
|
|
125
|
+
rss,
|
|
126
|
+
children: [],
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const getChildren = (pid: number): Effect.Effect<number[], never> =>
|
|
131
|
+
Effect.gen(function* () {
|
|
132
|
+
const output = yield* execShellSafe(
|
|
133
|
+
`wmic process where ParentProcessId=${pid} get ProcessId /format:list`
|
|
134
|
+
);
|
|
135
|
+
const matches = output.match(/ProcessId=(\d+)/g);
|
|
136
|
+
if (!matches) return [];
|
|
137
|
+
|
|
138
|
+
const children = matches.map((m) =>
|
|
139
|
+
parseInt(m.replace("ProcessId=", ""), 10)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (children.length > 0) {
|
|
143
|
+
yield* Effect.logDebug(
|
|
144
|
+
`[windows] PID ${pid} has ${children.length} children: ${children.join(", ")}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return children;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const traverse = (pid: number): Effect.Effect<void, never> =>
|
|
152
|
+
Effect.gen(function* () {
|
|
153
|
+
const proc = yield* getProcess(pid);
|
|
154
|
+
if (!proc) return;
|
|
155
|
+
|
|
156
|
+
const children = yield* getChildren(pid);
|
|
157
|
+
proc.children = children;
|
|
158
|
+
processes.push(proc);
|
|
159
|
+
|
|
160
|
+
for (const child of children) {
|
|
161
|
+
yield* traverse(child);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
for (const pid of rootPids) {
|
|
166
|
+
yield* traverse(pid);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
yield* Effect.logInfo(
|
|
170
|
+
`[windows] Process tree contains ${processes.length} processes`
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
return processes;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const getMemoryInfo = (): Effect.Effect<MemoryInfo, never> =>
|
|
177
|
+
Effect.gen(function* () {
|
|
178
|
+
yield* Effect.logDebug("[windows] Getting memory info");
|
|
179
|
+
|
|
180
|
+
const script = `
|
|
181
|
+
$os = Get-CimInstance Win32_OperatingSystem
|
|
182
|
+
$total = $os.TotalVisibleMemorySize * 1024
|
|
183
|
+
$free = $os.FreePhysicalMemory * 1024
|
|
184
|
+
Write-Output "$total,$free"
|
|
185
|
+
`;
|
|
186
|
+
const output = yield* powershellSafe(script);
|
|
187
|
+
|
|
188
|
+
if (!output) {
|
|
189
|
+
yield* Effect.logWarning("[windows] Could not get memory info");
|
|
190
|
+
return { total: 0, used: 0, free: 0, processRss: 0 };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const [totalStr, freeStr] = output.split(",");
|
|
194
|
+
const total = parseInt(totalStr, 10) || 0;
|
|
195
|
+
const free = parseInt(freeStr, 10) || 0;
|
|
196
|
+
|
|
197
|
+
const totalMB = (total / 1024 / 1024).toFixed(0);
|
|
198
|
+
const usedMB = ((total - free) / 1024 / 1024).toFixed(0);
|
|
199
|
+
yield* Effect.logDebug(
|
|
200
|
+
`[windows] Memory: ${usedMB}MB used / ${totalMB}MB total`
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
total,
|
|
205
|
+
used: total - free,
|
|
206
|
+
free,
|
|
207
|
+
processRss: 0,
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const getAllProcesses = (): Effect.Effect<ProcessInfo[], never> =>
|
|
212
|
+
Effect.gen(function* () {
|
|
213
|
+
yield* Effect.logDebug("[windows] Getting all processes");
|
|
214
|
+
|
|
215
|
+
const processes: ProcessInfo[] = [];
|
|
216
|
+
|
|
217
|
+
const output = yield* execShellSafe(
|
|
218
|
+
"wmic process get ProcessId,ParentProcessId,WorkingSetSize,Name /format:csv"
|
|
219
|
+
);
|
|
220
|
+
const lines = output
|
|
221
|
+
.split("\n")
|
|
222
|
+
.filter((l) => l.trim() && !l.startsWith("Node"));
|
|
223
|
+
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
const parts = line.split(",");
|
|
226
|
+
if (parts.length < 4) continue;
|
|
227
|
+
|
|
228
|
+
const name = parts[1] || "";
|
|
229
|
+
const ppid = parseInt(parts[2], 10);
|
|
230
|
+
const pid = parseInt(parts[3], 10);
|
|
231
|
+
const rss = parseInt(parts[4], 10) || 0;
|
|
232
|
+
|
|
233
|
+
if (isNaN(pid)) continue;
|
|
234
|
+
|
|
235
|
+
processes.push({
|
|
236
|
+
pid,
|
|
237
|
+
ppid: isNaN(ppid) ? 0 : ppid,
|
|
238
|
+
command: name,
|
|
239
|
+
args: [],
|
|
240
|
+
rss,
|
|
241
|
+
children: [],
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
yield* Effect.logDebug(
|
|
246
|
+
`[windows] Found ${processes.length} total processes`
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
return processes;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const findChildProcesses = (pid: number): Effect.Effect<number[], never> =>
|
|
253
|
+
Effect.gen(function* () {
|
|
254
|
+
yield* Effect.logDebug(`[windows] Finding all children of PID ${pid}`);
|
|
255
|
+
|
|
256
|
+
const children: number[] = [];
|
|
257
|
+
const visited = new Set<number>();
|
|
258
|
+
|
|
259
|
+
const recurse = (parentPid: number): Effect.Effect<void, never> =>
|
|
260
|
+
Effect.gen(function* () {
|
|
261
|
+
if (visited.has(parentPid)) return;
|
|
262
|
+
visited.add(parentPid);
|
|
263
|
+
|
|
264
|
+
const output = yield* execShellSafe(
|
|
265
|
+
`wmic process where ParentProcessId=${parentPid} get ProcessId /format:list`
|
|
266
|
+
);
|
|
267
|
+
const matches = output.match(/ProcessId=(\d+)/g);
|
|
268
|
+
if (!matches) return;
|
|
269
|
+
|
|
270
|
+
for (const match of matches) {
|
|
271
|
+
const childPid = parseInt(match.replace("ProcessId=", ""), 10);
|
|
272
|
+
if (!isNaN(childPid)) {
|
|
273
|
+
children.push(childPid);
|
|
274
|
+
yield* recurse(childPid);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
yield* recurse(pid);
|
|
280
|
+
|
|
281
|
+
if (children.length > 0) {
|
|
282
|
+
yield* Effect.logDebug(
|
|
283
|
+
`[windows] PID ${pid} has ${children.length} descendants`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return children;
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const windowsOperations: PlatformOperations = {
|
|
291
|
+
getPortInfo,
|
|
292
|
+
getProcessTree,
|
|
293
|
+
getMemoryInfo,
|
|
294
|
+
getAllProcesses,
|
|
295
|
+
findChildProcesses,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export const WindowsLayer = Layer.succeed(PlatformService, windowsOperations);
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { getConfigPath, getPortsFromConfig, loadConfig } from "../../config";
|
|
3
|
+
import { PlatformService, withPlatform } from "./platform";
|
|
4
|
+
import type { MonitorConfig, ProcessInfo, Snapshot } from "./types";
|
|
5
|
+
|
|
6
|
+
export const getPortsToMonitor = (
|
|
7
|
+
config?: MonitorConfig
|
|
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
|
+
});
|
|
29
|
+
|
|
30
|
+
export const getConfigPathSafe = (
|
|
31
|
+
config?: MonitorConfig
|
|
32
|
+
): 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)));
|
|
40
|
+
|
|
41
|
+
export const createSnapshot = (
|
|
42
|
+
config?: MonitorConfig
|
|
43
|
+
): Effect.Effect<Snapshot, never, PlatformService> =>
|
|
44
|
+
Effect.gen(function* () {
|
|
45
|
+
yield* Effect.logInfo("Creating system snapshot");
|
|
46
|
+
|
|
47
|
+
const platform = yield* PlatformService;
|
|
48
|
+
const ports = yield* getPortsToMonitor(config);
|
|
49
|
+
const configPath = yield* getConfigPathSafe(config);
|
|
50
|
+
|
|
51
|
+
yield* Effect.logDebug(`Monitoring ports: ${ports.join(", ")}`);
|
|
52
|
+
|
|
53
|
+
const portInfo = yield* platform.getPortInfo(ports);
|
|
54
|
+
const boundPorts = Object.values(portInfo).filter(
|
|
55
|
+
(p) => p.state !== "FREE"
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
yield* Effect.logInfo(`Found ${boundPorts.length} bound ports`);
|
|
59
|
+
|
|
60
|
+
const rootPids = boundPorts
|
|
61
|
+
.map((p) => p.pid)
|
|
62
|
+
.filter((pid): pid is number => pid !== null);
|
|
63
|
+
|
|
64
|
+
const processes =
|
|
65
|
+
rootPids.length > 0 ? yield* platform.getProcessTree(rootPids) : [];
|
|
66
|
+
|
|
67
|
+
yield* Effect.logInfo(`Tracked ${processes.length} processes in tree`);
|
|
68
|
+
|
|
69
|
+
const memory = yield* platform.getMemoryInfo();
|
|
70
|
+
|
|
71
|
+
const totalRss = processes.reduce((sum, p) => sum + p.rss, 0);
|
|
72
|
+
memory.processRss = totalRss;
|
|
73
|
+
|
|
74
|
+
yield* Effect.logDebug(
|
|
75
|
+
`Total process RSS: ${(totalRss / 1024 / 1024).toFixed(1)}MB`
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const snapshot: Snapshot = {
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
configPath,
|
|
81
|
+
ports: portInfo,
|
|
82
|
+
processes,
|
|
83
|
+
memory,
|
|
84
|
+
platform: process.platform,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
yield* Effect.logInfo(
|
|
88
|
+
`Snapshot created at ${new Date(snapshot.timestamp).toISOString()}`
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return snapshot;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const createSnapshotWithPlatform = (
|
|
95
|
+
config?: MonitorConfig
|
|
96
|
+
): Effect.Effect<Snapshot> => withPlatform(createSnapshot(config));
|
|
97
|
+
|
|
98
|
+
export const findProcessesByPattern = (
|
|
99
|
+
patterns: string[]
|
|
100
|
+
): Effect.Effect<ProcessInfo[], never, PlatformService> =>
|
|
101
|
+
Effect.gen(function* () {
|
|
102
|
+
yield* Effect.logDebug(
|
|
103
|
+
`Finding processes matching: ${patterns.join(", ")}`
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const platform = yield* PlatformService;
|
|
107
|
+
const allProcesses = yield* platform.getAllProcesses();
|
|
108
|
+
|
|
109
|
+
const matched = allProcesses.filter((proc) =>
|
|
110
|
+
patterns.some((pattern) =>
|
|
111
|
+
proc.command.toLowerCase().includes(pattern.toLowerCase())
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
yield* Effect.logInfo(
|
|
116
|
+
`Found ${matched.length} processes matching patterns`
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return matched;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export const findBosProcesses = (): Effect.Effect<
|
|
123
|
+
ProcessInfo[],
|
|
124
|
+
never,
|
|
125
|
+
PlatformService
|
|
126
|
+
> => {
|
|
127
|
+
const patterns = ["bun", "rspack", "rsbuild", "esbuild", "webpack", "node"];
|
|
128
|
+
return findProcessesByPattern(patterns);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const isProcessAliveSync = (pid: number): boolean => {
|
|
132
|
+
try {
|
|
133
|
+
process.kill(pid, 0);
|
|
134
|
+
return true;
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const isProcessAlive = (pid: number): Effect.Effect<boolean> =>
|
|
141
|
+
Effect.sync(() => isProcessAliveSync(pid));
|
|
142
|
+
|
|
143
|
+
export const waitForProcessDeath = (
|
|
144
|
+
pid: number,
|
|
145
|
+
timeoutMs = 5000
|
|
146
|
+
): 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
|
+
});
|
|
168
|
+
|
|
169
|
+
export const waitForPortFree = (
|
|
170
|
+
port: number,
|
|
171
|
+
timeoutMs = 5000
|
|
172
|
+
): 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
|
+
});
|
|
200
|
+
|
|
201
|
+
export const waitForPortFreeWithPlatform = (
|
|
202
|
+
port: number,
|
|
203
|
+
timeoutMs = 5000
|
|
204
|
+
): Effect.Effect<boolean> => withPlatform(waitForPortFree(port, timeoutMs));
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Context, Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
export interface PortInfo {
|
|
4
|
+
port: number;
|
|
5
|
+
pid: number | null;
|
|
6
|
+
command: string | null;
|
|
7
|
+
state: "LISTEN" | "ESTABLISHED" | "TIME_WAIT" | "FREE";
|
|
8
|
+
name?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ProcessInfo {
|
|
12
|
+
pid: number;
|
|
13
|
+
ppid: number;
|
|
14
|
+
command: string;
|
|
15
|
+
args: string[];
|
|
16
|
+
rss: number;
|
|
17
|
+
children: number[];
|
|
18
|
+
startTime?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MemoryInfo {
|
|
22
|
+
total: number;
|
|
23
|
+
used: number;
|
|
24
|
+
free: number;
|
|
25
|
+
processRss: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Snapshot {
|
|
29
|
+
timestamp: number;
|
|
30
|
+
configPath: string | null;
|
|
31
|
+
ports: Record<number, PortInfo>;
|
|
32
|
+
processes: ProcessInfo[];
|
|
33
|
+
memory: MemoryInfo;
|
|
34
|
+
platform: NodeJS.Platform;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SnapshotDiff {
|
|
38
|
+
from: Snapshot;
|
|
39
|
+
to: Snapshot;
|
|
40
|
+
orphanedProcesses: ProcessInfo[];
|
|
41
|
+
stillBoundPorts: PortInfo[];
|
|
42
|
+
freedPorts: number[];
|
|
43
|
+
memoryDeltaBytes: number;
|
|
44
|
+
newProcesses: ProcessInfo[];
|
|
45
|
+
killedProcesses: ProcessInfo[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MonitorConfig {
|
|
49
|
+
ports?: number[];
|
|
50
|
+
processPatterns?: string[];
|
|
51
|
+
refreshInterval?: number;
|
|
52
|
+
configPath?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface PlatformOperations {
|
|
56
|
+
readonly getPortInfo: (
|
|
57
|
+
ports: number[]
|
|
58
|
+
) => Effect.Effect<Record<number, PortInfo>, never>;
|
|
59
|
+
|
|
60
|
+
readonly getProcessTree: (
|
|
61
|
+
rootPids: number[]
|
|
62
|
+
) => Effect.Effect<ProcessInfo[], never>;
|
|
63
|
+
|
|
64
|
+
readonly getMemoryInfo: () => Effect.Effect<MemoryInfo, never>;
|
|
65
|
+
|
|
66
|
+
readonly getAllProcesses: () => Effect.Effect<ProcessInfo[], never>;
|
|
67
|
+
|
|
68
|
+
readonly findChildProcesses: (pid: number) => Effect.Effect<number[], never>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class PlatformService extends Context.Tag("PlatformService")<
|
|
72
|
+
PlatformService,
|
|
73
|
+
PlatformOperations
|
|
74
|
+
>() {}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
2
|
+
|
|
3
|
+
export class SessionTimeout extends Data.TaggedError("SessionTimeout")<{
|
|
4
|
+
readonly timeoutMs: number;
|
|
5
|
+
readonly elapsedMs: number;
|
|
6
|
+
}> {
|
|
7
|
+
get message() {
|
|
8
|
+
return `Session timed out after ${this.elapsedMs}ms (limit: ${this.timeoutMs}ms)`;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class BrowserLaunchFailed extends Data.TaggedError("BrowserLaunchFailed")<{
|
|
13
|
+
readonly reason: string;
|
|
14
|
+
readonly headless: boolean;
|
|
15
|
+
}> {
|
|
16
|
+
get message() {
|
|
17
|
+
return `Failed to launch browser (headless: ${this.headless}): ${this.reason}`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class ServerStartFailed extends Data.TaggedError("ServerStartFailed")<{
|
|
22
|
+
readonly server: string;
|
|
23
|
+
readonly port: number;
|
|
24
|
+
readonly reason: string;
|
|
25
|
+
}> {
|
|
26
|
+
get message() {
|
|
27
|
+
return `Failed to start ${this.server} on port ${this.port}: ${this.reason}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ServerNotReady extends Data.TaggedError("ServerNotReady")<{
|
|
32
|
+
readonly servers: string[];
|
|
33
|
+
readonly timeoutMs: number;
|
|
34
|
+
}> {
|
|
35
|
+
get message() {
|
|
36
|
+
return `Servers not ready after ${this.timeoutMs}ms: ${this.servers.join(", ")}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class FlowExecutionFailed extends Data.TaggedError("FlowExecutionFailed")<{
|
|
41
|
+
readonly flowName: string;
|
|
42
|
+
readonly step: string;
|
|
43
|
+
readonly reason: string;
|
|
44
|
+
}> {
|
|
45
|
+
get message() {
|
|
46
|
+
return `Flow "${this.flowName}" failed at step "${this.step}": ${this.reason}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class SnapshotFailed extends Data.TaggedError("SnapshotFailed")<{
|
|
51
|
+
readonly reason: string;
|
|
52
|
+
}> {
|
|
53
|
+
get message() {
|
|
54
|
+
return `Failed to capture snapshot: ${this.reason}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class ExportFailed extends Data.TaggedError("ExportFailed")<{
|
|
59
|
+
readonly path: string;
|
|
60
|
+
readonly reason: string;
|
|
61
|
+
}> {
|
|
62
|
+
get message() {
|
|
63
|
+
return `Failed to export session to ${this.path}: ${this.reason}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class BrowserMetricsFailed extends Data.TaggedError("BrowserMetricsFailed")<{
|
|
68
|
+
readonly reason: string;
|
|
69
|
+
}> {
|
|
70
|
+
get message() {
|
|
71
|
+
return `Failed to collect browser metrics: ${this.reason}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class PopupNotDetected extends Data.TaggedError("PopupNotDetected")<{
|
|
76
|
+
readonly timeoutMs: number;
|
|
77
|
+
}> {
|
|
78
|
+
get message() {
|
|
79
|
+
return `Popup window not detected within ${this.timeoutMs}ms`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class AuthenticationFailed extends Data.TaggedError("AuthenticationFailed")<{
|
|
84
|
+
readonly step: string;
|
|
85
|
+
readonly reason: string;
|
|
86
|
+
}> {
|
|
87
|
+
get message() {
|
|
88
|
+
return `Authentication failed at "${this.step}": ${this.reason}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type SessionRecorderError =
|
|
93
|
+
| SessionTimeout
|
|
94
|
+
| BrowserLaunchFailed
|
|
95
|
+
| ServerStartFailed
|
|
96
|
+
| ServerNotReady
|
|
97
|
+
| FlowExecutionFailed
|
|
98
|
+
| SnapshotFailed
|
|
99
|
+
| ExportFailed
|
|
100
|
+
| BrowserMetricsFailed
|
|
101
|
+
| PopupNotDetected
|
|
102
|
+
| AuthenticationFailed;
|