merge: integrate upstream/main (v1.2.15) into feature/webui
- Resolved conflict in src/constants.js: kept config-driven approach - Adopted upstream 10-second cooldown default - Added MAX_EMPTY_RESPONSE_RETRIES constant from upstream - Incorporated new test files and GitHub issue templates
This commit is contained in:
162
src/utils/native-module-helper.js
Normal file
162
src/utils/native-module-helper.js
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Native Module Helper
|
||||
* Detects and auto-rebuilds native Node.js modules when they become
|
||||
* incompatible after a Node.js version update.
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { dirname, join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* Check if an error is a NODE_MODULE_VERSION mismatch error
|
||||
* @param {Error} error - The error to check
|
||||
* @returns {boolean} True if it's a version mismatch error
|
||||
*/
|
||||
export function isModuleVersionError(error) {
|
||||
const message = error?.message || '';
|
||||
return message.includes('NODE_MODULE_VERSION') &&
|
||||
message.includes('was compiled against a different Node.js version');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the module path from a NODE_MODULE_VERSION error message
|
||||
* @param {Error} error - The error containing the module path
|
||||
* @returns {string|null} The path to the .node file, or null if not found
|
||||
*/
|
||||
export function extractModulePath(error) {
|
||||
const message = error?.message || '';
|
||||
// Match pattern like: "The module '/path/to/module.node'"
|
||||
const match = message.match(/The module '([^']+\.node)'/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the package root directory from a .node file path
|
||||
* @param {string} nodeFilePath - Path to the .node file
|
||||
* @returns {string|null} Path to the package root, or null if not found
|
||||
*/
|
||||
export function findPackageRoot(nodeFilePath) {
|
||||
// Walk up from the .node file to find package.json
|
||||
let dir = dirname(nodeFilePath);
|
||||
while (dir) {
|
||||
const packageJsonPath = join(dir, 'package.json');
|
||||
if (existsSync(packageJsonPath)) {
|
||||
return dir;
|
||||
}
|
||||
const parentDir = dirname(dir);
|
||||
// Stop when we've reached the filesystem root (dirname returns same path)
|
||||
if (parentDir === dir) {
|
||||
break;
|
||||
}
|
||||
dir = parentDir;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to rebuild a native module
|
||||
* @param {string} packagePath - Path to the package root directory
|
||||
* @returns {boolean} True if rebuild succeeded, false otherwise
|
||||
*/
|
||||
export function rebuildModule(packagePath) {
|
||||
try {
|
||||
logger.info(`[NativeModule] Rebuilding native module at: ${packagePath}`);
|
||||
|
||||
// Run npm rebuild in the package directory
|
||||
const output = execSync('npm rebuild', {
|
||||
cwd: packagePath,
|
||||
stdio: 'pipe', // Capture output instead of printing
|
||||
timeout: 120000 // 2 minute timeout
|
||||
});
|
||||
|
||||
// Log rebuild output for debugging
|
||||
const outputStr = output?.toString().trim();
|
||||
if (outputStr) {
|
||||
logger.debug(`[NativeModule] Rebuild output:\n${outputStr}`);
|
||||
}
|
||||
|
||||
logger.success('[NativeModule] Rebuild completed successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Include stdout/stderr from the failed command for troubleshooting
|
||||
const stdout = error.stdout?.toString().trim();
|
||||
const stderr = error.stderr?.toString().trim();
|
||||
let errorDetails = `[NativeModule] Rebuild failed: ${error.message}`;
|
||||
if (stdout) {
|
||||
errorDetails += `\n[NativeModule] stdout: ${stdout}`;
|
||||
}
|
||||
if (stderr) {
|
||||
errorDetails += `\n[NativeModule] stderr: ${stderr}`;
|
||||
}
|
||||
logger.error(errorDetails);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to auto-rebuild a native module from an error
|
||||
* @param {Error} error - The NODE_MODULE_VERSION error
|
||||
* @returns {boolean} True if rebuild succeeded, false otherwise
|
||||
*/
|
||||
export function attemptAutoRebuild(error) {
|
||||
const nodePath = extractModulePath(error);
|
||||
if (!nodePath) {
|
||||
logger.error('[NativeModule] Could not extract module path from error');
|
||||
return false;
|
||||
}
|
||||
|
||||
const packagePath = findPackageRoot(nodePath);
|
||||
if (!packagePath) {
|
||||
logger.error('[NativeModule] Could not find package root');
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.warn('[NativeModule] Native module version mismatch detected');
|
||||
logger.info('[NativeModule] Attempting automatic rebuild...');
|
||||
|
||||
return rebuildModule(packagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively clear a module and its dependencies from the require cache
|
||||
* This is needed after rebuilding a native module to force re-import
|
||||
* @param {string} modulePath - Resolved path to the module
|
||||
* @param {object} cache - The require.cache object
|
||||
* @param {Set} [visited] - Set of already-visited paths to prevent cycles
|
||||
*/
|
||||
export function clearRequireCache(modulePath, cache, visited = new Set()) {
|
||||
if (visited.has(modulePath)) return;
|
||||
visited.add(modulePath);
|
||||
|
||||
const mod = cache[modulePath];
|
||||
if (!mod) return;
|
||||
|
||||
// Recursively clear children first
|
||||
if (mod.children) {
|
||||
for (const child of mod.children) {
|
||||
clearRequireCache(child.id, cache, visited);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from parent's children array
|
||||
if (mod.parent && mod.parent.children) {
|
||||
const idx = mod.parent.children.indexOf(mod);
|
||||
if (idx !== -1) {
|
||||
mod.parent.children.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete from cache
|
||||
delete cache[modulePath];
|
||||
}
|
||||
|
||||
export default {
|
||||
isModuleVersionError,
|
||||
extractModulePath,
|
||||
findPackageRoot,
|
||||
rebuildModule,
|
||||
attemptAutoRebuild,
|
||||
clearRequireCache
|
||||
};
|
||||
Reference in New Issue
Block a user