feature/IO-3205-Paint-Scale-Integrations: Pre Protocol Handler Keep ALive.
This commit is contained in:
@@ -1,2 +0,0 @@
|
|||||||
VITE_FIREBASE_CONFIG={"apiKey":"AIzaSyDSezy-jGJreo7ulgpLdlpOwAOrgcaEkhU","authDomain":"imex-prod.firebaseapp.com","databaseURL":"https://imex-prod.firebaseio.com","projectId":"imex-prod","storageBucket":"imex-prod.appspot.com","messagingSenderId":"253497221485","appId":"1:253497221485:web:3c81c483b94db84b227a64","measurementId":"G-NTWBKG2L0M"}
|
|
||||||
VITE_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
|
|
||||||
@@ -26,6 +26,8 @@ nsis:
|
|||||||
shortcutName: ${productName}
|
shortcutName: ${productName}
|
||||||
uninstallDisplayName: ${productName}
|
uninstallDisplayName: ${productName}
|
||||||
createDesktopShortcut: always
|
createDesktopShortcut: always
|
||||||
|
requestExecutionLevel: admin # Ensure elevated privileges
|
||||||
|
include: "scripts/installer.nsh" # Reference NSIS script from scripts directory
|
||||||
mac:
|
mac:
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
category: public.app-category.business
|
category: public.app-category.business
|
||||||
@@ -34,6 +36,11 @@ mac:
|
|||||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
|
- CFBundleURLTypes:
|
||||||
|
- CFBundleTypeRole: Viewer # More specific role for protocol handling
|
||||||
|
CFBundleURLName: com.convenientbrands.bodyshop-desktop-imex
|
||||||
|
CFBundleURLSchemes:
|
||||||
|
- imexmedia
|
||||||
target:
|
target:
|
||||||
- target: default
|
- target: default
|
||||||
arch:
|
arch:
|
||||||
@@ -50,12 +57,16 @@ linux:
|
|||||||
- deb
|
- deb
|
||||||
maintainer: electronjs.org
|
maintainer: electronjs.org
|
||||||
category: Utility
|
category: Utility
|
||||||
|
desktop:
|
||||||
|
MimeType: x-scheme-handler/imexmedia;
|
||||||
|
afterInstall: |
|
||||||
|
# Install .desktop file for protocol handling
|
||||||
|
cp scripts/imex-shop-partner.desktop $HOME/.local/share/applications/
|
||||||
|
update-desktop-database $HOME/.local/share/applications/
|
||||||
appImage:
|
appImage:
|
||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: ${name}-${version}.${ext}
|
||||||
npmRebuild: false
|
npmRebuild: false
|
||||||
publish:
|
publish:
|
||||||
provider: s3
|
provider: s3
|
||||||
bucket: imex-partner
|
bucket: imex-partner
|
||||||
region: ca-central-1
|
region: ca-central-1
|
||||||
# electronDownload:
|
|
||||||
# mirror: https://npmmirror.com/mirrors/electron/
|
|
||||||
@@ -26,6 +26,8 @@ nsis:
|
|||||||
shortcutName: ${productName}
|
shortcutName: ${productName}
|
||||||
uninstallDisplayName: ${productName}
|
uninstallDisplayName: ${productName}
|
||||||
createDesktopShortcut: always
|
createDesktopShortcut: always
|
||||||
|
requestExecutionLevel: admin # Ensure elevated privileges
|
||||||
|
include: "scripts/installer.nsh" # Reference NSIS script from scripts directory
|
||||||
mac:
|
mac:
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
category: public.app-category.business
|
category: public.app-category.business
|
||||||
@@ -34,6 +36,11 @@ mac:
|
|||||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
|
- CFBundleURLTypes:
|
||||||
|
- CFBundleTypeRole: Viewer # More specific role for protocol handling
|
||||||
|
CFBundleURLName: com.convenientbrands.bodyshop-desktop-rome
|
||||||
|
CFBundleURLSchemes:
|
||||||
|
- imexmedia
|
||||||
target:
|
target:
|
||||||
- target: default
|
- target: default
|
||||||
arch:
|
arch:
|
||||||
@@ -50,12 +57,16 @@ linux:
|
|||||||
- deb
|
- deb
|
||||||
maintainer: electronjs.org
|
maintainer: electronjs.org
|
||||||
category: Utility
|
category: Utility
|
||||||
|
desktop:
|
||||||
|
MimeType: x-scheme-handler/imexmedia;
|
||||||
|
afterInstall: |
|
||||||
|
# Install .desktop file for protocol handling
|
||||||
|
cp scripts/rome-shop-partner.desktop $HOME/.local/share/applications/
|
||||||
|
update-desktop-database $HOME/.local/share/applications/
|
||||||
appImage:
|
appImage:
|
||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: ${name}-${version}.${ext}
|
||||||
npmRebuild: false
|
npmRebuild: false
|
||||||
publish:
|
publish:
|
||||||
provider: s3
|
provider: s3
|
||||||
bucket: rome-partner
|
bucket: rome-partner
|
||||||
region: us-east-2
|
region: us-east-2
|
||||||
# electronDownload:
|
|
||||||
# mirror: https://npmmirror.com/mirrors/electron/
|
|
||||||
8
scripts/imex-shop-partner.desktop
Normal file
8
scripts/imex-shop-partner.desktop
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=ImEX Shop Partner
|
||||||
|
Exec=/opt/ImEX%20Shop%20Partner/ShopPartner %u
|
||||||
|
Type=Application
|
||||||
|
Terminal=false
|
||||||
|
Icon=imex-shop-partner
|
||||||
|
Categories=Utility;
|
||||||
|
MimeType=x-scheme-handler/imexmedia;
|
||||||
13
scripts/installer.nsh
Normal file
13
scripts/installer.nsh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
!macro customInstall
|
||||||
|
; Register imexmedia protocol
|
||||||
|
WriteRegStr HKCR "imexmedia" "" "URL:ImEX Shop Partner Protocol"
|
||||||
|
WriteRegStr HKCR "imexmedia" "URL Protocol" ""
|
||||||
|
WriteRegStr HKCR "imexmedia\\shell" "" ""
|
||||||
|
WriteRegStr HKCR "imexmedia\\shell\\open" "" ""
|
||||||
|
WriteRegStr HKCR "imexmedia\\shell\\open\\command" "" '"$INSTDIR\ShopPartner.exe" "%1"'
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro customUnInstall
|
||||||
|
; Remove imexmedia protocol
|
||||||
|
DeleteRegKey HKCR "imexmedia"
|
||||||
|
!macroend
|
||||||
8
scripts/rome-shop-partner.desktop
Normal file
8
scripts/rome-shop-partner.desktop
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=Rome Shop Partner
|
||||||
|
Exec=/opt/Rome%20Shop%20Partner/ShopPartner %u
|
||||||
|
Type=Application
|
||||||
|
Terminal=false
|
||||||
|
Icon=rome-shop-partner
|
||||||
|
Categories=Utility;
|
||||||
|
MimeType=x-scheme-handler/imexmedia;
|
||||||
@@ -25,6 +25,14 @@ import store from "./store/store";
|
|||||||
import { checkForAppUpdates } from "./util/checkForAppUpdates";
|
import { checkForAppUpdates } from "./util/checkForAppUpdates";
|
||||||
import { getMainWindow } from "./util/toRenderer";
|
import { getMainWindow } from "./util/toRenderer";
|
||||||
import { GetAllEnvFiles } from "./watcher/watcher";
|
import { GetAllEnvFiles } from "./watcher/watcher";
|
||||||
|
import {
|
||||||
|
isKeepAliveAgentInstalled,
|
||||||
|
setupKeepAliveAgent,
|
||||||
|
} from "./setup-keep-alive-agent";
|
||||||
|
import {
|
||||||
|
isKeepAliveTaskInstalled,
|
||||||
|
setupKeepAliveTask,
|
||||||
|
} from "./setup-keep-alive-task";
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: "https://ba41d22656999a8c1fd63bcb7df98650@o492140.ingest.us.sentry.io/4509074139447296",
|
dsn: "https://ba41d22656999a8c1fd63bcb7df98650@o492140.ingest.us.sentry.io/4509074139447296",
|
||||||
@@ -34,6 +42,7 @@ log.initialize();
|
|||||||
const isMac: boolean = process.platform === "darwin";
|
const isMac: boolean = process.platform === "darwin";
|
||||||
const protocol: string = "imexmedia";
|
const protocol: string = "imexmedia";
|
||||||
let isAppQuitting = false; //Needed on Mac as an override to allow us to fully quit the app.
|
let isAppQuitting = false; //Needed on Mac as an override to allow us to fully quit the app.
|
||||||
|
let isKeepAliveLaunch = false; // Track if launched via keep-alive
|
||||||
// Initialize the server
|
// Initialize the server
|
||||||
const localServer = new LocalServer();
|
const localServer = new LocalServer();
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
@@ -52,7 +61,7 @@ function createWindow(): void {
|
|||||||
height,
|
height,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
show: false,
|
show: false, // Start hidden, show later if not keep-alive
|
||||||
minWidth: 600,
|
minWidth: 600,
|
||||||
minHeight: 400,
|
minHeight: 400,
|
||||||
//autoHideMenuBar: true,
|
//autoHideMenuBar: true,
|
||||||
@@ -192,7 +201,14 @@ function createWindow(): void {
|
|||||||
label: "Open Log File",
|
label: "Open Log File",
|
||||||
click: (): void => {
|
click: (): void => {
|
||||||
/* action for item 1 */
|
/* action for item 1 */
|
||||||
shell.openPath(log.transports.file.getFile().path);
|
shell
|
||||||
|
.openPath(log.transports.file.getFile().path)
|
||||||
|
.catch((error) => {
|
||||||
|
log.error(
|
||||||
|
"Failed to open log file:",
|
||||||
|
errorTypeCheck(error),
|
||||||
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -204,7 +220,12 @@ function createWindow(): void {
|
|||||||
{
|
{
|
||||||
label: "Open Config Folder",
|
label: "Open Config Folder",
|
||||||
click: (): void => {
|
click: (): void => {
|
||||||
shell.openPath(path.dirname(store.path));
|
shell.openPath(path.dirname(store.path)).catch((error) => {
|
||||||
|
log.error(
|
||||||
|
"Failed to open config folder:",
|
||||||
|
errorTypeCheck(error),
|
||||||
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -218,13 +239,34 @@ function createWindow(): void {
|
|||||||
{
|
{
|
||||||
type: "separator",
|
type: "separator",
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// label: "Decode Hardcoded Estimate",
|
// label: "Decode Hardcoded Estimate",
|
||||||
// click: (): void => {
|
// click: (): void => {
|
||||||
// ImportJob(`C:\\EMS\\CCC\\9ee762f4.ENV`);
|
// ImportJob(`C:\\EMS\\CCC\\9ee762f4.ENV`);
|
||||||
// },
|
// },
|
||||||
// },
|
// },
|
||||||
|
{
|
||||||
|
label: "Install Keep Alive",
|
||||||
|
enabled: true, // Default to enabled, update dynamically
|
||||||
|
click: async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (platform.isWindows) {
|
||||||
|
await setupKeepAliveTask();
|
||||||
|
log.info("Successfully installed Windows keep-alive task");
|
||||||
|
} else if (platform.isMacOS) {
|
||||||
|
await setupKeepAliveAgent();
|
||||||
|
log.info("Successfully installed macOS keep-alive agent");
|
||||||
|
}
|
||||||
|
// Rebuild menu and update enabled state
|
||||||
|
await updateKeepAliveMenuItem();
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
`Failed to install keep-alive: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
// Optionally notify user (e.g., via dialog or log)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Add All Estimates in watched directories",
|
label: "Add All Estimates in watched directories",
|
||||||
click: (): void => {
|
click: (): void => {
|
||||||
@@ -254,8 +296,45 @@ function createWindow(): void {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Dynamically update Install Keep Alive enabled state
|
||||||
|
const updateKeepAliveMenuItem = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const isInstalled = platform.isWindows
|
||||||
|
? await isKeepAliveTaskInstalled()
|
||||||
|
: platform.isMacOS
|
||||||
|
? await isKeepAliveAgentInstalled()
|
||||||
|
: false;
|
||||||
|
const developmentMenu = template
|
||||||
|
.find((item) => item.label === "Application")
|
||||||
|
// @ts-ignore
|
||||||
|
?.submenu?.find((item: { id: string }) => item.id === "development")
|
||||||
|
?.submenu as Electron.MenuItemConstructorOptions[];
|
||||||
|
const keepAliveItem = developmentMenu?.find(
|
||||||
|
(item) => item.label === "Install Keep Alive",
|
||||||
|
);
|
||||||
|
if (keepAliveItem) {
|
||||||
|
keepAliveItem.enabled = !isInstalled; // Enable if not installed, disable if installed
|
||||||
|
const menu = Menu.buildFromTemplate(template);
|
||||||
|
Menu.setApplicationMenu(menu);
|
||||||
|
log.debug(
|
||||||
|
`Updated Install Keep Alive menu item: enabled=${keepAliveItem.enabled}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
`Error updating Keep Alive menu item: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const menu: Electron.Menu = Menu.buildFromTemplate(template);
|
const menu: Electron.Menu = Menu.buildFromTemplate(template);
|
||||||
Menu.setApplicationMenu(menu);
|
Menu.setApplicationMenu(menu);
|
||||||
|
// Update menu item enabled state on app start
|
||||||
|
updateKeepAliveMenuItem().catch((error) => {
|
||||||
|
log.error(
|
||||||
|
`Error updating Keep Alive menu item: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Register a global shortcut to show the hidden item
|
// Register a global shortcut to show the hidden item
|
||||||
globalShortcut.register("CommandOrControl+Shift+T", () => {
|
globalShortcut.register("CommandOrControl+Shift+T", () => {
|
||||||
@@ -266,7 +345,7 @@ function createWindow(): void {
|
|||||||
const fileMenu = template.find((item) => item.label === "Application");
|
const fileMenu = template.find((item) => item.label === "Application");
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const hiddenItem = fileMenu?.submenu?.find(
|
const hiddenItem = fileMenu?.submenu?.find(
|
||||||
(item) => item.id === "development",
|
(item: { id: string }) => item.id === "development",
|
||||||
);
|
);
|
||||||
//Adjust the development menu as well.
|
//Adjust the development menu as well.
|
||||||
|
|
||||||
@@ -289,7 +368,9 @@ function createWindow(): void {
|
|||||||
mainWindow.on("moved", storeWindowState);
|
mainWindow.on("moved", storeWindowState);
|
||||||
|
|
||||||
mainWindow.on("ready-to-show", () => {
|
mainWindow.on("ready-to-show", () => {
|
||||||
mainWindow.show();
|
if (!isKeepAliveLaunch) {
|
||||||
|
mainWindow.show(); // Show only if not a keep-alive launch
|
||||||
|
}
|
||||||
//Start the HTTP server.
|
//Start the HTTP server.
|
||||||
// Start the local HTTP server
|
// Start the local HTTP server
|
||||||
try {
|
try {
|
||||||
@@ -307,16 +388,22 @@ function createWindow(): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
shell.openExternal(details.url);
|
shell.openExternal(details.url).catch((error) => {
|
||||||
|
log.error("Failed to open external URL:", errorTypeCheck(error));
|
||||||
|
});
|
||||||
return { action: "deny" };
|
return { action: "deny" };
|
||||||
});
|
});
|
||||||
|
|
||||||
// HMR for renderer base on electron-vite cli.
|
// HMR for renderer base on electron-vite cli.
|
||||||
// Load the remote URL for development or the local html file for production.
|
// Load the remote URL for development or the local html file for production.
|
||||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]).catch(error => {
|
||||||
|
log.error("Failed to load URL:", errorTypeCheck(error));
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(join(__dirname, "../renderer/index.html"));
|
mainWindow.loadFile(join(__dirname, "../renderer/index.html")).catch(error => {
|
||||||
|
log.error("Failed to load file:", errorTypeCheck(error));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
@@ -341,7 +428,8 @@ app.whenReady().then(async () => {
|
|||||||
optimizer.watchWindowShortcuts(window);
|
optimizer.watchWindowShortcuts(window);
|
||||||
});
|
});
|
||||||
|
|
||||||
let isDefaultProtocolClient;
|
let isDefaultProtocolClient: boolean;
|
||||||
|
|
||||||
// remove so we can register each time as we run the app.
|
// remove so we can register each time as we run the app.
|
||||||
app.removeAsDefaultProtocolClient(protocol);
|
app.removeAsDefaultProtocolClient(protocol);
|
||||||
|
|
||||||
@@ -369,11 +457,15 @@ app.whenReady().then(async () => {
|
|||||||
openMainWindow();
|
openMainWindow();
|
||||||
const url = argv.find((arg) => arg.startsWith(`${protocol}://`));
|
const url = argv.find((arg) => arg.startsWith(`${protocol}://`));
|
||||||
if (url) {
|
if (url) {
|
||||||
openInExplorer(url);
|
if (url.startsWith(`${protocol}://keep-alive`)) {
|
||||||
|
log.info("Keep-alive protocol received, app is already running.");
|
||||||
|
// Do nothing if already running
|
||||||
|
} else {
|
||||||
|
openInExplorer(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
//Dynamically load ipcMain handlers once ready.
|
//Dynamically load ipcMain handlers once ready.
|
||||||
try {
|
try {
|
||||||
const module = await import("./ipc/ipcMainConfig");
|
const module = await import("./ipc/ipcMainConfig");
|
||||||
@@ -453,6 +545,12 @@ app.whenReady().then(async () => {
|
|||||||
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.downloaded, info);
|
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.downloaded, info);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if launched with keep-alive protocol (Windows)
|
||||||
|
const args = process.argv.slice(1);
|
||||||
|
if (args.some((arg) => arg.startsWith(`${protocol}://keep-alive`))) {
|
||||||
|
isKeepAliveLaunch = true;
|
||||||
|
}
|
||||||
|
|
||||||
//The update itself will run when the bodyshop record is queried to know what release channel to use.
|
//The update itself will run when the bodyshop record is queried to know what release channel to use.
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
||||||
@@ -464,7 +562,13 @@ app.whenReady().then(async () => {
|
|||||||
app.on("open-url", (event: Electron.Event, url: string) => {
|
app.on("open-url", (event: Electron.Event, url: string) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
//Don't do anything for now. We just want to open the app.
|
//Don't do anything for now. We just want to open the app.
|
||||||
openInExplorer(url);
|
if (url.startsWith(`${protocol}://keep-alive`)) {
|
||||||
|
log.info("Keep-alive protocol received.");
|
||||||
|
isKeepAliveLaunch = true;
|
||||||
|
openMainWindow();
|
||||||
|
} else {
|
||||||
|
openInExplorer(url);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
// Quit when all windows are closed, except on macOS. There, it's common
|
||||||
@@ -518,5 +622,7 @@ function openMainWindow(): void {
|
|||||||
function openInExplorer(url: string): void {
|
function openInExplorer(url: string): void {
|
||||||
const folderPath: string = decodeURIComponent(url.split(`${protocol}://`)[1]);
|
const folderPath: string = decodeURIComponent(url.split(`${protocol}://`)[1]);
|
||||||
log.info("Opening folder in explorer", folderPath);
|
log.info("Opening folder in explorer", folderPath);
|
||||||
shell.openPath(folderPath);
|
shell.openPath(folderPath).catch((error) => {
|
||||||
|
log.error("Failed to open folder in explorer:", errorTypeCheck(error));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/main/setup-keep-alive-agent.ts
Normal file
68
src/main/setup-keep-alive-agent.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { homedir } from "os";
|
||||||
|
import { exec } from "child_process";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import log from "electron-log/main";
|
||||||
|
|
||||||
|
const execPromise = promisify(exec);
|
||||||
|
|
||||||
|
export async function setupKeepAliveAgent(): Promise<void> {
|
||||||
|
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.convenientbrands.bodyshop-desktop.keepalive</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>open</string>
|
||||||
|
<string>imexmedia://keep-alive</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>StartInterval</key>
|
||||||
|
<integer>300</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>`;
|
||||||
|
|
||||||
|
const plistPath = join(
|
||||||
|
homedir(),
|
||||||
|
"Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(plistPath, plistContent);
|
||||||
|
const { stdout, stderr } = await execPromise(`launchctl load ${plistPath}`);
|
||||||
|
log.info(`Launch agent created and loaded: ${stdout}`);
|
||||||
|
if (stderr) log.warn(`Launch agent stderr: ${stderr}`);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error setting up launch agent: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
throw error; // Rethrow to allow caller to handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isKeepAliveAgentInstalled(): Promise<boolean> {
|
||||||
|
const plistPath = join(
|
||||||
|
homedir(),
|
||||||
|
"Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
|
||||||
|
);
|
||||||
|
const maxRetries = 3;
|
||||||
|
const retryDelay = 500; // 500ms delay between retries
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
await fs.access(plistPath, fs.constants.F_OK);
|
||||||
|
const { stdout } = await execPromise(`launchctl list | grep com.convenientbrands.bodyshop-desktop.keepalive`);
|
||||||
|
return !!stdout; // Return true if plist exists and agent is loaded
|
||||||
|
} catch (error) {
|
||||||
|
log.debug(`Launch agent not found (attempt ${attempt}/${maxRetries}): ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
if (attempt === maxRetries) {
|
||||||
|
return false; // Return false after all retries fail
|
||||||
|
}
|
||||||
|
// Wait before retrying
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false; // Fallback return
|
||||||
|
}
|
||||||
46
src/main/setup-keep-alive-task.ts
Normal file
46
src/main/setup-keep-alive-task.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { exec } from "child_process";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import log from "electron-log/main";
|
||||||
|
|
||||||
|
const execPromise = promisify(exec);
|
||||||
|
|
||||||
|
export async function setupKeepAliveTask(): Promise<void> {
|
||||||
|
const taskName = "ImEXShopPartnerKeepAlive";
|
||||||
|
const protocolUrl = "imexmedia://keep-alive";
|
||||||
|
// Use PowerShell with -ExecutionPolicy Bypass to open the URL
|
||||||
|
const command = `powershell.exe -ExecutionPolicy Bypass -Command "Start-Process '${protocolUrl}'"`;
|
||||||
|
// Escape quotes for schtasks /tr parameter
|
||||||
|
const escapedCommand = command.replace(/"/g, '\\"');
|
||||||
|
|
||||||
|
const schtasksCommand = `schtasks /create /tn "${taskName}" /tr "${escapedCommand}" /sc minute /mo 5 /f`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await execPromise(schtasksCommand);
|
||||||
|
log.info(`Scheduled task created: ${stdout}`);
|
||||||
|
if (stderr) log.warn(`Scheduled task stderr: ${stderr}`);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error creating scheduled task: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
throw error; // Rethrow to allow caller to handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isKeepAliveTaskInstalled(): Promise<boolean> {
|
||||||
|
const taskName = "ImEXShopPartnerKeepAlive";
|
||||||
|
const maxRetries = 3;
|
||||||
|
const retryDelay = 500; // 500ms delay between retries
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execPromise(`schtasks /query /tn "${taskName}"`);
|
||||||
|
return !!stdout; // Return true if task exists
|
||||||
|
} catch (error) {
|
||||||
|
log.debug(`Scheduled task ${taskName} not found (attempt ${attempt}/${maxRetries}): ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
if (attempt === maxRetries) {
|
||||||
|
return false; // Return false after all retries fail
|
||||||
|
}
|
||||||
|
// Wait before retrying
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false; // Fallback return
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user