From 10368f8f9e0a2d7d2d6c5889a332ea58271cac95 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 12 Mar 2025 16:06:22 -0700 Subject: [PATCH] Add chokidar watcher settings. --- src/main/ipc/ipcMainConfig.ts | 22 +++- src/main/ipc/ipcMainHandler.settings.ts | 19 ++- src/main/watcher/watcher.ts | 110 ++++++++++++++++++ .../Settings/Settings.WatchedPaths.tsx | 25 +++- .../components/Settings/Settings.Watcher.tsx | 16 +++ .../src/components/Settings/Settings.tsx | 2 + src/util/errorTypeCheck.ts | 4 +- src/util/ipcTypes.json | 7 +- 8 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 src/main/watcher/watcher.ts create mode 100644 src/renderer/src/components/Settings/Settings.Watcher.tsx diff --git a/src/main/ipc/ipcMainConfig.ts b/src/main/ipc/ipcMainConfig.ts index e5feb1c..ccfae5a 100644 --- a/src/main/ipc/ipcMainConfig.ts +++ b/src/main/ipc/ipcMainConfig.ts @@ -1,13 +1,15 @@ import { ipcMain } from "electron"; -import ipcTypes from "../../util/ipcTypes.json"; import log from "electron-log/main"; -import { ipcMainHandleAuthStateChanged } from "./ipcMainHandler.user"; +import ipcTypes from "../../util/ipcTypes.json"; +import { StartWatcher } from "../watcher/watcher"; import { - SettingsWatchedFilePathsGet, SettingsWatchedFilePathsAdd, + SettingsWatchedFilePathsGet, + SettingsWatchedFilePathsRemove, } from "./ipcMainHandler.settings"; -// Log all IPC messages and their payloads +import { ipcMainHandleAuthStateChanged } from "./ipcMainHandler.user"; +// Log all IPC messages and their payloads const logIpcMessages = () => { // Get all message types from ipcTypes.toMain Object.keys(ipcTypes.toMain).forEach((key) => { @@ -37,8 +39,10 @@ ipcMain.on(ipcTypes.toMain.test, (payload: any) => console.log("** Verify that ipcMain is loaded and working.", payload) ); +//Auth handler ipcMain.on(ipcTypes.toMain.authStateChanged, ipcMainHandleAuthStateChanged); +//Settings Handlers ipcMain.handle( ipcTypes.toMain.settings.filepaths.get, SettingsWatchedFilePathsGet @@ -47,4 +51,14 @@ ipcMain.handle( ipcTypes.toMain.settings.filepaths.add, SettingsWatchedFilePathsAdd ); +ipcMain.handle( + ipcTypes.toMain.settings.filepaths.remove, + SettingsWatchedFilePathsRemove +); + +//Watcher Handlers +ipcMain.on(ipcTypes.toMain.watcher.start, () => { + StartWatcher(); +}); + logIpcMessages(); diff --git a/src/main/ipc/ipcMainHandler.settings.ts b/src/main/ipc/ipcMainHandler.settings.ts index 4a1ff97..8e8c5e2 100644 --- a/src/main/ipc/ipcMainHandler.settings.ts +++ b/src/main/ipc/ipcMainHandler.settings.ts @@ -16,16 +16,31 @@ const SettingsWatchedFilePathsAdd = async (event: IpcMainInvokeEvent) => { if (!result.canceled) { Store.set( "settings.filepaths", - _.union(result.filePaths, Store.get("filepaths")) + _.union(result.filePaths, Store.get("settings.filepaths")) ); } return Store.get("settings.filepaths"); }; +const SettingsWatchedFilePathsRemove = async ( + event: IpcMainInvokeEvent, + path: string +) => { + Store.set( + "settings.filepaths", + _.without(Store.get("settings.filepaths"), path) + ); + + return Store.get("settings.filepaths"); +}; const SettingsWatchedFilePathsGet = async (event: IpcMainInvokeEvent) => { const filepaths = Store.get("settings.filepaths"); return filepaths; }; -export { SettingsWatchedFilePathsAdd, SettingsWatchedFilePathsGet }; +export { + SettingsWatchedFilePathsAdd, + SettingsWatchedFilePathsGet, + SettingsWatchedFilePathsRemove, +}; diff --git a/src/main/watcher/watcher.ts b/src/main/watcher/watcher.ts new file mode 100644 index 0000000..a833ee3 --- /dev/null +++ b/src/main/watcher/watcher.ts @@ -0,0 +1,110 @@ +import chokidar, { FSWatcher } from "chokidar"; +import { Notification } from "electron"; +import log from "electron-log/main"; +import path from "path"; +import errorTypeCheck from "../../util/errorTypeCheck"; +import store from "../store/store"; + +var watcher: FSWatcher; + +async function StartWatcher(): Promise { + const filePaths = store.get("settings.filepaths") || []; + log.info("Use polling? ", store.get("settings.polling").enabled); + if (filePaths.length === 0) { + new Notification({ + //TODO: Add Translations + title: "Watcher cannot start", + body: "Please set the appropriate file paths and try again.", + }).show(); + log.warn("Cannot start watcher. No file paths set."); + return false; + } + + if (watcher) { + try { + log.info("Trying to close watcher - it already existed."); + await watcher.close(); + + log.info("Watcher closed successfully!"); + } catch (error) { + log.error("Error trying to close Watcher.", error); + } + } + + watcher = chokidar.watch(filePaths, { + ignored: (filepath, stats) => { + const p = path.parse(filepath); + + return !stats?.isFile() && p.ext !== "" && p.ext.toUpperCase() !== ".ENV"; + }, + usePolling: store.get("settings.polling").enabled || false, + interval: store.get("settings.polling").pollingInterval || 1000, + persistent: true, + ignoreInitial: true, + awaitWriteFinish: { + pollInterval: 500, + stabilityThreshold: 2000, + }, + }); + + watcher + .on("add", async function (path) { + console.log("File", path, "has been added"); + HandleNewFile(path); + }) + // .on("addDir", function (path) { + // console.log("Directory", path, "has been added"); + // }) + .on("change", async function (path) { + console.log("File", path, "has been changed"); + HandleNewFile(path); + }) + // .on("unlink", function (path) { + // console.log("File", path, "has been removed"); + // }) + // .on("unlinkDir", function (path) { + // console.log("Directory", path, "has been removed"); + // }) + .on("error", function (error) { + log.error("Error in Watcher", errorTypeCheck(error)); + }) + .on("ready", onWatcherReady) + .on("raw", function (event, path, details) { + // This event should be triggered everytime something happens. + // console.log("Raw event info:", event, path, details); + }); + + return true; +} + +function onWatcherReady() { + log.info("Watcher ready!"); + // const b = BrowserWindow.getAllWindows()[0]; + // b.webContents.send(ipcTypes.default.fileWatcher.toRenderer.startSuccess); + new Notification({ + title: "Watcher Started", + body: "Newly exported estimates will be automatically uploaded.", + }).show(); + log.info("Confirmed watched paths:", watcher.getWatched()); +} + +async function StopWatcher(): Promise { + if (watcher) { + await watcher.close(); + log.info("Watcher stopped."); + + new Notification({ + title: "RPS Watcher Stopped", + body: "Estimates will not be automatically uploaded.", + }).show(); + return true; + } + return false; +} + +async function HandleNewFile(path) { + //await ImportJob(path); + log.log("Received a new file", path); +} + +export { StartWatcher, StopWatcher, watcher }; diff --git a/src/renderer/src/components/Settings/Settings.WatchedPaths.tsx b/src/renderer/src/components/Settings/Settings.WatchedPaths.tsx index d4d3a4a..282f7a6 100644 --- a/src/renderer/src/components/Settings/Settings.WatchedPaths.tsx +++ b/src/renderer/src/components/Settings/Settings.WatchedPaths.tsx @@ -1,7 +1,8 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import ipcTypes from "../../../../util/ipcTypes.json"; -import { Button } from "antd"; +import { Button, Space } from "antd"; +import { DeleteFilled } from "@ant-design/icons"; const SettingsWatchedPaths: React.FC = () => { const [watchedPaths, setWatchedPaths] = useState([]); @@ -23,10 +24,30 @@ const SettingsWatchedPaths: React.FC = () => { }); }; + const handleRemovePath = (path: string) => { + window.electron.ipcRenderer + .invoke(ipcTypes.toMain.settings.filepaths.remove, path) + .then((paths: string[]) => { + setWatchedPaths(paths); + }); + }; + return (
Currently Watched paths
-
    {watchedPaths?.map((path, index) =>
  • {path}
  • )}
+
    + {watchedPaths?.map((path, index) => ( +
  • + + {path} +
  • + ))} +
); diff --git a/src/renderer/src/components/Settings/Settings.Watcher.tsx b/src/renderer/src/components/Settings/Settings.Watcher.tsx new file mode 100644 index 0000000..5828233 --- /dev/null +++ b/src/renderer/src/components/Settings/Settings.Watcher.tsx @@ -0,0 +1,16 @@ +import { Button } from "antd"; +import { useTranslation } from "react-i18next"; +import ipcTypes from "../../../../util/ipcTypes.json"; + +const SettingsWatcher: React.FC = () => { + const { t } = useTranslation(); + + const handleStart = () => { + window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.start); + }; + + return ( + + ); +}; +export default SettingsWatcher; diff --git a/src/renderer/src/components/Settings/Settings.tsx b/src/renderer/src/components/Settings/Settings.tsx index 8c5b8b8..834f28e 100644 --- a/src/renderer/src/components/Settings/Settings.tsx +++ b/src/renderer/src/components/Settings/Settings.tsx @@ -1,9 +1,11 @@ import SettingsWatchedPaths from "./Settings.WatchedPaths"; +import SettingsWatcher from "./Settings.Watcher"; const Settings: React.FC = () => { return (
+
); }; diff --git a/src/util/errorTypeCheck.ts b/src/util/errorTypeCheck.ts index 3d65c0e..9c97729 100644 --- a/src/util/errorTypeCheck.ts +++ b/src/util/errorTypeCheck.ts @@ -2,7 +2,9 @@ function errorTypeCheck(passedError: any): ParsedError { const errorMessage = passedError instanceof Error ? passedError.message : String(passedError); const errorStack = - passedError instanceof Error ? passedError.stack : "unknown"; + passedError instanceof Error + ? (passedError.stack ?? "") + : String(passedError); return { message: errorMessage, diff --git a/src/util/ipcTypes.json b/src/util/ipcTypes.json index ef585f5..bc1096c 100644 --- a/src/util/ipcTypes.json +++ b/src/util/ipcTypes.json @@ -2,10 +2,15 @@ "toMain": { "test": "toMain_test", "authStateChanged": "toMain_authStateChanged", + "watcher": { + "start": "toMain_watcher_start", + "stop": "toMain_watcher_stop" + }, "settings": { "filepaths": { "get": "toMain_settings_filepaths_get", - "add": "toMain_settings_filepaths_add" + "add": "toMain_settings_filepaths_add", + "remove": "toMain_settings_filepaths_remove" } } },