[bug] Add watchWithFallback to support fs.watch fallback on NFS; For bug 75439

This commit is contained in:
Sergey Konovalov
2025-07-14 19:56:42 +03:00
parent 6934af4758
commit 9d406d2b59
3 changed files with 82 additions and 18 deletions

View File

@ -99,15 +99,21 @@ async function saveConfig(ctx, config) {
}
/**
* Handle configuration file change events
* @param {string} eventType - Type of file system event (change, rename)
* @param {string} filename - Name of the file that triggered the event
* Supports both fs.watch (eventType, filename) and fs.watchFile (current, previous) callbacks
*/
function handleConfigFileChange(eventType, filename) {
function handleConfigFileChange(eventTypeOrCurrent, filenameOrPrevious) {
try {
if (configFileName === filename) {
let shouldReload = false;
if (typeof eventTypeOrCurrent === 'object' && eventTypeOrCurrent.isFile) {
shouldReload = eventTypeOrCurrent.mtime !== filenameOrPrevious.mtime;
operationContext.global.logger.info(`handleConfigFileChange reloaded=${shouldReload} watchFile: ${configFileName}`);
} else {
shouldReload = configFileName === filenameOrPrevious;
operationContext.global.logger.info(`handleConfigFileChange reloaded=${shouldReload} watch ${eventTypeOrCurrent}: ${filenameOrPrevious}`);
}
if (shouldReload) {
nodeCache.del(configFileName);
operationContext.global.logger.info(`handleConfigFileChange configuration ${eventType}: ${configFileName}`);
}
} catch (err) {
operationContext.global.logger.error(`handleConfigFileChange error: ${err.message}`);
@ -117,21 +123,13 @@ function handleConfigFileChange(eventType, filename) {
/**
* Initialize the configuration directory watcher
*/
function initRuntimeConfigWatcher(ctx) {
async function initRuntimeConfigWatcher(ctx) {
if (!configFilePath) {
ctx.logger.info(`runtimeConfig.filePath is not specified`);
return;
}
try {
const configDir = path.dirname(configFilePath);
const watcher = fsWatch.watch(configDir, handleConfigFileChange);
watcher.on('error', (err) => {
ctx.logger.warn(`initRuntimeConfigWatcher error: ${err.message}`);
});
ctx.logger.info(`watching for runtime config changes in: ${configDir}`);
} catch (watchErr) {
ctx.logger.warn(`initRuntimeConfigWatcher error: ${watchErr.message}`);
}
const configDir = path.dirname(configFilePath);
await utils.watchWithFallback(ctx, configDir, configFilePath, handleConfigFileChange);
}
module.exports = {
initRuntimeConfigWatcher,

View File

@ -40,6 +40,7 @@ const { buffer } = require('node:stream/consumers');
const { Transform } = require('stream');
var config = require('config');
var fs = require('fs');
const fsPromises = require('node:fs/promises');
var path = require('path');
const crypto = require('crypto');
var url = require('url');
@ -1361,3 +1362,66 @@ exports.isObject = isObject;
exports.deepMergeObjects = deepMergeObjects;
exports.NodeCache = NodeCache;//todo via require
//like suggestion in https://github.com/paulmillr/chokidar/issues/242#issuecomment-76205459
const UNSAFE_MAGIC = new Set([
0x6969, // NFS
0xFF534D42, // CIFS/SMB1
0xFE534D42, // SMB2+
0x517B, // legacy SMB
0x65735546, // FUSE
0x794C7630, // overlayfs
0x00C36400, // CephFS
0x73757245, // Coda
0x6B414653 // AFS
]);
/**
* Gets the file system type for the given path
* @param {operationContext} ctx - Operation context
* @param {string} path - Path to check
* @returns {Promise<number>} File system type
*/
async function getFsType(ctx, path) {
try {
const statfs = await fsPromises.statfs(path);
const fsType = Number(statfs.type);
ctx.logger.info(`getFsType fs type=${fsType} ${path}`);
return fsType;
} catch (err) {
ctx.logger.info(`getFsType error: ${path}: ${err.message}`);
return null;
}
}
/**
* File watcher with native events fallback to polling
* @param {operationContext} ctx - Operation context
* @param {string} dirPath - Directory path to watch
* @param {string} filePath - File path to watch
* @param {Function} listener - Change event callback
* @param {Object} opts - Options
* @returns {Promise<fs.FSWatcher|fs.StatWatcher>} Watcher instance
*/
exports.watchWithFallback = async function watchWithFallback(ctx, dirPath, filePath, listener, opts = {}) {
const fsType = await getFsType(ctx, dirPath);
if (null === fsType || UNSAFE_MAGIC.has(fsType)) {
ctx.logger.info(`watchWithFallback fs type=${fsType} unsupport watch. fallback to watchFile ${filePath}`);
return fs.watchFile(filePath, opts, listener);
}
//Try native watch
try {
const watcher = fs.watch(dirPath, opts, listener);
watcher.on('error', (err) => {
watcher.close();
ctx.logger.info(`watchWithFallback error ${dirPath} fallback to watchFile ${filePath}: ${err.message}`);
fs.watchFile(filePath, opts, listener);
});
ctx.logger.info(`watchWithFallback watch: ${dirPath}`);
return watcher;
} catch (err) {
ctx.logger.info(`watchWithFallback error ${dirPath} fallback to watchFile ${filePath}: ${err.message}`);
return fs.watchFile(filePath, opts, listener);
}
}

View File

@ -4011,7 +4011,9 @@ exports.install = function(server, callbackFunction) {
});
//Initialize watch here to avoid circular import with operationContext
runtimeConfigManager.initRuntimeConfigWatcher(operationContext.global);
runtimeConfigManager.initRuntimeConfigWatcher(operationContext.global).catch(err => {
operationContext.global.logger.warn('initRuntimeConfigWatcher error: %s', err.stack);
});
void aiProxyHandler.getPluginSettings(operationContext.global);
};
exports.setLicenseInfo = async function(globalCtx, data, original) {