gitnexus 1.6.5-rc.35 → 1.6.5-rc.37
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/dist/cli/setup.js +12 -0
- package/dist/core/ingestion/import-resolvers/standard.js +7 -3
- package/dist/storage/repo-manager.js +44 -4
- package/hooks/claude/gitnexus-hook.cjs +34 -3
- package/hooks/claude/hook-db-lock-probe.cjs +238 -0
- package/hooks/claude/win-rm-list-json.ps1 +76 -0
- package/package.json +1 -1
package/dist/cli/setup.js
CHANGED
|
@@ -314,6 +314,18 @@ async function installClaudeCodeHooks(result) {
|
|
|
314
314
|
catch {
|
|
315
315
|
// Helper not found in source — skip
|
|
316
316
|
}
|
|
317
|
+
try {
|
|
318
|
+
await fs.copyFile(path.join(pluginHooksPath, 'hook-db-lock-probe.cjs'), path.join(destHooksDir, 'hook-db-lock-probe.cjs'));
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Helper not found in source — skip
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
await fs.copyFile(path.join(pluginHooksPath, 'win-rm-list-json.ps1'), path.join(destHooksDir, 'win-rm-list-json.ps1'));
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// Helper not found in source — skip
|
|
328
|
+
}
|
|
317
329
|
const hookPath = path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/');
|
|
318
330
|
// Escape backslashes FIRST, then quotes (CodeQL js/incomplete-sanitization).
|
|
319
331
|
// The previous shape `replace(/"/g, '\\"')` alone would let `path\with"quote`
|
|
@@ -52,6 +52,13 @@ export const resolveImportPath = (currentFile, importPath, allFiles, allFileList
|
|
|
52
52
|
const resolved = tryResolveWithExtensions(rewritten, allFiles);
|
|
53
53
|
if (resolved)
|
|
54
54
|
return cache(resolved);
|
|
55
|
+
// ESM fallback: strip .js/.jsx/.mjs/.cjs and retry with TS equivalents
|
|
56
|
+
const strippedAlias = stripJsExtension(rewritten);
|
|
57
|
+
if (strippedAlias !== null) {
|
|
58
|
+
const esmResolved = tryResolveWithExtensions(strippedAlias, allFiles);
|
|
59
|
+
if (esmResolved)
|
|
60
|
+
return cache(esmResolved);
|
|
61
|
+
}
|
|
55
62
|
// Try suffix matching as fallback
|
|
56
63
|
const parts = rewritten.split('/').filter(Boolean);
|
|
57
64
|
const suffixResult = suffixResolve(parts, normalizedFileList, allFileList, index);
|
|
@@ -112,9 +119,6 @@ export const resolveImportPath = (currentFile, importPath, allFiles, allFileList
|
|
|
112
119
|
return cache(resolved);
|
|
113
120
|
// TypeScript ESM: imports use .js/.jsx/.mjs/.cjs but source files are
|
|
114
121
|
// .ts/.tsx/.mts/.cts. Strip the JS-family extension and re-resolve.
|
|
115
|
-
// NOTE: This fallback only applies to relative imports. Path alias imports
|
|
116
|
-
// (e.g. @/utils.js via tsconfig paths) do not yet strip .js extensions —
|
|
117
|
-
// that is a known limitation tracked for follow-up.
|
|
118
122
|
if (language === SupportedLanguages.TypeScript || language === SupportedLanguages.JavaScript) {
|
|
119
123
|
const stripped = stripJsExtension(basePath);
|
|
120
124
|
if (stripped !== null) {
|
|
@@ -10,6 +10,7 @@ import { realpathSync } from 'fs';
|
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import os from 'os';
|
|
12
12
|
import { getInferredRepoName, resolveRepoIdentityRoot } from './git.js';
|
|
13
|
+
import { logger } from '../core/logger.js';
|
|
13
14
|
/**
|
|
14
15
|
* Normalise a repo path for registry comparison across platforms
|
|
15
16
|
* (#664 review feedback from @evander-wang).
|
|
@@ -200,13 +201,42 @@ export const findRepo = async (startPath) => {
|
|
|
200
201
|
}
|
|
201
202
|
return null;
|
|
202
203
|
};
|
|
204
|
+
function isReadOnlyFilesystemError(err) {
|
|
205
|
+
const code = err?.code;
|
|
206
|
+
return code === 'EROFS' || code === 'EACCES' || code === 'EPERM';
|
|
207
|
+
}
|
|
203
208
|
/**
|
|
204
209
|
* Keep generated index files ignored without modifying the user's root .gitignore.
|
|
205
210
|
*/
|
|
206
211
|
export const ensureGitNexusIgnored = async (repoPath) => {
|
|
207
212
|
const gitignorePath = path.join(getStoragePath(repoPath), '.gitignore');
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
const desired = '*\n';
|
|
214
|
+
// Idempotent fast path: skip the write entirely when the file already has
|
|
215
|
+
// the expected content. Lets this run cleanly on read-only mounts (e.g.
|
|
216
|
+
// the documented Docker workflow with WORKSPACE_DIR bound :ro) when an
|
|
217
|
+
// earlier `analyze` already created the file. See issue #1549.
|
|
218
|
+
try {
|
|
219
|
+
if ((await fs.readFile(gitignorePath, 'utf-8')) === desired) {
|
|
220
|
+
await ensureGitInfoExclude(repoPath);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
if (err?.code !== 'ENOENT')
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
await fs.mkdir(path.dirname(gitignorePath), { recursive: true });
|
|
230
|
+
await fs.writeFile(gitignorePath, desired, 'utf-8');
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
if (isReadOnlyFilesystemError(err)) {
|
|
234
|
+
logger.warn({ path: gitignorePath, code: err.code }, 'GitNexus storage filesystem is not writable; skipping .gitnexus/.gitignore. Generated files may appear as untracked in this repo locally.');
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
210
240
|
await ensureGitInfoExclude(repoPath);
|
|
211
241
|
};
|
|
212
242
|
const ensureGitInfoExclude = async (repoPath) => {
|
|
@@ -220,7 +250,6 @@ const ensureGitInfoExclude = async (repoPath) => {
|
|
|
220
250
|
catch {
|
|
221
251
|
return;
|
|
222
252
|
}
|
|
223
|
-
await fs.mkdir(path.dirname(excludePath), { recursive: true });
|
|
224
253
|
let content = '';
|
|
225
254
|
try {
|
|
226
255
|
content = await fs.readFile(excludePath, 'utf-8');
|
|
@@ -236,7 +265,18 @@ const ensureGitInfoExclude = async (repoPath) => {
|
|
|
236
265
|
if (excludes.includes(GITNEXUS_DIR) || excludes.includes(GITNEXUS_EXCLUDE_ENTRY))
|
|
237
266
|
return;
|
|
238
267
|
const separator = content.length === 0 || content.endsWith('\n') ? '' : '\n';
|
|
239
|
-
|
|
268
|
+
try {
|
|
269
|
+
await fs.mkdir(path.dirname(excludePath), { recursive: true });
|
|
270
|
+
await fs.writeFile(excludePath, `${content}${separator}${GITNEXUS_EXCLUDE_ENTRY}\n`, 'utf-8');
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
if (isReadOnlyFilesystemError(err)) {
|
|
274
|
+
logger.warn({ path: excludePath, code: err.code }, 'GitNexus storage filesystem is not writable; skipping .git/info/exclude update. .gitnexus/ may appear as untracked in `git status` locally.');
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
240
280
|
};
|
|
241
281
|
// ─── Global Registry (~/.gitnexus/registry.json) ───────────────────────
|
|
242
282
|
/**
|
|
@@ -15,6 +15,7 @@ const fs = require('fs');
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { spawnSync } = require('child_process');
|
|
17
17
|
const { acquireHookSlot } = require('./hook-lock.cjs');
|
|
18
|
+
const { hasGitNexusDbLockedByGitNexusServer } = require('./hook-db-lock-probe.cjs');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Read JSON input from stdin synchronously.
|
|
@@ -103,6 +104,28 @@ function findGitNexusDir(startDir) {
|
|
|
103
104
|
return null;
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
function hasGitNexusServerOwner(gitNexusDir) {
|
|
108
|
+
return hasGitNexusDbLockedByGitNexusServer(path.join(gitNexusDir, 'lbug'), process.pid);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function extractAugmentContext(stderr) {
|
|
112
|
+
const output = (stderr || '').trim();
|
|
113
|
+
const marker = output.indexOf('[GitNexus]');
|
|
114
|
+
const debug = process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
|
|
115
|
+
if (debug && output.length > 0) {
|
|
116
|
+
// Emit the FULL discarded prefix (everything before the marker, or all of
|
|
117
|
+
// it when no marker is present) so suppressed diagnostics — KuzuDB lock
|
|
118
|
+
// warnings, parser errors, etc. — remain recoverable on the hook's own
|
|
119
|
+
// stderr. The untruncated payload lets operators see exactly what was
|
|
120
|
+
// filtered out instead of a 180-char JSON-quoted preview.
|
|
121
|
+
const discarded = marker === -1 ? output : output.slice(0, marker).trim();
|
|
122
|
+
if (discarded.length > 0) {
|
|
123
|
+
process.stderr.write(`[GitNexus hook] augment stderr discarded prefix:\n${discarded}\n`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return marker === -1 ? '' : output.slice(marker).trim();
|
|
127
|
+
}
|
|
128
|
+
|
|
106
129
|
/**
|
|
107
130
|
* Extract search pattern from tool input.
|
|
108
131
|
*/
|
|
@@ -168,6 +191,10 @@ function extractPattern(toolName, toolInput) {
|
|
|
168
191
|
* 3. Fall back to npx (returns empty string)
|
|
169
192
|
*/
|
|
170
193
|
function resolveCliPath() {
|
|
194
|
+
const fromEnv = process.env.GITNEXUS_HOOK_CLI_PATH;
|
|
195
|
+
if (fromEnv !== undefined && String(fromEnv).trim() && fs.existsSync(String(fromEnv))) {
|
|
196
|
+
return String(fromEnv);
|
|
197
|
+
}
|
|
171
198
|
let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');
|
|
172
199
|
if (!fs.existsSync(cliPath)) {
|
|
173
200
|
try {
|
|
@@ -218,6 +245,10 @@ function handlePreToolUse(input) {
|
|
|
218
245
|
|
|
219
246
|
const pattern = extractPattern(toolName, toolInput);
|
|
220
247
|
if (!pattern || pattern.length < 3) return;
|
|
248
|
+
if (hasGitNexusServerOwner(gitNexusDir)) {
|
|
249
|
+
process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
221
252
|
|
|
222
253
|
const release = acquireHookSlot(gitNexusDir);
|
|
223
254
|
if (!release) return;
|
|
@@ -227,7 +258,7 @@ function handlePreToolUse(input) {
|
|
|
227
258
|
try {
|
|
228
259
|
const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);
|
|
229
260
|
if (!child.error && child.status === 0) {
|
|
230
|
-
result = child.stderr || '';
|
|
261
|
+
result = extractAugmentContext(child.stderr || '');
|
|
231
262
|
}
|
|
232
263
|
} catch {
|
|
233
264
|
/* graceful failure */
|
|
@@ -235,8 +266,8 @@ function handlePreToolUse(input) {
|
|
|
235
266
|
release();
|
|
236
267
|
}
|
|
237
268
|
|
|
238
|
-
if (result
|
|
239
|
-
sendHookResponse('PreToolUse', result
|
|
269
|
+
if (result) {
|
|
270
|
+
sendHookResponse('PreToolUse', result);
|
|
240
271
|
}
|
|
241
272
|
}
|
|
242
273
|
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform best-effort probe: does another process hold dbPath open
|
|
3
|
+
* with a command line that looks like a GitNexus MCP/serve server?
|
|
4
|
+
*
|
|
5
|
+
* Backends (no user-installed Sysinternals):
|
|
6
|
+
* - Linux: scan procfs under /proc (per-PID fd entries) via stat(2) (dev+inode); works without lsof;
|
|
7
|
+
* optional lsof fallback when proc scan finds nothing.
|
|
8
|
+
* - macOS / *BSD / etc.: trusted lsof + ps (absolute paths first).
|
|
9
|
+
* - Windows: Restart Manager (rstrtmgr) via bundled PowerShell script +
|
|
10
|
+
* Win32_Process for command lines; trusted powershell.exe under %SystemRoot%.
|
|
11
|
+
*
|
|
12
|
+
* Fail-open on most errors; fail-closed only on lsof ETIMEDOUT (Unix) or
|
|
13
|
+
* PowerShell ETIMEDOUT (Windows), matching the hook contract.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { spawnSync } = require('child_process');
|
|
19
|
+
|
|
20
|
+
function isGitNexusServerCommand(command) {
|
|
21
|
+
const hasServerMode = /(?:^|\s)(mcp|serve)(?:\s|$)/.test(command);
|
|
22
|
+
const hasGitNexus =
|
|
23
|
+
/(?:^|[/\\\s])gitnexus(?:\.cmd)?(?:\s|$)/.test(command) ||
|
|
24
|
+
/node_modules[/\\]gitnexus[/\\]/.test(command);
|
|
25
|
+
return hasServerMode && hasGitNexus;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveHookBinary(tool) {
|
|
29
|
+
const envKey = tool === 'lsof' ? 'GITNEXUS_HOOK_LSOF_PATH' : 'GITNEXUS_HOOK_PS_PATH';
|
|
30
|
+
const fromEnv = process.env[envKey];
|
|
31
|
+
if (fromEnv && String(fromEnv).trim() && fs.existsSync(String(fromEnv))) {
|
|
32
|
+
return String(fromEnv);
|
|
33
|
+
}
|
|
34
|
+
const candidates =
|
|
35
|
+
tool === 'lsof'
|
|
36
|
+
? ['/usr/bin/lsof', '/usr/sbin/lsof', '/sbin/lsof', tool]
|
|
37
|
+
: ['/bin/ps', '/usr/bin/ps', tool];
|
|
38
|
+
for (const candidate of candidates) {
|
|
39
|
+
if (candidate === tool) return tool;
|
|
40
|
+
try {
|
|
41
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
42
|
+
} catch {
|
|
43
|
+
/* ignore */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return tool;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveWindowsPowerShellPath() {
|
|
50
|
+
const fromEnv = process.env.GITNEXUS_HOOK_POWERSHELL_PATH;
|
|
51
|
+
if (fromEnv && String(fromEnv).trim() && fs.existsSync(String(fromEnv).trim())) {
|
|
52
|
+
return String(fromEnv).trim();
|
|
53
|
+
}
|
|
54
|
+
const root = process.env.SystemRoot || 'C:\\Windows';
|
|
55
|
+
const ps = path.join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
56
|
+
if (fs.existsSync(ps)) return ps;
|
|
57
|
+
const psWow = path.join(root, 'SysWOW64', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
58
|
+
if (fs.existsSync(psWow)) return psWow;
|
|
59
|
+
return 'powershell.exe';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Sentinel:
|
|
63
|
+
// undefined = not loaded yet (try the read)
|
|
64
|
+
// string = encoded PowerShell command (successful load)
|
|
65
|
+
// null = load attempted and failed (do not retry; warning already emitted)
|
|
66
|
+
let windowsRmListPsEncodedCommandCache;
|
|
67
|
+
let windowsRmListPsLoadFailureWarned = false;
|
|
68
|
+
function getWindowsRmListEncodedCommand() {
|
|
69
|
+
if (windowsRmListPsEncodedCommandCache !== undefined) {
|
|
70
|
+
return windowsRmListPsEncodedCommandCache;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const ps1Path = path.join(__dirname, 'win-rm-list-json.ps1');
|
|
74
|
+
const src = fs
|
|
75
|
+
.readFileSync(ps1Path, 'utf8')
|
|
76
|
+
.replace(/^\uFEFF/, '')
|
|
77
|
+
.replace(/\r\n/g, '\n');
|
|
78
|
+
windowsRmListPsEncodedCommandCache = Buffer.from(src, 'utf16le').toString('base64');
|
|
79
|
+
} catch (err) {
|
|
80
|
+
windowsRmListPsEncodedCommandCache = null;
|
|
81
|
+
if (
|
|
82
|
+
!windowsRmListPsLoadFailureWarned &&
|
|
83
|
+
(process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true')
|
|
84
|
+
) {
|
|
85
|
+
windowsRmListPsLoadFailureWarned = true;
|
|
86
|
+
const msg = err && err.message ? String(err.message).slice(0, 200) : 'unknown';
|
|
87
|
+
process.stderr.write(`[GitNexus hook] win-rm-list-json.ps1 load failed: ${msg}\n`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return windowsRmListPsEncodedCommandCache;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasGitNexusServerOwnerWindows(dbPathAbs, myPid) {
|
|
94
|
+
const encoded = getWindowsRmListEncodedCommand();
|
|
95
|
+
if (!encoded) return false;
|
|
96
|
+
const psExe = resolveWindowsPowerShellPath();
|
|
97
|
+
const r = spawnSync(
|
|
98
|
+
psExe,
|
|
99
|
+
[
|
|
100
|
+
'-NoProfile',
|
|
101
|
+
'-NonInteractive',
|
|
102
|
+
'-ExecutionPolicy',
|
|
103
|
+
'Bypass',
|
|
104
|
+
'-STA',
|
|
105
|
+
'-EncodedCommand',
|
|
106
|
+
encoded,
|
|
107
|
+
],
|
|
108
|
+
{
|
|
109
|
+
encoding: 'utf-8',
|
|
110
|
+
timeout: 6000,
|
|
111
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
112
|
+
env: { ...process.env, GITNEXUS_HOOK_RM_TARGET: dbPathAbs },
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
// ETIMEDOUT means the PowerShell probe didn't return in time; treat as 'unresponsive process holds DB' → fail-closed (skip augment).
|
|
116
|
+
if (r.error) return r.error.code === 'ETIMEDOUT';
|
|
117
|
+
if (r.status !== 0) return false;
|
|
118
|
+
let rows;
|
|
119
|
+
try {
|
|
120
|
+
rows = JSON.parse(String(r.stdout || '').trim() || '[]');
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (!Array.isArray(rows)) return false;
|
|
125
|
+
for (const row of rows) {
|
|
126
|
+
const procId = Number(row.pid);
|
|
127
|
+
const cmd = String(row.cmd || '');
|
|
128
|
+
if (!Number.isFinite(procId) || procId === myPid) continue;
|
|
129
|
+
if (isGitNexusServerCommand(cmd)) return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readLinuxCmdline(pidStr) {
|
|
135
|
+
try {
|
|
136
|
+
return fs.readFileSync(`/proc/${pidStr}/cmdline`, 'utf8').replace(/\0+/g, ' ').trim();
|
|
137
|
+
} catch {
|
|
138
|
+
return '';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function linuxProcScanFindGitNexusServer(dbPathAbs, myPid) {
|
|
143
|
+
const raw = process.env.GITNEXUS_HOOK_LINUX_PROC_BUDGET_MS;
|
|
144
|
+
const budget = Number(raw && String(raw).trim()) ? Number.parseInt(String(raw), 10) : 1200;
|
|
145
|
+
const start = Date.now();
|
|
146
|
+
let targetStat;
|
|
147
|
+
try {
|
|
148
|
+
targetStat = fs.statSync(dbPathAbs);
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
let procEntries;
|
|
153
|
+
try {
|
|
154
|
+
procEntries = fs.readdirSync('/proc', { withFileTypes: true });
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
for (const ent of procEntries) {
|
|
159
|
+
if (Date.now() - start > budget) return false;
|
|
160
|
+
if (!ent.isDirectory() || !/^\d+$/.test(ent.name)) continue;
|
|
161
|
+
const pid = Number.parseInt(ent.name, 10);
|
|
162
|
+
if (!Number.isFinite(pid) || pid === myPid) continue;
|
|
163
|
+
const fdDir = path.join('/proc', ent.name, 'fd');
|
|
164
|
+
let fds;
|
|
165
|
+
try {
|
|
166
|
+
fds = fs.readdirSync(fdDir);
|
|
167
|
+
} catch {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
let holds = false;
|
|
171
|
+
for (const fd of fds) {
|
|
172
|
+
if (Date.now() - start > budget) return false;
|
|
173
|
+
try {
|
|
174
|
+
const st = fs.statSync(path.join(fdDir, fd));
|
|
175
|
+
if (st.dev === targetStat.dev && st.ino === targetStat.ino) {
|
|
176
|
+
holds = true;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
/* ignore */
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!holds) continue;
|
|
184
|
+
if (isGitNexusServerCommand(readLinuxCmdline(ent.name))) return true;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function unixLsofPsFindGitNexusServer(dbPathAbs, myPid) {
|
|
190
|
+
const lsofPath = resolveHookBinary('lsof');
|
|
191
|
+
const lsof = spawnSync(lsofPath, ['-nP', '-t', '--', dbPathAbs], {
|
|
192
|
+
encoding: 'utf-8',
|
|
193
|
+
timeout: 1000,
|
|
194
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
195
|
+
});
|
|
196
|
+
if (lsof.error) return lsof.error.code === 'ETIMEDOUT';
|
|
197
|
+
|
|
198
|
+
const pids = (lsof.stdout || '').split(/\s+/).filter(Boolean);
|
|
199
|
+
const psPath = resolveHookBinary('ps');
|
|
200
|
+
for (const pid of pids) {
|
|
201
|
+
if (Number(pid) === myPid) continue;
|
|
202
|
+
const ps = spawnSync(psPath, ['-p', pid, '-o', 'command='], {
|
|
203
|
+
encoding: 'utf-8',
|
|
204
|
+
timeout: 500,
|
|
205
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
206
|
+
});
|
|
207
|
+
if (ps.error) {
|
|
208
|
+
if (ps.error.code === 'ETIMEDOUT') return true;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (isGitNexusServerCommand(ps.stdout || '')) return true;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @param {string} dbPath Absolute or relative path to the DB file (e.g. .../lbug).
|
|
218
|
+
* @param {number} myPid Current process PID (hook runner), excluded from matches.
|
|
219
|
+
*/
|
|
220
|
+
function hasGitNexusDbLockedByGitNexusServer(dbPath, myPid) {
|
|
221
|
+
if (!fs.existsSync(dbPath)) return false;
|
|
222
|
+
const dbPathAbs = path.resolve(dbPath);
|
|
223
|
+
|
|
224
|
+
if (process.platform === 'win32') {
|
|
225
|
+
return hasGitNexusServerOwnerWindows(dbPathAbs, myPid);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (process.platform === 'linux') {
|
|
229
|
+
if (linuxProcScanFindGitNexusServer(dbPathAbs, myPid)) return true;
|
|
230
|
+
return unixLsofPsFindGitNexusServer(dbPathAbs, myPid);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return unixLsofPsFindGitNexusServer(dbPathAbs, myPid);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = {
|
|
237
|
+
hasGitNexusDbLockedByGitNexusServer,
|
|
238
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
$target = $env:GITNEXUS_HOOK_RM_TARGET
|
|
3
|
+
if ([string]::IsNullOrWhiteSpace($target)) { Write-Output '[]'; exit 0 }
|
|
4
|
+
$target = (Resolve-Path -LiteralPath $target).ProviderPath
|
|
5
|
+
|
|
6
|
+
if (-not ([Management.Automation.PSTypeName]'GitNexusHookRm.Native').Type) {
|
|
7
|
+
Add-Type @'
|
|
8
|
+
using System;
|
|
9
|
+
using System.Runtime.InteropServices;
|
|
10
|
+
namespace GitNexusHookRm {
|
|
11
|
+
public static class Native {
|
|
12
|
+
public const int ErrorMoreData = 234;
|
|
13
|
+
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
|
14
|
+
public struct RM_UNIQUE_PROCESS {
|
|
15
|
+
public int dwProcessId;
|
|
16
|
+
public long ProcessStartTime;
|
|
17
|
+
}
|
|
18
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
19
|
+
public struct RM_PROCESS_INFO {
|
|
20
|
+
public RM_UNIQUE_PROCESS Process;
|
|
21
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
|
|
22
|
+
public string strAppName;
|
|
23
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
|
|
24
|
+
public string strServiceShortName;
|
|
25
|
+
public uint ApplicationType;
|
|
26
|
+
public uint AppStatus;
|
|
27
|
+
public uint TSSessionId;
|
|
28
|
+
public uint bRestartable;
|
|
29
|
+
}
|
|
30
|
+
[DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
|
|
31
|
+
public static extern int RmStartSession(out uint pSessionHandle, uint dwSessionFlags, string strSessionKey);
|
|
32
|
+
[DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
|
|
33
|
+
public static extern int RmRegisterResources(uint pSessionHandle, uint nFiles, string[] rgsFileNames, uint nApplications, IntPtr rgApplications, uint nServices, string[] rgsServiceNames);
|
|
34
|
+
[DllImport("rstrtmgr.dll")]
|
|
35
|
+
public static extern int RmGetList(uint dwSessionHandle, out uint pnProcInfoNeeded, ref uint pnProcInfo, [In, Out] RM_PROCESS_INFO[] rgAffectedApps, ref uint lpdwRebootReasons);
|
|
36
|
+
[DllImport("rstrtmgr.dll")]
|
|
37
|
+
public static extern int RmEndSession(uint pSessionHandle);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
'@
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
$h = [uint32]0
|
|
44
|
+
$key = [guid]::NewGuid().ToString('N')
|
|
45
|
+
$rmErr = [GitNexusHookRm.Native]::RmStartSession([ref]$h, 0, $key)
|
|
46
|
+
if ($rmErr -ne 0) { Write-Output '[]'; exit 0 }
|
|
47
|
+
$files = @($target)
|
|
48
|
+
$err = [GitNexusHookRm.Native]::RmRegisterResources($h, 1, $files, 0, [IntPtr]::Zero, 0, $null)
|
|
49
|
+
if ($err -ne 0) {
|
|
50
|
+
[void][GitNexusHookRm.Native]::RmEndSession($h)
|
|
51
|
+
Write-Output '[]'
|
|
52
|
+
exit 0
|
|
53
|
+
}
|
|
54
|
+
$need = [uint32]0
|
|
55
|
+
$n = [uint32]0
|
|
56
|
+
$reboot = [uint32]0
|
|
57
|
+
$err = [GitNexusHookRm.Native]::RmGetList($h, [ref]$need, [ref]$n, $null, [ref]$reboot)
|
|
58
|
+
if ($err -ne [GitNexusHookRm.Native]::ErrorMoreData) {
|
|
59
|
+
[void][GitNexusHookRm.Native]::RmEndSession($h)
|
|
60
|
+
Write-Output '[]'
|
|
61
|
+
exit 0
|
|
62
|
+
}
|
|
63
|
+
$n = $need
|
|
64
|
+
$buf = New-Object GitNexusHookRm.Native+RM_PROCESS_INFO[] ([int]$n)
|
|
65
|
+
$err = [GitNexusHookRm.Native]::RmGetList($h, [ref]$need, [ref]$n, $buf, [ref]$reboot)
|
|
66
|
+
[void][GitNexusHookRm.Native]::RmEndSession($h)
|
|
67
|
+
if ($err -ne 0) { Write-Output '[]'; exit 0 }
|
|
68
|
+
|
|
69
|
+
$out = @()
|
|
70
|
+
for ($i = 0; $i -lt [int]$n; $i++) {
|
|
71
|
+
$procId = $buf[$i].Process.dwProcessId
|
|
72
|
+
$p = Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$procId" -ErrorAction SilentlyContinue
|
|
73
|
+
$cmd = if ($p) { $p.CommandLine } else { '' }
|
|
74
|
+
$out += [PSCustomObject]@{ pid = [int]$procId; cmd = $cmd }
|
|
75
|
+
}
|
|
76
|
+
ConvertTo-Json -InputObject @($out) -Compress
|
package/package.json
CHANGED