From c40474866ace4d838d973840746a136dcbaed8bc Mon Sep 17 00:00:00 2001 From: Rainer Kottenhoff Date: Wed, 25 Feb 2026 15:07:04 +0100 Subject: [PATCH] refactor: File Change Notification handling --- .github/copilot-instructions.md | 1 + Build/Notepad3.ini | 3 +- CLAUDE.md | 1 + src/Config/Config.cpp | 1 + src/Dialogs.c | 18 +++-- src/Helpers.c | 5 ++ src/Notepad3.c | 115 ++++++++++++++++++++++++-------- src/Notepad3.h | 3 +- src/TypeDefs.h | 3 + 9 files changed, 114 insertions(+), 36 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3e78d4126..9fe11b245 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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` diff --git a/Build/Notepad3.ini b/Build/Notepad3.ini index 86abc50be..7543e3b45 100644 --- a/Build/Notepad3.ini +++ b/Build/Notepad3.ini @@ -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] diff --git a/CLAUDE.md b/CLAUDE.md index fd79a1374..b60a6d598 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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\`) diff --git a/src/Config/Config.cpp b/src/Config/Config.cpp index 76fc69732..e1f059cb6 100644 --- a/src/Config/Config.cpp +++ b/src/Config/Config.cpp @@ -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)); diff --git a/src/Dialogs.c b/src/Dialogs.c index d1826cc73..cdc383554 100644 --- a/src/Dialogs.c +++ b/src/Dialogs.c @@ -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); } diff --git a/src/Helpers.c b/src/Helpers.c index 61948e850..2e9a7cae1 100644 --- a/src/Helpers.c +++ b/src/Helpers.c @@ -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); diff --git a/src/Notepad3.c b/src/Notepad3.c index c07ea49d4..e1f356c9a 100644 --- a/src/Notepad3.c +++ b/src/Notepad3.c @@ -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 { diff --git a/src/Notepad3.h b/src/Notepad3.h index 28d6b12a0..bc5168bae 100644 --- a/src/Notepad3.h +++ b/src/Notepad3.h @@ -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 ============================================== diff --git a/src/TypeDefs.h b/src/TypeDefs.h index f8109e04a..310b3bce6 100644 --- a/src/TypeDefs.h +++ b/src/TypeDefs.h @@ -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;