refactor: File Change Notification handling

This commit is contained in:
Rainer Kottenhoff 2026-02-25 15:07:04 +01:00
parent ae3f50ba1a
commit c40474866a
9 changed files with 114 additions and 36 deletions

View File

@ -157,3 +157,4 @@ Notepad3 follows a **portable-app** design for its configuration file (`Notepad3
- **Key paths**: `Paths.IniFile` = active writable INI (empty if none exists), `Paths.IniFileDefault` = fallback path for "Save Settings Now" recovery.
- **Configuration code**: All INI init logic lives in `src\Config\Config.cpp``FindIniFile()``TestIniFile()``CreateIniFile()``LoadSettings()`.
- **MiniPath** follows the same portable INI and admin-redirect pattern (`minipath\src\Config.cpp`). Redirect targets are auto-created via `CreateIniFileEx()`.
- **New parameters**: When adding new `Settings2` (or other INI) parameters, always document them as commented entries in `Build\Notepad3.ini`

View File

@ -15,6 +15,7 @@ SettingsVersion=5
;filebrowser.exe=minipath.exe
;grepWin.exe=grepWinNP3.exe
;FileCheckInterval=2000 ;(min: 200[msec] - if equal or less, notify immediately)
;FileWatchingMethod=0 ;(0=both[default], 1=poll-only, 2=push-only)
;FileChangedIndicator=[@]
;FileDeletedIndicator=[X]
;FileDlgFilters=
@ -59,7 +60,7 @@ SettingsVersion=5
;AnalyzeReliableConfidenceLevel=90
;LocaleAnsiCodePageAnalysisBonus=33
;LexerSQLNumberSignAsComment=1
;AtomicFileSave=true;
;AtomicFileSave=true
;ExitOnESCSkipLevel=2
;ZoomTooltipTimeout=3200 ;in [msec]
;WrapAroundTooltipTimeout=2000 ;in [msec]

View File

@ -117,6 +117,7 @@ Resource-based MUI system with 27+ locales. Each locale has a `np3_LANG_COUNTRY\
- Key paths: `Paths.IniFile` (active writable INI), `Paths.IniFileDefault` (fallback for recovery)
- INI init flow: `FindIniFile()` -> `TestIniFile()` -> `CreateIniFile()` -> `LoadSettings()`
- **MiniPath** follows the same portable INI and admin-redirect pattern (`minipath\src\Config.cpp`). Redirect targets are auto-created via `CreateIniFileEx()`.
- **New parameters**: When adding new `Settings2` (or other INI) parameters, always document them as commented entries in `Build\Notepad3.ini`
### DarkMode (`src\DarkMode\`)

View File

@ -1313,6 +1313,7 @@ void LoadSettings()
FileWatching.FileCheckInterval = Settings2.FileCheckInterval;
Settings2.FileWatchingMethod = clampi(IniSectionGetInt(IniSecSettings2, L"FileWatchingMethod", 0), 0, 2);
IniSectionGetString(IniSecSettings2, L"FileChangedIndicator", L"[@]", Settings2.FileChangedIndicator, COUNTOF(Settings2.FileChangedIndicator));

View File

@ -2710,6 +2710,7 @@ bool FileMRUDlg(HWND hwnd, HPATHL hFilePath_out)
static INT_PTR CALLBACK ChangeNotifyDlgProc(HWND hwnd, UINT umsg, WPARAM wParam, LPARAM lParam)
{
static FILE_WATCHING_MODE s_FWM = FWM_NO_INIT;
static bool s_wasMonitoring = false;
switch (umsg) {
case WM_INITDIALOG: {
@ -2732,6 +2733,7 @@ static INT_PTR CALLBACK ChangeNotifyDlgProc(HWND hwnd, UINT umsg, WPARAM wParam,
if (s_FWM == FWM_NO_INIT) {
s_FWM = Settings.FileWatchingMode;
}
s_wasMonitoring = FileWatching.MonitoringLog;
CheckDlgButton(hwnd, IDC_CHECK_BOX_A, SetBtn(Settings.ResetFileWatching));
CheckDlgButton(hwnd, IDC_CHECK_BOX_B, SetBtn(FileWatching.MonitoringLog));
@ -2799,8 +2801,7 @@ CASE_WM_CTLCOLOR_SET:
case IDC_CHECK_BOX_B:
FileWatching.MonitoringLog = IsButtonChecked(hwnd, IDC_CHECK_BOX_B);
if (FileWatching.MonitoringLog) {
if (IsButtonChecked(hwnd, IDC_CHECK_BOX_B)) {
CheckRadioButton(hwnd, IDC_RADIO_BTN_A, IDC_RADIO_BTN_E, IDC_RADIO_BTN_C);
EnableItem(hwnd, IDC_RADIO_BTN_A, FALSE);
EnableItem(hwnd, IDC_RADIO_BTN_B, FALSE);
@ -2852,9 +2853,11 @@ CASE_WM_CTLCOLOR_SET:
s_FWM = FWM_EXCLUSIVELOCK;
}
bool const wantMonitoring = IsButtonChecked(hwnd, IDC_CHECK_BOX_B);
Settings.ResetFileWatching = IsButtonChecked(hwnd, IDC_CHECK_BOX_A);
if (!FileWatching.MonitoringLog) {
if (!wantMonitoring) {
FileWatching.FileWatchingMode = s_FWM;
}
if (!Settings.ResetFileWatching) {
@ -2873,8 +2876,13 @@ CASE_WM_CTLCOLOR_SET:
}
}
if (FileWatching.MonitoringLog) {
FileWatching.MonitoringLog = false; // will be toggled in IDM_VIEW_CHASING_DOCTAIL
if (s_wasMonitoring && !wantMonitoring) {
// Turning monitoring OFF — toggle handler restores Settings.FileWatchingMode
PostWMCommand(Globals.hwndMain, IDM_VIEW_CHASING_DOCTAIL);
}
else if (wantMonitoring) {
// Turning ON, or re-entering (settings changed while monitoring)
FileWatching.MonitoringLog = false;
PostWMCommand(Globals.hwndMain, IDM_VIEW_CHASING_DOCTAIL);
}

View File

@ -611,7 +611,12 @@ void BackgroundWorker_Cancel(BackgroundWorker* worker) {
if (IS_VALID_HANDLE(workerThread)) {
// Optimize: MsgDispatch only in case of hwnd ?
// DWORD const wait = SignalObjectAndWait(worker->eventCancel, workerThread, 100 /*INFINITE*/, FALSE);
DWORD const dwTimeout = 5000; // 5 seconds max
DWORD const dwStart = GetTickCount();
while (WaitForSingleObject(workerThread, 0) != WAIT_OBJECT_0) {
if ((GetTickCount() - dwStart) > dwTimeout) {
break; // give up waiting — thread will self-terminate
}
MSG msg;
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);

View File

@ -476,11 +476,13 @@ static FCOBSRVDATA_T s_FileChgObsvrData = INIT_FCOBSRV_T;
// ----------------------------------------------------------------------------
static inline bool IsFileChangedFlagSet() {
return (WaitForSingleObject(s_FileChgObsvrData.hEventFileChanged, 0) != WAIT_TIMEOUT);
return IS_VALID_HANDLE(s_FileChgObsvrData.hEventFileChanged)
&& (WaitForSingleObject(s_FileChgObsvrData.hEventFileChanged, 0) == WAIT_OBJECT_0);
}
static inline bool IsFileDeletedFlagSet() {
return (WaitForSingleObject(s_FileChgObsvrData.hEventFileDeleted, 0) != WAIT_TIMEOUT);
return IS_VALID_HANDLE(s_FileChgObsvrData.hEventFileDeleted)
&& (WaitForSingleObject(s_FileChgObsvrData.hEventFileDeleted, 0) == WAIT_OBJECT_0);
}
static inline bool RaiseFlagIfCurrentFileChanged() {
@ -6306,24 +6308,21 @@ LRESULT MsgCommand(HWND hwnd, UINT umsg, WPARAM wParam, LPARAM lParam)
if (_lastCaretPos == -1) {
_lastCaretPos = SciCall_GetCurrentPos();
}
static FILE_WATCHING_MODE _saveChgNotify = FWM_NO_INIT;
if (_saveChgNotify == FWM_NO_INIT) {
_saveChgNotify = FileWatching.FileWatchingMode;
}
FileWatching.MonitoringLog = !FileWatching.MonitoringLog; // toggle
if (FileWatching.MonitoringLog) {
SetForegroundWindow(hwnd);
_lastCaretPos = SciCall_GetCurrentPos();
_saveChgNotify = FileWatching.FileWatchingMode;
FileWatching.FileWatchingMode = FWM_AUTORELOAD;
SciCall_SetEndAtLastLine(false); // false(!)
FileRevert(Paths.CurrentFile, true);
SciCall_SetReadOnly(FileWatching.MonitoringLog);
}
else {
FileWatching.FileWatchingMode = _saveChgNotify;
KillTimer(hwnd, ID_LOGROTATETIMER); // cancel any pending log rotation retry
FileWatching.LogRotateRetryCount = 0;
FileWatching.FileWatchingMode = Settings.FileWatchingMode;
SciCall_SetEndAtLastLine(!Settings.ScrollPastEOF);
SciCall_SetReadOnly(Settings.DocReadOnlyMode);
SciCall_GotoPos(_lastCaretPos);
@ -10902,11 +10901,16 @@ static inline bool IsFileVarLogFile()
}
static inline void _ResetFileWatchingMode() {
FileWatching.FileWatchingMode = (s_flagChangeNotify != FWM_NO_INIT) ? s_flagChangeNotify : Settings.FileWatchingMode;
if (FileWatching.MonitoringLog) {
FileWatching.FileWatchingMode = FWM_AUTORELOAD;
PostWMCommand(Globals.hwndMain, IDM_VIEW_CHASING_DOCTAIL);
KillTimer(Globals.hwndMain, ID_LOGROTATETIMER);
FileWatching.LogRotateRetryCount = 0;
FileWatching.MonitoringLog = false;
SciCall_SetEndAtLastLine(!Settings.ScrollPastEOF);
SciCall_SetReadOnly(Settings.DocReadOnlyMode);
CheckCmd(GetMenu(Globals.hwndMain), IDM_VIEW_CHASING_DOCTAIL, false);
}
FileWatching.FileWatchingMode = Settings.FileWatchingMode;
ResetFileObservationData(true);
}
@ -11264,7 +11268,7 @@ bool FileRevert(const HPATHL hfile_pth, bool bIgnoreCmdLnEnc)
if (result) {
bool bPreserveView = !IsFileVarLogFile();
if (FileWatching.FileWatchingMode == FWM_AUTORELOAD) {
if (bIsAtDocEnd || FileWatching.MonitoringLog || (s_flagChangeNotify == FWM_AUTORELOAD)) {
if (bIsAtDocEnd || (s_flagChangeNotify == FWM_AUTORELOAD)) {
bPreserveView = false;
}
}
@ -12252,6 +12256,9 @@ void CALLBACK PasteBoardTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD
}
// forward declaration for LogRotateTimerProc (defined after InstallFileWatching)
static void CALLBACK LogRotateTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);
//=============================================================================
//
// MsgFileChangeNotify() - Handles WM_FILECHANGEDNOTIFY
@ -12337,19 +12344,34 @@ LRESULT MsgFileChangeNotify(HWND hwnd, WPARAM wParam, LPARAM lParam)
}
else { // file has been deleted
InstallFileWatching(false); // terminate
if (FileWatching.FileWatchingMode == FWM_MSGBOX) {
if (IsYesOkay(InfoBoxLng(MB_YESNO | MB_ICONWARNING, NULL, IDS_MUI_FILECHANGENOTIFY2))) {
FileSave(FSF_SaveAlways);
}
else {
SetSaveNeeded(true);
}
// Brief delay to handle atomic save (delete + rename) pattern
Sleep(100);
if (Path_IsExistingFile(Paths.CurrentFile)) {
// File was restored (atomic save) — re-process as modification
ResetFileObservationData(true);
PostMessage(Globals.hwndMain, WM_FILECHANGEDNOTIFY, 0, 0);
}
else {
// FWM_INDICATORSILENT: nothing todo here
SetSaveNeeded(true);
InstallFileWatching(false); // truly deleted — terminate
if (FileWatching.MonitoringLog) {
// File deleted while monitoring — start retry timer for log rotation recovery
FileWatching.LogRotateRetryCount = 0;
SetTimer(Globals.hwndMain, ID_LOGROTATETIMER, 500, LogRotateTimerProc);
SetSaveNeeded(true);
}
else if (FileWatching.FileWatchingMode == FWM_MSGBOX) {
if (IsYesOkay(InfoBoxLng(MB_YESNO | MB_ICONWARNING, NULL, IDS_MUI_FILECHANGENOTIFY2))) {
FileSave(FSF_SaveAlways);
}
else {
SetSaveNeeded(true);
}
}
else {
// FWM_INDICATORSILENT: nothing todo here
SetSaveNeeded(true);
}
}
}
@ -12383,7 +12405,7 @@ static void CALLBACK WatchTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWOR
UNREFERENCED_PARAMETER(uMsg);
UNREFERENCED_PARAMETER(hwnd);
LONG64 const diff = (GetTicks_ms() - InterlockedOr64(&(s_FileChgObsvrData.iFileChangeNotifyTime), 0LL));
LONG64 const diff = (GetTicks_ms() - InterlockedCompareExchange64(&(s_FileChgObsvrData.iFileChangeNotifyTime), 0LL, 0LL));
// Directory-Observer is not notified for continuously updated (log-)files
if (diff > FileWatching.FileCheckInterval) {
NotifyIfFileHasChanged();
@ -12391,6 +12413,32 @@ static void CALLBACK WatchTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWOR
}
// ----------------------------------------------------------------------------
#define LOG_ROTATE_MAX_RETRIES 20 // 20 * 500ms = 10 seconds
static void CALLBACK LogRotateTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) {
UNREFERENCED_PARAMETER(dwTime);
UNREFERENCED_PARAMETER(idEvent);
UNREFERENCED_PARAMETER(uMsg);
UNREFERENCED_PARAMETER(hwnd);
if (Path_IsExistingFile(Paths.CurrentFile)) {
// File reappeared (log rotation complete) — resume monitoring
KillTimer(Globals.hwndMain, ID_LOGROTATETIMER);
FileWatching.LogRotateRetryCount = 0;
ResetFileObservationData(true);
PostMessage(Globals.hwndMain, WM_FILECHANGEDNOTIFY, 0, 0);
InstallFileWatching(true);
}
else if (++FileWatching.LogRotateRetryCount >= LOG_ROTATE_MAX_RETRIES) {
// Timeout — file did not reappear, cleanly exit monitoring mode
KillTimer(Globals.hwndMain, ID_LOGROTATETIMER);
FileWatching.LogRotateRetryCount = 0;
PostWMCommand(Globals.hwndMain, IDM_VIEW_CHASING_DOCTAIL);
}
}
// ----------------------------------------------------------------------------
unsigned int WINAPI FileChangeObserver(LPVOID lpParam)
{
@ -12409,6 +12457,11 @@ unsigned int WINAPI FileChangeObserver(LPVOID lpParam)
FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE |
FILE_NOTIFY_CHANGE_LAST_WRITE);
if (!IS_VALID_HANDLE(pFCOBSVData->hFileChanged)) {
BackgroundWorker_End(worker, 1);
return 1;
}
while (BackgroundWorker_Continue(worker)) {
switch (WaitForSingleObject(pFCOBSVData->hFileChanged, 100)) {
@ -12420,7 +12473,7 @@ unsigned int WINAPI FileChangeObserver(LPVOID lpParam)
case WAIT_OBJECT_0:
// check if current file is trigger for directory notification
if (RaiseFlagIfCurrentFileChanged()) {
if (FileWatching.FileCheckInterval <= MIN_FC_POLL_INTERVAL) {
if ((Settings2.FileWatchingMethod == FWMTH_PUSH) || (FileWatching.FileCheckInterval <= MIN_FC_POLL_INTERVAL)) {
NotifyIfFileHasChanged(); // immediate notification
}
}
@ -12488,19 +12541,23 @@ void InstallFileWatching(const bool bInstall) {
if (bWatchFile) {
bool const bUsePush = (Settings2.FileWatchingMethod != FWMTH_POLL); // both or push
bool const bUsePoll = (Settings2.FileWatchingMethod != FWMTH_PUSH); // both or poll
if (!IS_VALID_HANDLE(s_FileChgObsvrData.worker.workerThread)) {
// Save data of current file
ResetFileObservationData(false); // (!) false
Path_Reset(s_FileChgObsvrData.worker.hFilePath, Path_Get(hdir_pth)); // directory monitoring
BackgroundWorker_Start(&(s_FileChgObsvrData.worker), FileChangeObserver, &s_FileChgObsvrData);
if (bUsePush) {
Path_Reset(s_FileChgObsvrData.worker.hFilePath, Path_Get(hdir_pth)); // directory monitoring
BackgroundWorker_Start(&(s_FileChgObsvrData.worker), FileChangeObserver, &s_FileChgObsvrData);
}
}
InterlockedExchange64(&(s_FileChgObsvrData.iFileChangeNotifyTime), GetTicks_ms());
if (Settings2.FileCheckInterval > 0) {
if (bUsePoll && (Settings2.FileCheckInterval > 0)) {
SetTimer(Globals.hwndMain, ID_WATCHTIMER, (UINT)FileWatching.FileCheckInterval, WatchTimerProc);
}
else {

View File

@ -66,7 +66,8 @@ np3params, *LPnp3params;
//==== Timer ==================================================================
#define ID_WATCHTIMER (0xA000) // File Watching
#define ID_PASTEBOARDTIMER (0xA001) // Paste Board
#define ID_AUTOSAVETIMER (0xA002) // Paste Board
#define ID_AUTOSAVETIMER (0xA002) // Auto Save Timer
#define ID_LOGROTATETIMER (0xA003) // Log Rotation Retry
//==== Reuse Window Lock Timeout ==============================================

View File

@ -174,6 +174,7 @@ typedef COLORREF COLORALPHAREF;
typedef enum COLOR_LAYER { BACKGROUND_LAYER = 0, FOREGROUND_LAYER = 1 } COLOR_LAYER; // Style_GetColor()
typedef enum HYPERLINK_OPS { OPEN_WITH_BROWSER = 1, OPEN_IN_NOTEPAD3 = (1<<1), OPEN_NEW_NOTEPAD3 = (1<<2), COPY_HYPERLINK = (1<<3), SELECT_HYPERLINK = (1<<4) } HYPERLINK_OPS; // Hyperlink Operations
typedef enum FILE_WATCHING_MODE { FWM_NO_INIT = -1, FWM_DONT_CARE = 0, FWM_INDICATORSILENT = 1, FWM_MSGBOX = 2, FWM_AUTORELOAD = 3, FWM_EXCLUSIVELOCK = 4 } FILE_WATCHING_MODE;
typedef enum FILE_WATCHING_METHOD { FWMTH_BOTH = 0, FWMTH_POLL = 1, FWMTH_PUSH = 2 } FILE_WATCHING_METHOD;
typedef enum FOCUSVIEW_MARKER_MODE { FVMM_MARGIN = 1, FVMM_LN_BACKGR = 2, FVMM_FOLD = 4 } FOCUSVIEW_MARKER_MODE;
typedef enum DEFAULT_FONT_STYLES { DFS_GLOBAL = 0,
DFS_CURR_LEXER = 1,
@ -765,6 +766,7 @@ typedef struct SETTINGS2_T {
int OpacityLevel;
int FindReplaceOpacityLevel;
LONG64 FileCheckInterval;
int FileWatchingMethod;
LONG64 UndoTransactionTimeout;
int IMEInteraction;
int SciFontQuality;
@ -889,6 +891,7 @@ typedef struct FILEWATCHING_T {
FILE_WATCHING_MODE FileWatchingMode; // <-> Settings.FileWatchingMode;
LONG64 FileCheckInterval; // <-> clampll(Settings2.FileCheckInterval, MIN_FC_POLL_INTERVAL, MAX_FC_POLL_INTERVAL);
bool MonitoringLog;
int LogRotateRetryCount;
} FILEWATCHING_T, *PFILEWATCHING_T;