gitnexus 1.6.6-rc.62 → 1.6.6-rc.63
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.
|
@@ -4,11 +4,13 @@ import { createInterface } from 'readline';
|
|
|
4
4
|
import { once } from 'events';
|
|
5
5
|
import { finished } from 'stream/promises';
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import crypto from 'crypto';
|
|
7
9
|
import lbug from '@ladybugdb/core';
|
|
8
10
|
import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, STALE_HASH_SENTINEL, } from './schema.js';
|
|
9
11
|
import { streamAllCSVsToDisk } from './csv-generator.js';
|
|
10
12
|
import { extensionManager } from './extension-loader.js';
|
|
11
|
-
import { closeLbugConnection, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
|
|
13
|
+
import { closeLbugConnection, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, toNativeSafePath, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
|
|
12
14
|
import { finalizeLbugSidecarsAfterClose, inspectLbugSidecars, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, shadowSidecarRecoveryMessage, } from './sidecar-recovery.js';
|
|
13
15
|
import { isVectorExtensionSupportedByPlatform } from '../platform/capabilities.js';
|
|
14
16
|
import { logger } from '../logger.js';
|
|
@@ -298,7 +300,7 @@ const runWithSessionLock = async (operation) => {
|
|
|
298
300
|
release?.();
|
|
299
301
|
}
|
|
300
302
|
};
|
|
301
|
-
const normalizeCopyPath = (filePath) => filePath.replace(/\\/g, '/');
|
|
303
|
+
const normalizeCopyPath = (filePath) => toNativeSafePath(filePath).replace(/\\/g, '/');
|
|
302
304
|
const closeQueryResult = async (result) => {
|
|
303
305
|
try {
|
|
304
306
|
await result.close();
|
|
@@ -718,7 +720,14 @@ export const loadGraphToLbug = async (graph, repoPath, storagePath, onProgress)
|
|
|
718
720
|
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
719
721
|
}
|
|
720
722
|
const log = onProgress || (() => { });
|
|
721
|
-
|
|
723
|
+
let csvDir;
|
|
724
|
+
if (process.platform === 'win32' && /[^\x00-\x7F]/.test(storagePath)) {
|
|
725
|
+
const hash = crypto.createHash('sha256').update(storagePath).digest('hex').slice(0, 16);
|
|
726
|
+
csvDir = toNativeSafePath(path.join(os.tmpdir(), `gitnexus-csv-${hash}`));
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
csvDir = path.join(storagePath, 'csv');
|
|
730
|
+
}
|
|
722
731
|
log('Streaming CSVs to disk...');
|
|
723
732
|
const csvResult = await streamAllCSVsToDisk(graph, repoPath, csvDir);
|
|
724
733
|
const validTables = new Set(NODE_TABLES);
|
|
@@ -1,7 +1,184 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
+
import fsSync from 'fs';
|
|
2
3
|
import os from 'os';
|
|
3
4
|
import path from 'path';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import { execFileSync } from 'child_process';
|
|
7
|
+
import { isMainThread } from 'worker_threads';
|
|
4
8
|
import { logger } from '../logger.js';
|
|
9
|
+
// ─── Windows non-ASCII path workaround (#1811) ───────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// KuzuDB's native C++ layer on Windows uses CreateFileA (ANSI), not
|
|
12
|
+
// CreateFileW. Non-ASCII path bytes from Node.js (UTF-8) are
|
|
13
|
+
// misinterpreted via the system's Active Code Page (e.g. GBK), producing
|
|
14
|
+
// a garbled path — "Error 3: The system cannot find the path."
|
|
15
|
+
//
|
|
16
|
+
// Layered workaround:
|
|
17
|
+
// 1. Try 8.3 short-name form (fast, no persistent state)
|
|
18
|
+
// 2. Fall back to an NTFS junction from an ASCII temp path
|
|
19
|
+
// 3. If both fail, log a diagnostic and return the original path
|
|
20
|
+
const NON_ASCII_RE = /[^\x00-\x7F]/;
|
|
21
|
+
const JUNCTION_PREFIX = 'gitnexus-junction-';
|
|
22
|
+
const activeJunctions = new Set();
|
|
23
|
+
let cleanupRegistered = false;
|
|
24
|
+
let orphanScanDone = false;
|
|
25
|
+
function junctionHash(targetDir) {
|
|
26
|
+
return crypto.createHash('sha256').update(targetDir).digest('hex').slice(0, 16);
|
|
27
|
+
}
|
|
28
|
+
function tryShortPath(p) {
|
|
29
|
+
try {
|
|
30
|
+
// Pass the path via environment variable so the command string is
|
|
31
|
+
// static — avoids CodeQL command-injection taint (the path never
|
|
32
|
+
// appears in the shell command text).
|
|
33
|
+
const result = execFileSync('cmd.exe', ['/c', 'for %I in ("%GITNEXUS_SP%") do @echo %~sI'], {
|
|
34
|
+
encoding: 'utf-8',
|
|
35
|
+
timeout: 5000,
|
|
36
|
+
windowsHide: true,
|
|
37
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
38
|
+
env: { ...process.env, GITNEXUS_SP: p },
|
|
39
|
+
});
|
|
40
|
+
const shortPath = result.trim();
|
|
41
|
+
if (shortPath &&
|
|
42
|
+
!NON_ASCII_RE.test(shortPath) &&
|
|
43
|
+
(!shortPath.includes('?') || p.includes('?'))) {
|
|
44
|
+
return shortPath;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// 8.3 unavailable or cmd failed
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function tryJunction(targetDir, leaf) {
|
|
53
|
+
const hash = junctionHash(targetDir);
|
|
54
|
+
const junctionLink = path.join(os.tmpdir(), `${JUNCTION_PREFIX}${hash}`);
|
|
55
|
+
if (fsSync.existsSync(junctionLink)) {
|
|
56
|
+
try {
|
|
57
|
+
const existing = fsSync.readlinkSync(junctionLink);
|
|
58
|
+
if (path.resolve(existing) === path.resolve(targetDir)) {
|
|
59
|
+
activeJunctions.add(junctionLink);
|
|
60
|
+
return path.join(junctionLink, leaf);
|
|
61
|
+
}
|
|
62
|
+
fsSync.rmSync(junctionLink, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Stale or broken junction — remove and recreate
|
|
66
|
+
try {
|
|
67
|
+
fsSync.rmSync(junctionLink, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* best effort */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
fsSync.symlinkSync(targetDir, junctionLink, 'junction');
|
|
76
|
+
activeJunctions.add(junctionLink);
|
|
77
|
+
return path.join(junctionLink, leaf);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (err.code === 'EEXIST') {
|
|
81
|
+
try {
|
|
82
|
+
const existing = fsSync.readlinkSync(junctionLink);
|
|
83
|
+
if (path.resolve(existing) === path.resolve(targetDir)) {
|
|
84
|
+
activeJunctions.add(junctionLink);
|
|
85
|
+
return path.join(junctionLink, leaf);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* cannot verify — fall through */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function registerCleanupHandlers() {
|
|
96
|
+
if (cleanupRegistered)
|
|
97
|
+
return;
|
|
98
|
+
cleanupRegistered = true;
|
|
99
|
+
process.on('exit', () => cleanupNativePathJunctions());
|
|
100
|
+
for (const signal of ['SIGTERM', 'SIGINT']) {
|
|
101
|
+
process.on(signal, () => {
|
|
102
|
+
cleanupNativePathJunctions();
|
|
103
|
+
if (process.platform === 'win32') {
|
|
104
|
+
process.exit(signal === 'SIGINT' ? 130 : 143);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
process.kill(process.pid, signal);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function scanOrphanedJunctions() {
|
|
113
|
+
if (orphanScanDone)
|
|
114
|
+
return;
|
|
115
|
+
orphanScanDone = true;
|
|
116
|
+
try {
|
|
117
|
+
const tmpdir = os.tmpdir();
|
|
118
|
+
const entries = fsSync.readdirSync(tmpdir);
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (!entry.startsWith(JUNCTION_PREFIX))
|
|
121
|
+
continue;
|
|
122
|
+
const junctionPath = path.join(tmpdir, entry);
|
|
123
|
+
try {
|
|
124
|
+
const target = fsSync.readlinkSync(junctionPath);
|
|
125
|
+
try {
|
|
126
|
+
fsSync.lstatSync(target);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
fsSync.rmSync(junctionPath, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Not a symlink/junction or unreadable — leave it
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// tmpdir unreadable — skip scan
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function cleanupNativePathJunctions() {
|
|
142
|
+
for (const junctionPath of activeJunctions) {
|
|
143
|
+
try {
|
|
144
|
+
fsSync.rmSync(junctionPath, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Best effort — EPERM on Windows is common during exit
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
activeJunctions.clear();
|
|
151
|
+
}
|
|
152
|
+
export function toNativeSafePath(p) {
|
|
153
|
+
if (process.platform !== 'win32')
|
|
154
|
+
return p;
|
|
155
|
+
if (!NON_ASCII_RE.test(p))
|
|
156
|
+
return p;
|
|
157
|
+
if (isMainThread) {
|
|
158
|
+
scanOrphanedJunctions();
|
|
159
|
+
registerCleanupHandlers();
|
|
160
|
+
}
|
|
161
|
+
const shortPath = tryShortPath(p);
|
|
162
|
+
if (shortPath)
|
|
163
|
+
return shortPath;
|
|
164
|
+
if (!isMainThread) {
|
|
165
|
+
logger.warn(`GitNexus: non-ASCII path in worker thread — junction fallback skipped. ` +
|
|
166
|
+
`Path: "${p}". 8.3 short names may need to be enabled on this volume.`);
|
|
167
|
+
return p;
|
|
168
|
+
}
|
|
169
|
+
const targetDir = path.dirname(p);
|
|
170
|
+
const leaf = path.basename(p);
|
|
171
|
+
if (fsSync.existsSync(targetDir)) {
|
|
172
|
+
const junctionResult = tryJunction(targetDir, leaf);
|
|
173
|
+
if (junctionResult)
|
|
174
|
+
return junctionResult;
|
|
175
|
+
}
|
|
176
|
+
logger.warn(`GitNexus: non-ASCII path "${p}" could not be converted to an ASCII-safe form. ` +
|
|
177
|
+
'LadybugDB may fail with "Cannot open file." To fix: move the repo to a path ' +
|
|
178
|
+
'without CJK/Unicode characters, or enable 8.3 short names on this volume ' +
|
|
179
|
+
'(fsutil 8dot3name set 0).');
|
|
180
|
+
return p;
|
|
181
|
+
}
|
|
5
182
|
/**
|
|
6
183
|
* Shared configuration for `@ladybugdb/core` `Database` construction.
|
|
7
184
|
*
|
|
@@ -306,9 +483,10 @@ const openWithLockRetry = async (construct, dbPath) => {
|
|
|
306
483
|
throw tagOpenRetryExhausted(originalLockError);
|
|
307
484
|
};
|
|
308
485
|
export async function openLbugConnection(lbugModule, databasePath, options = {}) {
|
|
486
|
+
const safePath = toNativeSafePath(databasePath);
|
|
309
487
|
let db;
|
|
310
488
|
try {
|
|
311
|
-
db = await openWithLockRetry(() => createLbugDatabase(lbugModule,
|
|
489
|
+
db = await openWithLockRetry(() => createLbugDatabase(lbugModule, safePath, options), safePath);
|
|
312
490
|
return { db, conn: new lbugModule.Connection(db) };
|
|
313
491
|
}
|
|
314
492
|
catch (err) {
|
|
@@ -19,7 +19,7 @@ import os from 'os';
|
|
|
19
19
|
import path from 'path';
|
|
20
20
|
import lbug from '@ladybugdb/core';
|
|
21
21
|
import { isReadOnlyDbError, loadFTSExtension } from './lbug-adapter.js';
|
|
22
|
-
import { createLbugDatabase, isWalCorruptionError, WAL_RECOVERY_SUGGESTION, } from './lbug-config.js';
|
|
22
|
+
import { createLbugDatabase, isWalCorruptionError, toNativeSafePath, WAL_RECOVERY_SUGGESTION, } from './lbug-config.js';
|
|
23
23
|
import { isMissingFsError, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, statIfExists, } from './sidecar-recovery.js';
|
|
24
24
|
/**
|
|
25
25
|
* Probe whether a Windows FTS extension binary is locally installed under
|
|
@@ -313,7 +313,7 @@ async function probeDatabaseForShadowReplay(db) {
|
|
|
313
313
|
async function replayShadowPagesWithWritableOpen(dbPath) {
|
|
314
314
|
let db;
|
|
315
315
|
try {
|
|
316
|
-
db = createLbugDatabase(lbug, dbPath, { throwOnWalReplayFailure: false });
|
|
316
|
+
db = createLbugDatabase(lbug, toNativeSafePath(dbPath), { throwOnWalReplayFailure: false });
|
|
317
317
|
await db.init();
|
|
318
318
|
await probeDatabaseForShadowReplay(db);
|
|
319
319
|
}
|
|
@@ -340,7 +340,7 @@ async function openReadOnlyDatabase(dbPath) {
|
|
|
340
340
|
logger: poolSidecarLogger,
|
|
341
341
|
allowQuarantine: true,
|
|
342
342
|
});
|
|
343
|
-
db = createLbugDatabase(lbug, dbPath, {
|
|
343
|
+
db = createLbugDatabase(lbug, toNativeSafePath(dbPath), {
|
|
344
344
|
readOnly: true,
|
|
345
345
|
throwOnWalReplayFailure: false,
|
|
346
346
|
});
|
|
@@ -360,7 +360,7 @@ async function openReadOnlyDatabase(dbPath) {
|
|
|
360
360
|
logger: poolSidecarLogger,
|
|
361
361
|
allowQuarantine: true,
|
|
362
362
|
});
|
|
363
|
-
db = createLbugDatabase(lbug, dbPath, {
|
|
363
|
+
db = createLbugDatabase(lbug, toNativeSafePath(dbPath), {
|
|
364
364
|
readOnly: true,
|
|
365
365
|
throwOnWalReplayFailure: false,
|
|
366
366
|
});
|
|
@@ -374,7 +374,7 @@ async function openReadOnlyDatabase(dbPath) {
|
|
|
374
374
|
await db.close().catch(() => { });
|
|
375
375
|
db = undefined;
|
|
376
376
|
await replayShadowPagesWithWritableOpen(dbPath);
|
|
377
|
-
db = createLbugDatabase(lbug, dbPath, {
|
|
377
|
+
db = createLbugDatabase(lbug, toNativeSafePath(dbPath), {
|
|
378
378
|
readOnly: true,
|
|
379
379
|
throwOnWalReplayFailure: false,
|
|
380
380
|
});
|
package/package.json
CHANGED
|
@@ -59,6 +59,7 @@ const LBUG_NATIVE = [
|
|
|
59
59
|
'test/integration/lbug-close-handle-release.test.ts',
|
|
60
60
|
'test/integration/lbug-orphan-sidecar-recovery.test.ts',
|
|
61
61
|
'test/integration/lbug-readonly-init.test.ts',
|
|
62
|
+
'test/integration/lbug-non-ascii-path.test.ts',
|
|
62
63
|
'test/integration/local-backend.test.ts',
|
|
63
64
|
'test/integration/local-backend-calltool.test.ts',
|
|
64
65
|
'test/integration/search-core.test.ts',
|