stim 0.0.0 → 1.0.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/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/android-cas-compiler.d.mts +1 -0
- package/dist/android-cas-compiler.mjs +71 -0
- package/dist/android-jc8MnUyL.mjs +514 -0
- package/dist/cache-manifest-4tH64LQ9.mjs +66 -0
- package/dist/cache-manifest.d.mts +27 -0
- package/dist/cache-manifest.mjs +2 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +27182 -0
- package/dist/collector-run.d.mts +119 -0
- package/dist/collector-run.mjs +539 -0
- package/dist/config-7kmtuhO2.mjs +350 -0
- package/dist/errors-B6W7FkxQ.mjs +15 -0
- package/dist/exec-bsN9MJXb.mjs +63 -0
- package/dist/metro-store-COLl1pOk.mjs +1249 -0
- package/dist/ndjson-DcAtEx_K.mjs +131 -0
- package/dist/ndjson-DvHSsNU9.d.mts +16 -0
- package/dist/process-output-C7IlIzd7.mjs +40 -0
- package/dist/project-Dit1CckW.mjs +234 -0
- package/dist/server-bare-BmTVVmBB.mjs +351 -0
- package/dist/server-expo-BZJm--wA.mjs +2 -0
- package/dist/server-expo-CHJe1r6j.mjs +311 -0
- package/dist/state-2nush3MK.mjs +269 -0
- package/dist/supervisor-run.d.mts +85 -0
- package/dist/supervisor-run.mjs +210 -0
- package/package.json +61 -8
- package/shim/android-cas-pch.cmake +35 -0
- package/shim/android-cas.gradle +29 -0
- package/shim/android-cas.toolchain.cmake +12 -0
- package/shim/android-optimizations.gradle +30 -0
- package/shim/bundle-response.cjs +46 -0
- package/shim/bundle-response.d.cts +5 -0
- package/shim/expo-metro-config.cjs +188 -0
- package/shim/metro-cache-generation.cjs +20 -0
- package/shim/metro-cache-generation.d.cts +5 -0
- package/skill/SKILL.md +14 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { t as NdjsonWriter } from "./ndjson-DvHSsNU9.mjs";
|
|
2
|
+
import { ChildProcess, SpawnOptions } from "child_process";
|
|
3
|
+
import { ChildProcess as ChildProcess$1 } from "node:child_process";
|
|
4
|
+
//#region src/exec.d.ts
|
|
5
|
+
interface ExecOptions {
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
omitEnv?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
interface Executor {
|
|
12
|
+
run(cmd: string, opts?: ExecOptions): string;
|
|
13
|
+
runFile(file: string, args?: string[], opts?: ExecOptions): string;
|
|
14
|
+
runQuiet(cmd: string, opts?: ExecOptions): string | null;
|
|
15
|
+
runFileQuiet(file: string, args?: string[], opts?: ExecOptions): string | null;
|
|
16
|
+
spawn(cmd: string, args?: readonly string[], opts?: SpawnOptions): ChildProcess;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/collector/android.d.ts
|
|
20
|
+
declare function androidClockOffset(serial: string, { exec, now }?: {
|
|
21
|
+
exec?: Executor;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
}): number | null;
|
|
24
|
+
interface PidResolution {
|
|
25
|
+
ok?: boolean;
|
|
26
|
+
pid?: number;
|
|
27
|
+
failed?: boolean;
|
|
28
|
+
reason?: string;
|
|
29
|
+
}
|
|
30
|
+
interface PidWatcher {
|
|
31
|
+
stop(): void;
|
|
32
|
+
probe(): number | null;
|
|
33
|
+
readonly pid: number | null;
|
|
34
|
+
}
|
|
35
|
+
declare function watchAppPid({ serial, packageName, pid, intervalMs, exec, onChange, resolve, setTimer, clearTimer }: {
|
|
36
|
+
serial: string;
|
|
37
|
+
packageName: string;
|
|
38
|
+
pid: number | null;
|
|
39
|
+
intervalMs?: number | null;
|
|
40
|
+
exec?: Executor | null;
|
|
41
|
+
onChange: (nextPid: number) => void | Promise<void>;
|
|
42
|
+
resolve?: (serial: string, packageName: string, opts: {
|
|
43
|
+
exec?: Executor | null;
|
|
44
|
+
}) => number | null;
|
|
45
|
+
setTimer?: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;
|
|
46
|
+
clearTimer?: (t: ReturnType<typeof setTimeout>) => void;
|
|
47
|
+
}): PidWatcher;
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/collector/state.d.ts
|
|
50
|
+
declare function registerCollector(root: string, platform: string, record: Record<string, unknown>): Record<string, unknown>;
|
|
51
|
+
declare function unregisterCollector(root: string, platform: string, pid: number, processToken: string): Record<string, unknown>;
|
|
52
|
+
declare function readCollectors(root: string): Record<string, unknown>;
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/collector/run.d.ts
|
|
55
|
+
declare const PLATFORMS: string[];
|
|
56
|
+
interface ParsedCollectorArgs {
|
|
57
|
+
platform?: string;
|
|
58
|
+
root?: string;
|
|
59
|
+
udid?: string;
|
|
60
|
+
bundleId?: string;
|
|
61
|
+
appName?: string;
|
|
62
|
+
appExecutable?: string | null;
|
|
63
|
+
serial?: string | null;
|
|
64
|
+
packageName?: string | null;
|
|
65
|
+
physical?: boolean;
|
|
66
|
+
payloadUrl?: string | null;
|
|
67
|
+
error?: string;
|
|
68
|
+
}
|
|
69
|
+
declare function parseArgs(argv: string[]): ParsedCollectorArgs;
|
|
70
|
+
interface RunCollectorOptions {
|
|
71
|
+
platform: string;
|
|
72
|
+
root: string;
|
|
73
|
+
udid?: string | null;
|
|
74
|
+
appName?: string | null;
|
|
75
|
+
appExecutable?: string | null;
|
|
76
|
+
bundleId?: string | null;
|
|
77
|
+
serial?: string | null;
|
|
78
|
+
packageName?: string | null;
|
|
79
|
+
physical?: boolean;
|
|
80
|
+
payloadUrl?: string | null;
|
|
81
|
+
startStream?: ((opts: {
|
|
82
|
+
platform: string;
|
|
83
|
+
udid?: string | null;
|
|
84
|
+
appName?: string | null;
|
|
85
|
+
appExecutable?: string | null;
|
|
86
|
+
bundleId?: string | null;
|
|
87
|
+
serial?: string | null;
|
|
88
|
+
physical?: boolean;
|
|
89
|
+
payloadUrl?: string | null;
|
|
90
|
+
pid?: number | null;
|
|
91
|
+
}) => ChildProcess$1) | null;
|
|
92
|
+
resolvePid?: ((opts: {
|
|
93
|
+
serial?: string | null;
|
|
94
|
+
packageName?: string | null;
|
|
95
|
+
timeoutMs: number;
|
|
96
|
+
}) => Promise<PidResolution>) | null;
|
|
97
|
+
pidTimeoutMs?: number;
|
|
98
|
+
pidOf?: ((serial: string, packageName: string, opts: {
|
|
99
|
+
exec?: Executor | null;
|
|
100
|
+
}) => number | null) | null;
|
|
101
|
+
pidWatchMs?: number | null;
|
|
102
|
+
watchPid?: typeof watchAppPid;
|
|
103
|
+
resolveClockOffset?: typeof androidClockOffset;
|
|
104
|
+
now?: () => number;
|
|
105
|
+
onExit?: (code: number) => void;
|
|
106
|
+
attachSignals?: boolean;
|
|
107
|
+
stderr?: (line: string) => void;
|
|
108
|
+
}
|
|
109
|
+
interface RunCollectorHandle {
|
|
110
|
+
child: ChildProcess$1 | null;
|
|
111
|
+
writer: NdjsonWriter;
|
|
112
|
+
finish: (code: number, level: string, msg: string, event: string) => void;
|
|
113
|
+
startedAt: string;
|
|
114
|
+
pid: number | null;
|
|
115
|
+
}
|
|
116
|
+
declare function runCollector({ platform, root, udid, appName, appExecutable, bundleId, serial, packageName, physical, payloadUrl, startStream, resolvePid, pidTimeoutMs, pidOf, pidWatchMs, watchPid, resolveClockOffset, now, onExit, attachSignals, stderr }: RunCollectorOptions): Promise<RunCollectorHandle | null>;
|
|
117
|
+
declare function main(argv?: string[]): Promise<void>;
|
|
118
|
+
//#endregion
|
|
119
|
+
export { PLATFORMS, ParsedCollectorArgs, RunCollectorHandle, RunCollectorOptions, main, parseArgs, readCollectors, registerCollector, runCollector, unregisterCollector };
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-bsN9MJXb.mjs";
|
|
2
|
+
import { S as captureProcessToken } from "./config-7kmtuhO2.mjs";
|
|
3
|
+
import { t as createLineReader } from "./process-output-C7IlIzd7.mjs";
|
|
4
|
+
import { a as watchAppPid, c as parseDeviceConsoleLine, f as readCollectors, i as waitForAppPid, l as startIosDeviceConsole, m as unregisterCollector, n as parseLogcatLine, p as registerCollector, r as startAndroidLogcat, t as androidClockOffset, u as collectorProcessTitle } from "./android-jc8MnUyL.mjs";
|
|
5
|
+
import { E as workspaceLogsDir } from "./state-2nush3MK.mjs";
|
|
6
|
+
import { r as createNdjsonWriter } from "./ndjson-DcAtEx_K.mjs";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
//#region src/collector/ios.ts
|
|
11
|
+
const MESSAGE_TYPE_LEVEL = {
|
|
12
|
+
Debug: "debug",
|
|
13
|
+
Info: "info",
|
|
14
|
+
Default: "info",
|
|
15
|
+
Error: "error",
|
|
16
|
+
Fault: "fatal"
|
|
17
|
+
};
|
|
18
|
+
function levelFromMessageType(messageType) {
|
|
19
|
+
return MESSAGE_TYPE_LEVEL[messageType] ?? "info";
|
|
20
|
+
}
|
|
21
|
+
const NOISE_RULES = [
|
|
22
|
+
{
|
|
23
|
+
id: "network",
|
|
24
|
+
subsystem: "com.apple.network"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "network-default-subsystem",
|
|
28
|
+
messagePrefix: [
|
|
29
|
+
"nw_socket",
|
|
30
|
+
"nw_connection",
|
|
31
|
+
"nw_protocol",
|
|
32
|
+
"nw_endpoint",
|
|
33
|
+
"nw_path",
|
|
34
|
+
"nw_resolver",
|
|
35
|
+
"nw_flow",
|
|
36
|
+
"nw_read_request",
|
|
37
|
+
"nw_write_request",
|
|
38
|
+
"tcp_input"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "sectrust",
|
|
43
|
+
subsystem: "com.apple.securityd"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "sectrust-default-subsystem",
|
|
47
|
+
messagePrefix: [
|
|
48
|
+
"SecTrust",
|
|
49
|
+
"SecOSStatus",
|
|
50
|
+
"TrustResultType"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "webkit",
|
|
55
|
+
subsystem: "com.apple.WebKit"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "webkit-default-subsystem",
|
|
59
|
+
messagePrefix: [
|
|
60
|
+
"WebPrivacy",
|
|
61
|
+
"GPUProcessProxy",
|
|
62
|
+
"WebProcessProxy"
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "audio-factory",
|
|
67
|
+
messageIncludes: ["AddInstanceForFactory"]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "coreui",
|
|
71
|
+
subsystem: "com.apple.coreui"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "coreui-default-subsystem",
|
|
75
|
+
messagePrefix: [
|
|
76
|
+
"CUICatalog:",
|
|
77
|
+
"CoreUI:",
|
|
78
|
+
"CoreThemeDefinition"
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "uiscene-deprecation",
|
|
83
|
+
messageIncludes: [
|
|
84
|
+
"UIScene lifecycle will soon be required",
|
|
85
|
+
"`UIScene` lifecycle will soon be required",
|
|
86
|
+
"must migrate to UIScene",
|
|
87
|
+
"migrate to UIScene lifecycle"
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "react-native-focus-cache",
|
|
92
|
+
exactSubsystem: "com.apple.UIKit",
|
|
93
|
+
category: "UIFocus",
|
|
94
|
+
messageEquals: "RCTScrollViewComponentView implements focusItemsInRect: - caching for linear focus movement is limited as long as this view is on screen."
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
function ruleMatches(rule, { subsystem, category, message }) {
|
|
98
|
+
if (rule.subsystem && subsystem !== rule.subsystem && !subsystem.startsWith(`${rule.subsystem}.`)) return false;
|
|
99
|
+
if (rule.exactSubsystem && subsystem !== rule.exactSubsystem) return false;
|
|
100
|
+
if (rule.category && category !== rule.category) return false;
|
|
101
|
+
if (rule.messageEquals && message !== rule.messageEquals) return false;
|
|
102
|
+
if (rule.messagePrefix && !rule.messagePrefix.some((p) => message.startsWith(p))) return false;
|
|
103
|
+
if (rule.messageIncludes && !rule.messageIncludes.some((p) => message.includes(p))) return false;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
function noiseRuleId(event) {
|
|
107
|
+
const subsystem = typeof event?.subsystem === "string" ? event.subsystem : "";
|
|
108
|
+
const category = typeof event?.category === "string" ? event.category : "";
|
|
109
|
+
const message = typeof event?.eventMessage === "string" ? event.eventMessage : "";
|
|
110
|
+
for (const rule of NOISE_RULES) if (ruleMatches(rule, {
|
|
111
|
+
subsystem,
|
|
112
|
+
category,
|
|
113
|
+
message
|
|
114
|
+
})) return rule.id;
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function levelForEvent(event) {
|
|
118
|
+
const level = levelFromMessageType(event?.messageType);
|
|
119
|
+
if (level !== "error" && level !== "fatal") return level;
|
|
120
|
+
return noiseRuleId(event) ? "info" : level;
|
|
121
|
+
}
|
|
122
|
+
function procFromImagePath(path) {
|
|
123
|
+
if (typeof path !== "string" || !path) return null;
|
|
124
|
+
const parts = path.split("/");
|
|
125
|
+
return parts[parts.length - 1] || null;
|
|
126
|
+
}
|
|
127
|
+
function tsFromEvent(event, now = Date.now) {
|
|
128
|
+
const parsed = Date.parse(event?.timestamp);
|
|
129
|
+
return Number.isFinite(parsed) ? parsed : now();
|
|
130
|
+
}
|
|
131
|
+
function recordFromLogEvent(event, { now = Date.now } = {}) {
|
|
132
|
+
if (!event || typeof event !== "object") return null;
|
|
133
|
+
const eventType = event.eventType;
|
|
134
|
+
if (eventType && eventType !== "logEvent") return null;
|
|
135
|
+
const msg = typeof event.eventMessage === "string" ? event.eventMessage : "";
|
|
136
|
+
if (!msg.trim()) return null;
|
|
137
|
+
const record = {
|
|
138
|
+
ts: tsFromEvent(event, now),
|
|
139
|
+
src: "device",
|
|
140
|
+
level: levelForEvent(event),
|
|
141
|
+
msg
|
|
142
|
+
};
|
|
143
|
+
const proc = procFromImagePath(event.processImagePath);
|
|
144
|
+
if (proc) record.proc = proc;
|
|
145
|
+
if (typeof event.subsystem === "string") record.subsystem = event.subsystem;
|
|
146
|
+
if (typeof event.category === "string") record.category = event.category;
|
|
147
|
+
return record;
|
|
148
|
+
}
|
|
149
|
+
function parseLogStreamLine(line, { now = Date.now } = {}) {
|
|
150
|
+
if (typeof line !== "string") return null;
|
|
151
|
+
const trimmed = line.trim();
|
|
152
|
+
if (!trimmed || trimmed[0] !== "{") return null;
|
|
153
|
+
let event;
|
|
154
|
+
try {
|
|
155
|
+
event = JSON.parse(trimmed);
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return recordFromLogEvent(event, { now });
|
|
160
|
+
}
|
|
161
|
+
function logStreamArgs(udid, appName, executableName) {
|
|
162
|
+
return [
|
|
163
|
+
"simctl",
|
|
164
|
+
"spawn",
|
|
165
|
+
udid,
|
|
166
|
+
"log",
|
|
167
|
+
"stream",
|
|
168
|
+
"--style",
|
|
169
|
+
"ndjson",
|
|
170
|
+
"--predicate",
|
|
171
|
+
`processImagePath ENDSWITH "/${appName}.app/${executableName && executableName.trim() !== "" ? executableName.trim() : appName}"`,
|
|
172
|
+
"--level",
|
|
173
|
+
"info"
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
function appNameFromBundleId(bundleId) {
|
|
177
|
+
const parts = String(bundleId || "").split(".");
|
|
178
|
+
return parts[parts.length - 1] || String(bundleId || "");
|
|
179
|
+
}
|
|
180
|
+
function startIosLogStream({ udid, appName, executableName = null, spawnFn = null }) {
|
|
181
|
+
return (spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts)))("xcrun", logStreamArgs(udid, appName, executableName), {
|
|
182
|
+
stdio: [
|
|
183
|
+
"ignore",
|
|
184
|
+
"pipe",
|
|
185
|
+
"pipe"
|
|
186
|
+
],
|
|
187
|
+
detached: false
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/collector/run.ts
|
|
192
|
+
const PLATFORMS = ["ios", "android"];
|
|
193
|
+
function parseArgs(argv) {
|
|
194
|
+
let platform;
|
|
195
|
+
let root;
|
|
196
|
+
let udid;
|
|
197
|
+
let bundleId;
|
|
198
|
+
let appName;
|
|
199
|
+
let appExecutable = null;
|
|
200
|
+
let serial = null;
|
|
201
|
+
let packageName = null;
|
|
202
|
+
let physical = false;
|
|
203
|
+
let payloadUrl = null;
|
|
204
|
+
for (let i = 0; i < argv.length; i++) {
|
|
205
|
+
const arg = argv[i];
|
|
206
|
+
if (arg === "--platform") {
|
|
207
|
+
platform = argv[++i];
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (arg === "--root") {
|
|
211
|
+
root = argv[++i];
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (arg === "--udid") {
|
|
215
|
+
udid = argv[++i];
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (arg === "--bundle") {
|
|
219
|
+
bundleId = argv[++i];
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (arg === "--app-name") {
|
|
223
|
+
appName = argv[++i];
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (arg === "--app-executable") {
|
|
227
|
+
appExecutable = argv[++i] ?? null;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (arg === "--serial") {
|
|
231
|
+
serial = argv[++i] ?? null;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (arg === "--package") {
|
|
235
|
+
packageName = argv[++i] ?? null;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (arg === "--physical") {
|
|
239
|
+
physical = true;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (arg === "--payload-url") {
|
|
243
|
+
payloadUrl = argv[++i] ?? null;
|
|
244
|
+
if (!payloadUrl) return { error: "--payload-url needs a URL." };
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
return { error: `Unknown collector argument "${arg}".` };
|
|
248
|
+
}
|
|
249
|
+
if (!platform || !PLATFORMS.includes(platform)) return { error: `--platform must be one of ${PLATFORMS.join(", ")}, got "${platform}".` };
|
|
250
|
+
if (!root) return { error: "Missing --root." };
|
|
251
|
+
if (!isAbsolute(root)) return { error: `--root must be an absolute path, got "${root}".` };
|
|
252
|
+
root = resolve(root);
|
|
253
|
+
if (platform === "ios") {
|
|
254
|
+
if (!udid) return { error: "Missing --udid (required for --platform ios)." };
|
|
255
|
+
if (!bundleId) return { error: "Missing --bundle (required for --platform ios)." };
|
|
256
|
+
if (!appName) appName = appNameFromBundleId(bundleId);
|
|
257
|
+
} else {
|
|
258
|
+
if (physical) return { error: "--physical is an iOS option; --platform android reads a serial." };
|
|
259
|
+
if (!serial) return { error: "Missing --serial (required for --platform android)." };
|
|
260
|
+
if (!packageName) return { error: "Missing --package (required for --platform android)." };
|
|
261
|
+
}
|
|
262
|
+
if (payloadUrl && !physical) return { error: "--payload-url only applies to --physical, which launches the app itself." };
|
|
263
|
+
return {
|
|
264
|
+
platform,
|
|
265
|
+
root,
|
|
266
|
+
udid,
|
|
267
|
+
bundleId,
|
|
268
|
+
appName,
|
|
269
|
+
appExecutable,
|
|
270
|
+
serial,
|
|
271
|
+
packageName,
|
|
272
|
+
physical,
|
|
273
|
+
payloadUrl
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const noopFlush = () => {};
|
|
277
|
+
function startMessage({ platform, physical, pid, appName, bundleId, udid, packageName, serial, appPid }) {
|
|
278
|
+
if (platform !== "ios") return `device log collector pid ${pid} streaming ${packageName} (pid ${appPid}) on ${serial}`;
|
|
279
|
+
if (!physical) return `device log collector pid ${pid} streaming ${appName} on ${udid}`;
|
|
280
|
+
return `device log collector pid ${pid} launching ${bundleId} on device ${udid} and streaming its console; subsystem, category and severity are not carried on hardware -- see \`stim guide logs\``;
|
|
281
|
+
}
|
|
282
|
+
async function runCollector({ platform, root, udid = null, appName = null, appExecutable = null, bundleId = null, serial = null, packageName = null, physical = false, payloadUrl = null, startStream = null, resolvePid = null, pidTimeoutMs = 3e4, pidOf = null, pidWatchMs = null, watchPid = watchAppPid, resolveClockOffset = androidClockOffset, now = Date.now, onExit = (code) => process.exit(code), attachSignals = true, stderr = (line) => console.error(line) }) {
|
|
283
|
+
const writer = createNdjsonWriter(join(workspaceLogsDir(root), "device.ndjson"));
|
|
284
|
+
const startedAt = new Date(now()).toISOString();
|
|
285
|
+
const processToken = captureProcessToken(process.pid);
|
|
286
|
+
if (!processToken) {
|
|
287
|
+
stderr("Stim collector: could not capture process identity; refusing to start an unmanaged collector.");
|
|
288
|
+
writer.close();
|
|
289
|
+
onExit(1);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
let finished = false;
|
|
293
|
+
let captured = 0;
|
|
294
|
+
let watcher = null;
|
|
295
|
+
const finish = (code, level, msg, event, { signalled = false } = {}) => {
|
|
296
|
+
if (finished) return;
|
|
297
|
+
finished = true;
|
|
298
|
+
watcher?.stop();
|
|
299
|
+
if (physical && captured === 0 && !signalled) writer.write({
|
|
300
|
+
src: "device",
|
|
301
|
+
platform,
|
|
302
|
+
level: "warn",
|
|
303
|
+
event: "collector_empty",
|
|
304
|
+
msg: `the device console produced no output for ${bundleId}; devicectl only connects the app's streams when it starts the app, and os_log below the info level never reaches them`
|
|
305
|
+
});
|
|
306
|
+
writer.write({
|
|
307
|
+
src: "device",
|
|
308
|
+
platform,
|
|
309
|
+
level,
|
|
310
|
+
event,
|
|
311
|
+
msg
|
|
312
|
+
});
|
|
313
|
+
try {
|
|
314
|
+
unregisterCollector(root, platform, process.pid, processToken);
|
|
315
|
+
} catch {}
|
|
316
|
+
const closed = writer.close();
|
|
317
|
+
if (closed.dropped > 0) stderr(`Stim collector: dropped ${closed.dropped} record(s); last error: ${describe(closed.lastError)}`);
|
|
318
|
+
onExit(code);
|
|
319
|
+
};
|
|
320
|
+
let child = null;
|
|
321
|
+
let flushReaders = noopFlush;
|
|
322
|
+
if (attachSignals) for (const signal of ["SIGTERM", "SIGINT"]) process.on(signal, () => {
|
|
323
|
+
flushReaders();
|
|
324
|
+
killChild(child);
|
|
325
|
+
finish(0, "info", `device log collector received ${signal}; detaching`, "collector_stopped", { signalled: true });
|
|
326
|
+
});
|
|
327
|
+
try {
|
|
328
|
+
registerCollector(root, platform, {
|
|
329
|
+
pid: process.pid,
|
|
330
|
+
processToken,
|
|
331
|
+
startedAt
|
|
332
|
+
});
|
|
333
|
+
} catch (err) {
|
|
334
|
+
stderr(`Stim collector: could not record the collector in ${root}: ${describe(err)}`);
|
|
335
|
+
finish(1, "error", "Could not persist collector identity", "collector_failed");
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
let pid = null;
|
|
339
|
+
if (platform === "android") {
|
|
340
|
+
const resolved = resolvePid ? await resolvePid({
|
|
341
|
+
serial,
|
|
342
|
+
packageName,
|
|
343
|
+
timeoutMs: pidTimeoutMs
|
|
344
|
+
}) : await waitForAppPid({
|
|
345
|
+
serial,
|
|
346
|
+
packageName,
|
|
347
|
+
timeoutMs: pidTimeoutMs
|
|
348
|
+
});
|
|
349
|
+
if (!resolved.ok) {
|
|
350
|
+
finish(1, "error", `device log collector could not attach: ${resolved.reason}`, "collector_failed");
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
pid = resolved.pid ?? null;
|
|
354
|
+
}
|
|
355
|
+
writer.write({
|
|
356
|
+
src: "device",
|
|
357
|
+
platform,
|
|
358
|
+
level: "info",
|
|
359
|
+
event: "collector_started",
|
|
360
|
+
msg: startMessage({
|
|
361
|
+
platform,
|
|
362
|
+
physical,
|
|
363
|
+
pid: process.pid,
|
|
364
|
+
appName,
|
|
365
|
+
bundleId,
|
|
366
|
+
udid,
|
|
367
|
+
packageName,
|
|
368
|
+
serial,
|
|
369
|
+
appPid: pid
|
|
370
|
+
})
|
|
371
|
+
});
|
|
372
|
+
const parse = platform === "ios" ? physical ? parseDeviceConsoleLine : parseLogStreamLine : parseLogcatLine;
|
|
373
|
+
const attach = (streamPid) => {
|
|
374
|
+
const clockOffsetMs = platform === "android" ? resolveClockOffset(serial, { now }) : null;
|
|
375
|
+
if (platform === "android") writer.write({
|
|
376
|
+
src: "device",
|
|
377
|
+
platform,
|
|
378
|
+
level: clockOffsetMs === null ? "warn" : "debug",
|
|
379
|
+
event: "collector_clock",
|
|
380
|
+
clockOffsetMs,
|
|
381
|
+
msg: clockOffsetMs === null ? "Could not align Android device time; log timestamps retain device time and may miss launch filters." : `Android log timestamps aligned to host time (offset ${clockOffsetMs}ms).`
|
|
382
|
+
});
|
|
383
|
+
const onLine = (line) => {
|
|
384
|
+
const record = parse(line, {
|
|
385
|
+
now,
|
|
386
|
+
clockOffsetMs
|
|
387
|
+
});
|
|
388
|
+
if (!record) return;
|
|
389
|
+
captured++;
|
|
390
|
+
writer.write({
|
|
391
|
+
...record,
|
|
392
|
+
platform
|
|
393
|
+
});
|
|
394
|
+
};
|
|
395
|
+
const spawned = startStream ? startStream({
|
|
396
|
+
platform,
|
|
397
|
+
udid,
|
|
398
|
+
appName,
|
|
399
|
+
appExecutable,
|
|
400
|
+
bundleId,
|
|
401
|
+
serial,
|
|
402
|
+
physical,
|
|
403
|
+
payloadUrl,
|
|
404
|
+
pid: streamPid
|
|
405
|
+
}) : platform === "ios" ? physical ? startIosDeviceConsole({
|
|
406
|
+
udid,
|
|
407
|
+
bundleId,
|
|
408
|
+
payloadUrl
|
|
409
|
+
}) : startIosLogStream({
|
|
410
|
+
udid,
|
|
411
|
+
appName,
|
|
412
|
+
executableName: appExecutable
|
|
413
|
+
}) : startAndroidLogcat({
|
|
414
|
+
serial,
|
|
415
|
+
pid: streamPid
|
|
416
|
+
});
|
|
417
|
+
const outReader = createLineReader(onLine);
|
|
418
|
+
const errReader = createLineReader(physical ? onLine : (line) => {
|
|
419
|
+
const text = String(line).trimEnd();
|
|
420
|
+
if (text.trim()) writer.write({
|
|
421
|
+
src: "device",
|
|
422
|
+
platform,
|
|
423
|
+
level: "debug",
|
|
424
|
+
raw: true,
|
|
425
|
+
event: "collector_stderr",
|
|
426
|
+
msg: text
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
flushReaders = () => {
|
|
430
|
+
outReader.flush();
|
|
431
|
+
errReader.flush();
|
|
432
|
+
};
|
|
433
|
+
spawned.stdout?.setEncoding?.("utf-8");
|
|
434
|
+
spawned.stderr?.setEncoding?.("utf-8");
|
|
435
|
+
spawned.stdout?.on("data", (chunk) => outReader.push(chunk));
|
|
436
|
+
spawned.stderr?.on("data", (chunk) => errReader.push(chunk));
|
|
437
|
+
spawned.on("exit", (code, signal) => {
|
|
438
|
+
if (spawned !== child) return;
|
|
439
|
+
flushReaders();
|
|
440
|
+
const next = platform === "android" ? watcher?.probe() : null;
|
|
441
|
+
if (next && next !== pid) {
|
|
442
|
+
reattach(next, "the log stream ended");
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const how = signal ? `signal ${signal}` : `exit code ${code}`;
|
|
446
|
+
if (physical && code) {
|
|
447
|
+
finish(1, "error", `the devicectl console ended with ${how}; treat the run as failed`, "collector_failed");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
finish(0, "info", `device log stream ended (${how}); the app or device is gone`, "collector_stopped");
|
|
451
|
+
});
|
|
452
|
+
spawned.on("error", (err) => {
|
|
453
|
+
if (spawned !== child) return;
|
|
454
|
+
finish(1, "error", `device log stream failed: ${describe(err)}`, "collector_failed");
|
|
455
|
+
});
|
|
456
|
+
return spawned;
|
|
457
|
+
};
|
|
458
|
+
const reattach = (nextPid, why = "the app restarted") => {
|
|
459
|
+
if (finished) return;
|
|
460
|
+
const previous = pid;
|
|
461
|
+
flushReaders();
|
|
462
|
+
killChild(child);
|
|
463
|
+
child = null;
|
|
464
|
+
writer.write({
|
|
465
|
+
src: "device",
|
|
466
|
+
platform,
|
|
467
|
+
level: "info",
|
|
468
|
+
event: "collector_reattached",
|
|
469
|
+
msg: `${why}: ${packageName} is now pid ${nextPid} on ${serial} (was ${previous}); reattaching the log stream`
|
|
470
|
+
});
|
|
471
|
+
pid = nextPid;
|
|
472
|
+
try {
|
|
473
|
+
child = attach(nextPid);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
finish(1, "error", `device log collector could not reattach to pid ${nextPid}: ${describe(err)}`, "collector_failed");
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
try {
|
|
479
|
+
child = attach(pid);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
finish(1, "error", `device log collector could not start: ${describe(err)}`, "collector_failed");
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
if (platform === "android" && !finished) watcher = watchPid({
|
|
485
|
+
serial,
|
|
486
|
+
packageName,
|
|
487
|
+
pid,
|
|
488
|
+
intervalMs: pidWatchMs,
|
|
489
|
+
resolve: pidOf || void 0,
|
|
490
|
+
onChange: (nextPid) => reattach(nextPid)
|
|
491
|
+
});
|
|
492
|
+
return {
|
|
493
|
+
child,
|
|
494
|
+
writer,
|
|
495
|
+
finish,
|
|
496
|
+
startedAt,
|
|
497
|
+
pid
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function killChild(child) {
|
|
501
|
+
try {
|
|
502
|
+
child?.kill?.("SIGTERM");
|
|
503
|
+
} catch {}
|
|
504
|
+
}
|
|
505
|
+
function describe(err) {
|
|
506
|
+
if (!err) return "unknown error";
|
|
507
|
+
return err.message || String(err);
|
|
508
|
+
}
|
|
509
|
+
async function main(argv = process.argv.slice(2)) {
|
|
510
|
+
const parsed = parseArgs(argv);
|
|
511
|
+
if (parsed.error) {
|
|
512
|
+
console.error(`Stim collector: ${parsed.error}`);
|
|
513
|
+
process.exit(2);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const root = parsed.root;
|
|
517
|
+
if (!existsSync(root)) {
|
|
518
|
+
console.error(`Stim collector: --root ${root} does not exist.`);
|
|
519
|
+
process.exit(2);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
process.title = collectorProcessTitle(parsed.platform, root);
|
|
523
|
+
await runCollector(parsed);
|
|
524
|
+
}
|
|
525
|
+
function invokedDirectly() {
|
|
526
|
+
const entry = process.argv[1];
|
|
527
|
+
if (!entry) return false;
|
|
528
|
+
try {
|
|
529
|
+
return resolve(entry) === fileURLToPath(import.meta.url);
|
|
530
|
+
} catch {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (invokedDirectly()) main().catch((err) => {
|
|
535
|
+
console.error(`Stim collector: ${describe(err)}`);
|
|
536
|
+
process.exit(1);
|
|
537
|
+
});
|
|
538
|
+
//#endregion
|
|
539
|
+
export { PLATFORMS, main, parseArgs, readCollectors, registerCollector, runCollector, unregisterCollector };
|