fchek 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/README.md +64 -0
- package/bin/fchek.js +107 -0
- package/lib/api.js +110 -0
- package/lib/audit.js +211 -0
- package/lib/bench.js +248 -0
- package/lib/config.js +191 -0
- package/lib/context.js +356 -0
- package/lib/convention.js +526 -0
- package/lib/coverage.js +604 -0
- package/lib/db.js +135 -0
- package/lib/deps-check.js +264 -0
- package/lib/deps.js +374 -0
- package/lib/docker.js +84 -0
- package/lib/doctor.js +149 -0
- package/lib/dom.js +226 -0
- package/lib/fuzz.js +470 -0
- package/lib/git.js +290 -0
- package/lib/goto.js +544 -0
- package/lib/launch.js +182 -0
- package/lib/lint.js +624 -0
- package/lib/new_features.test.js +181 -0
- package/lib/output.js +46 -0
- package/lib/port.js +173 -0
- package/lib/process.js +228 -0
- package/lib/profile.js +453 -0
- package/lib/python.js +41 -0
- package/lib/race.js +186 -0
- package/lib/registry.js +179 -0
- package/lib/repl.js +135 -0
- package/lib/run.js +403 -0
- package/lib/screenshot.js +152 -0
- package/lib/secrets.js +257 -0
- package/lib/state.js +219 -0
- package/lib/test.js +471 -0
- package/lib/vuln.js +253 -0
- package/lib/watch.js +240 -0
- package/lib/winlog.js +123 -0
- package/package.json +27 -0
- package/skills/ACTIVATE.md +274 -0
- package/skills/README.md +163 -0
- package/skills/agent.md +444 -0
- package/skills/api.md +47 -0
- package/skills/bench.md +117 -0
- package/skills/context.md +116 -0
- package/skills/convention.md +143 -0
- package/skills/coverage.md +99 -0
- package/skills/csharp.md +97 -0
- package/skills/db.md +66 -0
- package/skills/deps-check.md +135 -0
- package/skills/deps.md +143 -0
- package/skills/docker.md +61 -0
- package/skills/dom.md +56 -0
- package/skills/fuzz.md +167 -0
- package/skills/goto.md +111 -0
- package/skills/lint.md +123 -0
- package/skills/port.md +57 -0
- package/skills/profile.md +91 -0
- package/skills/race.md +117 -0
- package/skills/repl.md +81 -0
- package/skills/rules.md +318 -0
- package/skills/run.md +135 -0
- package/skills/secrets.md +170 -0
- package/skills/security.md +360 -0
- package/skills/state.md +261 -0
- package/skills/vuln.md +57 -0
- package/skills/windows.md +320 -0
package/lib/profile.js
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync, spawnSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { output, ok, fail } = require('./output');
|
|
8
|
+
const { findPython } = require('./python');
|
|
9
|
+
|
|
10
|
+
const HELP = `
|
|
11
|
+
fchek profile <file> [app|web]
|
|
12
|
+
|
|
13
|
+
Profile code and show hot functions by CPU time.
|
|
14
|
+
|
|
15
|
+
Supported languages:
|
|
16
|
+
.py → py-spy (preferred) or cProfile (fallback)
|
|
17
|
+
.js / .ts → node --cpu-prof (generates .cpuprofile JSON)
|
|
18
|
+
.rs / Cargo → cargo-flamegraph (cargo install flamegraph) or perf
|
|
19
|
+
.c / .cpp → valgrind --tool=callgrind [Linux only]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
app Run as standalone app (default)
|
|
23
|
+
web Python web mode — attach to running process by PID (requires --pid)
|
|
24
|
+
|
|
25
|
+
Platform notes:
|
|
26
|
+
valgrind does not work on macOS >= 10.15 or Windows.
|
|
27
|
+
On macOS: use Instruments.app manually. On Windows: use WSL.
|
|
28
|
+
cargo-flamegraph requires: cargo install flamegraph + perf (Linux) or DTrace (macOS).
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
fchek profile main.py
|
|
32
|
+
fchek profile server.py web
|
|
33
|
+
fchek profile main.cpp
|
|
34
|
+
fchek profile app.js
|
|
35
|
+
fchek profile src/main.rs
|
|
36
|
+
`.trim();
|
|
37
|
+
|
|
38
|
+
const DEFAULT_TIMEOUT_MS = 60_000; // 1 minute max
|
|
39
|
+
|
|
40
|
+
function commandExists(cmd) {
|
|
41
|
+
try {
|
|
42
|
+
execSync(
|
|
43
|
+
process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`,
|
|
44
|
+
{ stdio: 'ignore' }
|
|
45
|
+
);
|
|
46
|
+
return true;
|
|
47
|
+
} catch { return false; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function detectLang(file) {
|
|
51
|
+
const ext = path.extname(file).toLowerCase();
|
|
52
|
+
if (ext === '.py') return 'python';
|
|
53
|
+
if (['.c', '.cpp', '.cc'].includes(ext)) return 'c';
|
|
54
|
+
if (['.js', '.mjs', '.cjs'].includes(ext)) return 'javascript';
|
|
55
|
+
if (ext === '.ts') return 'typescript';
|
|
56
|
+
if (ext === '.rs') return 'rust';
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseCProfileOutput(raw) {
|
|
61
|
+
const lines = raw.split('\n');
|
|
62
|
+
const entries = [];
|
|
63
|
+
// cProfile text format: ncalls tottime percall cumtime percall filename:lineno(function)
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
const m = line.match(/^\s*(\d+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+(.+)$/);
|
|
66
|
+
if (m) {
|
|
67
|
+
entries.push({
|
|
68
|
+
ncalls: parseInt(m[1]),
|
|
69
|
+
tottime: parseFloat(m[2]),
|
|
70
|
+
cumtime: parseFloat(m[4]),
|
|
71
|
+
location: m[6].trim(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return entries.slice(0, 20); // top 20
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function profilePython(file, mode, timeoutMs) {
|
|
79
|
+
const absFile = path.resolve(file);
|
|
80
|
+
|
|
81
|
+
// Try py-spy first — it gives the best per-function breakdown
|
|
82
|
+
if (commandExists('py-spy')) {
|
|
83
|
+
const outSvg = path.join(os.tmpdir(), 'fchek_profile.svg');
|
|
84
|
+
const spyRes = spawnSync(
|
|
85
|
+
'py-spy',
|
|
86
|
+
['record', '-o', outSvg, '--duration', '5', '--', 'python', absFile],
|
|
87
|
+
{ encoding: 'utf8', timeout: timeoutMs }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (spyRes.error && spyRes.error.code === 'ETIMEDOUT') {
|
|
91
|
+
return output(fail(`Profile timed out after ${timeoutMs}ms — possible infinite loop in code`));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// py-spy top for text summary
|
|
95
|
+
const topRes = spawnSync(
|
|
96
|
+
'py-spy',
|
|
97
|
+
['top', '--noninteractive', '--duration', '3', '--', 'python', absFile],
|
|
98
|
+
{ encoding: 'utf8', timeout: timeoutMs }
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const raw = topRes.stdout || topRes.stderr || '';
|
|
102
|
+
return output(ok({
|
|
103
|
+
file: absFile,
|
|
104
|
+
lang: 'python',
|
|
105
|
+
mode,
|
|
106
|
+
tool: 'py-spy',
|
|
107
|
+
timeout_ms: timeoutMs,
|
|
108
|
+
raw_summary: raw.slice(0, 4000),
|
|
109
|
+
flame_graph: fs.existsSync(outSvg) ? outSvg : null,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Fallback: cProfile via subprocess — script written to tmpfile to avoid -c issues on Windows
|
|
114
|
+
const py = findPython();
|
|
115
|
+
if (!py) {
|
|
116
|
+
return output(fail('Python not found. Install Python 3: https://python.org'));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const script = [
|
|
120
|
+
'import cProfile, pstats, io, sys',
|
|
121
|
+
'TARGET = ' + JSON.stringify(absFile),
|
|
122
|
+
'sys.argv = [TARGET]',
|
|
123
|
+
'pr = cProfile.Profile()',
|
|
124
|
+
'pr.enable()',
|
|
125
|
+
'with open(TARGET) as f:',
|
|
126
|
+
' code = f.read()',
|
|
127
|
+
'try:',
|
|
128
|
+
' exec(compile(code, TARGET, "exec"), {"__file__": TARGET, "__name__": "__main__"})',
|
|
129
|
+
'except SystemExit:',
|
|
130
|
+
' pass',
|
|
131
|
+
'pr.disable()',
|
|
132
|
+
's = io.StringIO()',
|
|
133
|
+
'pstats.Stats(pr, stream=s).sort_stats("cumulative").print_stats(25)',
|
|
134
|
+
'print(s.getvalue())',
|
|
135
|
+
].join('\n');
|
|
136
|
+
|
|
137
|
+
const tmpScript = path.join(os.tmpdir(), 'fchek_profile_' + process.pid + '.py');
|
|
138
|
+
fs.writeFileSync(tmpScript, script, 'utf8');
|
|
139
|
+
|
|
140
|
+
const res = spawnSync(py, [tmpScript], {
|
|
141
|
+
encoding: 'utf8',
|
|
142
|
+
timeout: timeoutMs,
|
|
143
|
+
windowsHide: true,
|
|
144
|
+
});
|
|
145
|
+
try { fs.unlinkSync(tmpScript); } catch {}
|
|
146
|
+
|
|
147
|
+
if (res.error && res.error.code === 'ETIMEDOUT') {
|
|
148
|
+
return output(fail(`Profile timed out after ${timeoutMs}ms — possible infinite loop`));
|
|
149
|
+
}
|
|
150
|
+
if (res.error) {
|
|
151
|
+
return output(fail(`Python not found: ${res.error.message}`));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const entries = parseCProfileOutput(res.stdout || '');
|
|
155
|
+
|
|
156
|
+
output(ok({
|
|
157
|
+
file: absFile,
|
|
158
|
+
lang: 'python',
|
|
159
|
+
mode,
|
|
160
|
+
tool: 'cProfile',
|
|
161
|
+
timeout_ms: timeoutMs,
|
|
162
|
+
top_functions: entries,
|
|
163
|
+
raw_summary: (res.stdout || '').slice(0, 4000),
|
|
164
|
+
stderr: (res.stderr || '').slice(0, 500) || null,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── JS/TS via node --cpu-prof ────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
function profileJs(file, timeoutMs) {
|
|
171
|
+
const absFile = path.resolve(file);
|
|
172
|
+
const ext = path.extname(file).toLowerCase();
|
|
173
|
+
const isTs = ext === '.ts';
|
|
174
|
+
const workDir = path.dirname(absFile);
|
|
175
|
+
|
|
176
|
+
// For TypeScript: compile first with tsc, then profile the JS
|
|
177
|
+
let targetFile = absFile;
|
|
178
|
+
let tmpJs = null;
|
|
179
|
+
|
|
180
|
+
if (isTs) {
|
|
181
|
+
if (!commandExists('tsc') && !commandExists('npx')) {
|
|
182
|
+
return output(fail('tsc not found. Install: npm install -g typescript'));
|
|
183
|
+
}
|
|
184
|
+
tmpJs = path.join(os.tmpdir(), `fchek_profile_${process.pid}.js`);
|
|
185
|
+
const tscArgs = commandExists('tsc')
|
|
186
|
+
? ['--outFile', tmpJs, '--module', 'none', '--target', 'ES2020', absFile]
|
|
187
|
+
: ['tsc', '--outFile', tmpJs, '--module', 'none', '--target', 'ES2020', absFile];
|
|
188
|
+
const tscRunner = commandExists('tsc') ? 'tsc' : 'npx';
|
|
189
|
+
const tscRes = spawnSync(tscRunner, tscArgs, {
|
|
190
|
+
encoding: 'utf8', timeout: 30000, windowsHide: true,
|
|
191
|
+
});
|
|
192
|
+
if (tscRes.status !== 0 || !fs.existsSync(tmpJs)) {
|
|
193
|
+
return output(fail(`TypeScript compilation failed:\n${(tscRes.stderr || tscRes.stdout || '').slice(0, 500)}`));
|
|
194
|
+
}
|
|
195
|
+
targetFile = tmpJs;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// node --cpu-prof --cpu-prof-dir=<tmpdir> <file>
|
|
199
|
+
const profDir = os.tmpdir();
|
|
200
|
+
const res = spawnSync(
|
|
201
|
+
'node',
|
|
202
|
+
['--cpu-prof', `--cpu-prof-dir=${profDir}`, targetFile],
|
|
203
|
+
{
|
|
204
|
+
encoding: 'utf8',
|
|
205
|
+
cwd: workDir,
|
|
206
|
+
timeout: timeoutMs,
|
|
207
|
+
windowsHide: true,
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (tmpJs) { try { fs.unlinkSync(tmpJs); } catch {} }
|
|
212
|
+
|
|
213
|
+
if (res.error?.code === 'ETIMEDOUT') {
|
|
214
|
+
return output(fail(`Profile timed out after ${timeoutMs}ms — possible infinite loop`));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Find the generated .cpuprofile file (node names it isolate-*-*.cpuprofile)
|
|
218
|
+
let cpuprofileFile = null;
|
|
219
|
+
try {
|
|
220
|
+
const files = fs.readdirSync(profDir)
|
|
221
|
+
.filter(f => f.endsWith('.cpuprofile'))
|
|
222
|
+
.map(f => ({ f, mtime: fs.statSync(path.join(profDir, f)).mtimeMs }))
|
|
223
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
224
|
+
if (files.length > 0) cpuprofileFile = path.join(profDir, files[0].f);
|
|
225
|
+
} catch {}
|
|
226
|
+
|
|
227
|
+
// Parse .cpuprofile to extract hot functions
|
|
228
|
+
const topFunctions = [];
|
|
229
|
+
if (cpuprofileFile && fs.existsSync(cpuprofileFile)) {
|
|
230
|
+
try {
|
|
231
|
+
const profile = JSON.parse(fs.readFileSync(cpuprofileFile, 'utf8'));
|
|
232
|
+
// V8 cpuprofile: nodes[] with callFrame + hitCount
|
|
233
|
+
const nodes = profile.nodes || [];
|
|
234
|
+
const totalHits = nodes.reduce((s, n) => s + (n.hitCount || 0), 0) || 1;
|
|
235
|
+
|
|
236
|
+
const sorted = nodes
|
|
237
|
+
.filter(n => n.hitCount > 0 && n.callFrame?.functionName)
|
|
238
|
+
.sort((a, b) => (b.hitCount || 0) - (a.hitCount || 0))
|
|
239
|
+
.slice(0, 20);
|
|
240
|
+
|
|
241
|
+
for (const node of sorted) {
|
|
242
|
+
const f = node.callFrame;
|
|
243
|
+
topFunctions.push({
|
|
244
|
+
function: f.functionName || '(anonymous)',
|
|
245
|
+
url: f.url ? path.basename(f.url) : null,
|
|
246
|
+
line: f.lineNumber != null ? f.lineNumber + 1 : null,
|
|
247
|
+
hit_count: node.hitCount,
|
|
248
|
+
pct: Math.round((node.hitCount / totalHits) * 100 * 10) / 10,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
output(ok({
|
|
255
|
+
file: absFile,
|
|
256
|
+
lang: isTs ? 'typescript' : 'javascript',
|
|
257
|
+
tool: 'node --cpu-prof',
|
|
258
|
+
timeout_ms: timeoutMs,
|
|
259
|
+
top_functions: topFunctions,
|
|
260
|
+
cpuprofile: cpuprofileFile,
|
|
261
|
+
exit_code: res.status,
|
|
262
|
+
stderr: (res.stderr || '').slice(0, 500) || null,
|
|
263
|
+
note: cpuprofileFile
|
|
264
|
+
? `Full profile saved: ${cpuprofileFile} (open in Chrome DevTools → Performance tab)`
|
|
265
|
+
: 'No .cpuprofile generated — script may have exited too fast.',
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── Rust via cargo-flamegraph ────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
function profileRust(file, timeoutMs) {
|
|
272
|
+
const absFile = path.resolve(file);
|
|
273
|
+
|
|
274
|
+
// Find Cargo.toml root
|
|
275
|
+
let cargoRoot = path.dirname(absFile);
|
|
276
|
+
for (let i = 0; i < 6; i++) {
|
|
277
|
+
if (fs.existsSync(path.join(cargoRoot, 'Cargo.toml'))) break;
|
|
278
|
+
const parent = path.dirname(cargoRoot);
|
|
279
|
+
if (parent === cargoRoot) { cargoRoot = path.dirname(absFile); break; }
|
|
280
|
+
cargoRoot = parent;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!commandExists('cargo')) {
|
|
284
|
+
return output(fail('cargo not found. Install Rust: https://rustup.rs'));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check if cargo-flamegraph is installed
|
|
288
|
+
const hasFlamegraph = (() => {
|
|
289
|
+
const r = spawnSync('cargo', ['flamegraph', '--version'],
|
|
290
|
+
{ encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
291
|
+
return r.status === 0;
|
|
292
|
+
})();
|
|
293
|
+
|
|
294
|
+
if (!hasFlamegraph) {
|
|
295
|
+
// Fallback: cargo build --release + time + basic stats
|
|
296
|
+
const buildRes = spawnSync(
|
|
297
|
+
'cargo', ['build', '--release'],
|
|
298
|
+
{ encoding: 'utf8', cwd: cargoRoot, timeout: timeoutMs, windowsHide: true }
|
|
299
|
+
);
|
|
300
|
+
if (buildRes.status !== 0) {
|
|
301
|
+
return output(fail(
|
|
302
|
+
`cargo-flamegraph not installed and cargo build failed.\n` +
|
|
303
|
+
`Install flamegraph: cargo install flamegraph\n` +
|
|
304
|
+
`Build error: ${(buildRes.stderr || '').slice(0, 400)}`
|
|
305
|
+
));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Run with time measurement
|
|
309
|
+
const t0 = Date.now();
|
|
310
|
+
const runRes = spawnSync(
|
|
311
|
+
'cargo', ['run', '--release'],
|
|
312
|
+
{ encoding: 'utf8', cwd: cargoRoot, timeout: timeoutMs, windowsHide: true }
|
|
313
|
+
);
|
|
314
|
+
const elapsed = Date.now() - t0;
|
|
315
|
+
|
|
316
|
+
return output(ok({
|
|
317
|
+
file: absFile,
|
|
318
|
+
lang: 'rust',
|
|
319
|
+
tool: 'cargo run --release (basic timing)',
|
|
320
|
+
timeout_ms: timeoutMs,
|
|
321
|
+
elapsed_ms: elapsed,
|
|
322
|
+
exit_code: runRes.status,
|
|
323
|
+
stdout: (runRes.stdout || '').slice(0, 2000),
|
|
324
|
+
stderr: (runRes.stderr || '').slice(0, 500) || null,
|
|
325
|
+
note: 'For detailed flamegraph: cargo install flamegraph then re-run fchek profile',
|
|
326
|
+
install_hint: 'cargo install flamegraph',
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// cargo flamegraph --output <tmpfile.svg>
|
|
331
|
+
const svgOut = path.join(os.tmpdir(), `fchek_flamegraph_${process.pid}.svg`);
|
|
332
|
+
const fgRes = spawnSync(
|
|
333
|
+
'cargo', ['flamegraph', '--output', svgOut],
|
|
334
|
+
{
|
|
335
|
+
encoding: 'utf8',
|
|
336
|
+
cwd: cargoRoot,
|
|
337
|
+
timeout: timeoutMs,
|
|
338
|
+
windowsHide: true,
|
|
339
|
+
env: { ...process.env, CARGO_PROFILE_RELEASE_DEBUG: '1' },
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
if (fgRes.error?.code === 'ETIMEDOUT') {
|
|
344
|
+
return output(fail(`Rust profile timed out after ${timeoutMs}ms`));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const hasSvg = fs.existsSync(svgOut);
|
|
348
|
+
|
|
349
|
+
output(ok({
|
|
350
|
+
file: absFile,
|
|
351
|
+
lang: 'rust',
|
|
352
|
+
tool: 'cargo-flamegraph',
|
|
353
|
+
timeout_ms: timeoutMs,
|
|
354
|
+
exit_code: fgRes.status,
|
|
355
|
+
flamegraph: hasSvg ? svgOut : null,
|
|
356
|
+
stderr: (fgRes.stderr || '').slice(0, 1000) || null,
|
|
357
|
+
note: hasSvg
|
|
358
|
+
? `Flamegraph saved: ${svgOut} (open in browser)`
|
|
359
|
+
: 'Flamegraph not generated — check stderr for details.',
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ─── C/C++ via valgrind/callgrind ─────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
function profileC(file, timeoutMs) {
|
|
366
|
+
const absFile = path.resolve(file);
|
|
367
|
+
|
|
368
|
+
if (process.platform !== 'linux') {
|
|
369
|
+
return output(fail(
|
|
370
|
+
`C/C++ profiling via valgrind is only supported on Linux.\n` +
|
|
371
|
+
`On macOS: use Instruments.app. On Windows: use WSL.`
|
|
372
|
+
));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!commandExists('valgrind')) {
|
|
376
|
+
return output(fail('valgrind not found. Install: apt install valgrind'));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const ext = path.extname(file).toLowerCase();
|
|
380
|
+
const compiler = (ext === '.cpp' || ext === '.cc') ? 'g++' : 'gcc';
|
|
381
|
+
|
|
382
|
+
if (!commandExists(compiler)) {
|
|
383
|
+
return output(fail(`${compiler} not found. Install: apt install build-essential`));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const outBin = path.join(os.tmpdir(), `fchek_${path.basename(file, ext)}.out`);
|
|
387
|
+
const compile = spawnSync(compiler, ['-g', '-O1', '-o', outBin, absFile], { encoding: 'utf8' });
|
|
388
|
+
|
|
389
|
+
if (compile.status !== 0) {
|
|
390
|
+
return output(fail(`Compilation failed:\n${compile.stderr}`));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const cgOut = path.join(os.tmpdir(), 'fchek_callgrind.out');
|
|
394
|
+
const vg = spawnSync(
|
|
395
|
+
'valgrind',
|
|
396
|
+
['--tool=callgrind', `--callgrind-out-file=${cgOut}`, outBin],
|
|
397
|
+
{ encoding: 'utf8', timeout: timeoutMs }
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (vg.error && vg.error.code === 'ETIMEDOUT') {
|
|
401
|
+
try { fs.unlinkSync(outBin); } catch {}
|
|
402
|
+
return output(fail(`Profile timed out after ${timeoutMs}ms`));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let annotation = null;
|
|
406
|
+
if (commandExists('callgrind_annotate') && fs.existsSync(cgOut)) {
|
|
407
|
+
const ann = spawnSync('callgrind_annotate', ['--auto=yes', cgOut], { encoding: 'utf8' });
|
|
408
|
+
annotation = ann.stdout ? ann.stdout.slice(0, 5000) : null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try { fs.unlinkSync(outBin); } catch {}
|
|
412
|
+
try { fs.unlinkSync(cgOut); } catch {}
|
|
413
|
+
|
|
414
|
+
output(ok({
|
|
415
|
+
file: absFile,
|
|
416
|
+
lang: 'c',
|
|
417
|
+
tool: 'valgrind/callgrind',
|
|
418
|
+
timeout_ms: timeoutMs,
|
|
419
|
+
valgrind_output: (vg.stderr || '').slice(0, 3000),
|
|
420
|
+
annotation,
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function run(args) {
|
|
425
|
+
if (args.length === 0 || args[0] === '--help') {
|
|
426
|
+
console.log(HELP);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const file = args[0];
|
|
431
|
+
const mode = args[1] || 'app';
|
|
432
|
+
const timeout = parseInt(
|
|
433
|
+
(args.find(a => a.startsWith('--timeout=')) || '').replace('--timeout=', '') || DEFAULT_TIMEOUT_MS,
|
|
434
|
+
10
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
if (!fs.existsSync(file)) {
|
|
438
|
+
return output(fail(`File not found: ${file}`));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const lang = detectLang(file);
|
|
442
|
+
if (!lang) {
|
|
443
|
+
return output(fail(`Unsupported file type: ${path.extname(file)}. Supported: .py .js .ts .rs .c .cpp`));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (lang === 'python') return profilePython(file, mode, timeout);
|
|
447
|
+
if (lang === 'javascript') return profileJs(file, timeout);
|
|
448
|
+
if (lang === 'typescript') return profileJs(file, timeout);
|
|
449
|
+
if (lang === 'rust') return profileRust(file, timeout);
|
|
450
|
+
if (lang === 'c') return profileC(file, timeout);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
module.exports = { run };
|
package/lib/python.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* python.js — find a working Python interpreter on any platform.
|
|
5
|
+
*
|
|
6
|
+
* On Windows, "python3" is often a Microsoft Store alias that prints an error
|
|
7
|
+
* instead of running. This helper finds the first interpreter that actually works.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
let _cachedPython = undefined;
|
|
13
|
+
|
|
14
|
+
function findPython() {
|
|
15
|
+
if (_cachedPython !== undefined) return _cachedPython;
|
|
16
|
+
|
|
17
|
+
const candidates = process.platform === 'win32'
|
|
18
|
+
? ['python', 'python3', 'py'] // on Windows "python" is usually real, "python3" is Store alias
|
|
19
|
+
: ['python3', 'python']; // on Linux/macOS python3 is preferred
|
|
20
|
+
|
|
21
|
+
for (const p of candidates) {
|
|
22
|
+
try {
|
|
23
|
+
const r = spawnSync(p, ['-c', 'import sys; print(sys.version_info[0])'], {
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
timeout: 4000,
|
|
26
|
+
// suppress Windows Store popup
|
|
27
|
+
windowsHide: true,
|
|
28
|
+
});
|
|
29
|
+
// Must exit 0 AND print a version number (not a Store redirect error)
|
|
30
|
+
if (r.status === 0 && r.stdout && r.stdout.trim().match(/^[23]$/)) {
|
|
31
|
+
_cachedPython = p;
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_cachedPython = null;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { findPython };
|
package/lib/race.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync, spawnSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const { output, ok, fail } = require('./output');
|
|
7
|
+
|
|
8
|
+
const HELP = `
|
|
9
|
+
fchek race <file.cpp> [--asan | --tsan | --ubsan | --all]
|
|
10
|
+
|
|
11
|
+
Compile and check for data races / memory errors using sanitizers.
|
|
12
|
+
Automatically detects project build system (CMake, Makefile, Cargo, plain gcc).
|
|
13
|
+
|
|
14
|
+
Flags:
|
|
15
|
+
--tsan ThreadSanitizer — data races (default)
|
|
16
|
+
--asan AddressSanitizer — buffer overflows, use-after-free, leaks
|
|
17
|
+
--ubsan UndefinedBehaviorSanitizer — integer overflow, null deref
|
|
18
|
+
--all Run all three sanitizers
|
|
19
|
+
|
|
20
|
+
Platform notes:
|
|
21
|
+
Linux — full support (gcc/clang + valgrind)
|
|
22
|
+
macOS — use clang (Apple clang supports -fsanitize=address/thread)
|
|
23
|
+
Windows — use WSL; native MinGW does NOT support sanitizers
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
fchek race threaded.cpp
|
|
27
|
+
fchek race memory.cpp --asan
|
|
28
|
+
fchek race app.cpp --all
|
|
29
|
+
`.trim();
|
|
30
|
+
|
|
31
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
32
|
+
|
|
33
|
+
function commandExists(cmd) {
|
|
34
|
+
try {
|
|
35
|
+
execSync(
|
|
36
|
+
process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`,
|
|
37
|
+
{ stdio: 'ignore' }
|
|
38
|
+
);
|
|
39
|
+
return true;
|
|
40
|
+
} catch { return false; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function detectBuildSystem(startDir) {
|
|
44
|
+
let dir = path.resolve(startDir);
|
|
45
|
+
for (let i = 0; i < 5; i++) {
|
|
46
|
+
if (fs.existsSync(path.join(dir, 'CMakeLists.txt'))) return { type: 'cmake', dir };
|
|
47
|
+
if (fs.existsSync(path.join(dir, 'Makefile'))) return { type: 'make', dir };
|
|
48
|
+
if (fs.existsSync(path.join(dir, 'Cargo.toml'))) return { type: 'cargo', dir };
|
|
49
|
+
const parent = path.dirname(dir);
|
|
50
|
+
if (parent === dir) break;
|
|
51
|
+
dir = parent;
|
|
52
|
+
}
|
|
53
|
+
return { type: 'single', dir: startDir };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pickCompiler() {
|
|
57
|
+
if (commandExists('clang++')) return { cc: 'clang', cxx: 'clang++' };
|
|
58
|
+
if (commandExists('g++')) return { cc: 'gcc', cxx: 'g++' };
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseSanitizerOutput(raw, name) {
|
|
63
|
+
const issues = [];
|
|
64
|
+
const lines = raw.split('\n');
|
|
65
|
+
let cur = null;
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
if (/ERROR:|WARNING:|runtime error:/i.test(line)) {
|
|
68
|
+
if (cur) issues.push(cur);
|
|
69
|
+
cur = { sanitizer: name, headline: line.trim(), stack: [] };
|
|
70
|
+
} else if (cur && line.trim()) {
|
|
71
|
+
cur.stack.push(line.trim());
|
|
72
|
+
if (cur.stack.length >= 8) { issues.push(cur); cur = null; }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (cur) issues.push(cur);
|
|
76
|
+
return issues;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function runSanitizer(file, sanitizerFlag, sanitizerName, compiler, timeoutMs) {
|
|
80
|
+
const base = path.basename(file, path.extname(file));
|
|
81
|
+
const outBin = path.join(
|
|
82
|
+
require('os').tmpdir(),
|
|
83
|
+
`fchek_${base}_${sanitizerName}.out`
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const compileArgs = [
|
|
87
|
+
'-g', '-O1',
|
|
88
|
+
`-fsanitize=${sanitizerFlag}`,
|
|
89
|
+
'-fno-omit-frame-pointer',
|
|
90
|
+
'-o', outBin, file
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const compile = spawnSync(compiler.cxx, compileArgs, { encoding: 'utf8' });
|
|
94
|
+
if (compile.status !== 0) {
|
|
95
|
+
return { sanitizer: sanitizerName, compiled: false, error: compile.stderr.trim() };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const run = spawnSync(outBin, [], {
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
timeout: timeoutMs,
|
|
101
|
+
env: {
|
|
102
|
+
...process.env,
|
|
103
|
+
ASAN_OPTIONS: 'symbolize=1:detect_leaks=1:abort_on_error=0',
|
|
104
|
+
TSAN_OPTIONS: 'symbolize=1:abort_on_error=0',
|
|
105
|
+
UBSAN_OPTIONS: 'print_stacktrace=1:abort_on_error=0',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
try { fs.unlinkSync(outBin); } catch {}
|
|
110
|
+
|
|
111
|
+
const timedOut = run.error && run.error.code === 'ETIMEDOUT';
|
|
112
|
+
const combined = (run.stdout || '') + (run.stderr || '');
|
|
113
|
+
const hasFinding = /ERROR:|WARNING:|runtime error:/i.test(combined);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
sanitizer: sanitizerName,
|
|
117
|
+
compiled: true,
|
|
118
|
+
timed_out: timedOut,
|
|
119
|
+
issues: hasFinding ? parseSanitizerOutput(combined, sanitizerName) : [],
|
|
120
|
+
raw_output: combined.slice(0, 3000),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function run(args) {
|
|
125
|
+
if (args.length === 0 || args[0] === '--help') {
|
|
126
|
+
console.log(HELP);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const file = args[0];
|
|
131
|
+
const flags = args.slice(1);
|
|
132
|
+
|
|
133
|
+
if (!fs.existsSync(file)) {
|
|
134
|
+
return output(fail(`File not found: ${file}`));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const ext = path.extname(file).toLowerCase();
|
|
138
|
+
if (!['.c', '.cpp', '.cc'].includes(ext)) {
|
|
139
|
+
return output(fail(`Unsupported file type: ${ext}. Supported: .c .cpp .cc`));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (process.platform === 'win32') {
|
|
143
|
+
return output(fail(
|
|
144
|
+
'Sanitizers are not supported on native Windows.\n' +
|
|
145
|
+
'Use WSL: wsl --install, then run fchek inside WSL.'
|
|
146
|
+
));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const compiler = pickCompiler();
|
|
150
|
+
if (!compiler) {
|
|
151
|
+
return output(fail('No C++ compiler found. Install: apt install build-essential'));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const buildSystem = detectBuildSystem(path.dirname(path.resolve(file)));
|
|
155
|
+
|
|
156
|
+
const runAll = flags.includes('--all');
|
|
157
|
+
const useAsan = flags.includes('--asan');
|
|
158
|
+
const useTsan = flags.includes('--tsan');
|
|
159
|
+
const useUbsan = flags.includes('--ubsan');
|
|
160
|
+
|
|
161
|
+
const sanitizers = runAll
|
|
162
|
+
? [['thread', 'ThreadSanitizer'], ['address', 'AddressSanitizer'], ['undefined', 'UBSanitizer']]
|
|
163
|
+
: useAsan ? [['address', 'AddressSanitizer']]
|
|
164
|
+
: useUbsan ? [['undefined', 'UBSanitizer']]
|
|
165
|
+
: [['thread', 'ThreadSanitizer']];
|
|
166
|
+
|
|
167
|
+
const results = sanitizers.map(([flag, name]) =>
|
|
168
|
+
runSanitizer(file, flag, name, compiler, DEFAULT_TIMEOUT_MS)
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const totalIssues = results.reduce((n, r) => n + (r.issues ? r.issues.length : 0), 0);
|
|
172
|
+
|
|
173
|
+
output(ok({
|
|
174
|
+
file: path.resolve(file),
|
|
175
|
+
build_system: buildSystem.type,
|
|
176
|
+
compiler: compiler.cxx,
|
|
177
|
+
timeout_ms: DEFAULT_TIMEOUT_MS,
|
|
178
|
+
sanitizers: results,
|
|
179
|
+
summary: {
|
|
180
|
+
total_issues: totalIssues,
|
|
181
|
+
verdict: totalIssues === 0 ? 'clean' : 'issues_found',
|
|
182
|
+
},
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { run };
|