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}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
requestExecutionLevel: admin # Ensure elevated privileges
|
||||
include: "scripts/installer.nsh" # Reference NSIS script from scripts directory
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
category: public.app-category.business
|
||||
@@ -34,6 +36,11 @@ mac:
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents 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: default
|
||||
arch:
|
||||
@@ -50,12 +57,16 @@ linux:
|
||||
- deb
|
||||
maintainer: electronjs.org
|
||||
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:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
npmRebuild: false
|
||||
publish:
|
||||
provider: s3
|
||||
bucket: imex-partner
|
||||
region: ca-central-1
|
||||
# electronDownload:
|
||||
# mirror: https://npmmirror.com/mirrors/electron/
|
||||
region: ca-central-1
|
||||
@@ -26,6 +26,8 @@ nsis:
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
requestExecutionLevel: admin # Ensure elevated privileges
|
||||
include: "scripts/installer.nsh" # Reference NSIS script from scripts directory
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
category: public.app-category.business
|
||||
@@ -34,6 +36,11 @@ mac:
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents 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: default
|
||||
arch:
|
||||
@@ -50,12 +57,16 @@ linux:
|
||||
- deb
|
||||
maintainer: electronjs.org
|
||||
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:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
npmRebuild: false
|
||||
publish:
|
||||
provider: s3
|
||||
bucket: rome-partner
|
||||
region: us-east-2
|
||||
# electronDownload:
|
||||
# mirror: https://npmmirror.com/mirrors/electron/
|
||||
region: us-east-2
|
||||
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 { getMainWindow } from "./util/toRenderer";
|
||||
import { GetAllEnvFiles } from "./watcher/watcher";
|
||||
import {
|
||||
isKeepAliveAgentInstalled,
|
||||
setupKeepAliveAgent,
|
||||
} from "./setup-keep-alive-agent";
|
||||
import {
|
||||
isKeepAliveTaskInstalled,
|
||||
setupKeepAliveTask,
|
||||
} from "./setup-keep-alive-task";
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://ba41d22656999a8c1fd63bcb7df98650@o492140.ingest.us.sentry.io/4509074139447296",
|
||||
@@ -34,6 +42,7 @@ log.initialize();
|
||||
const isMac: boolean = process.platform === "darwin";
|
||||
const protocol: string = "imexmedia";
|
||||
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
|
||||
const localServer = new LocalServer();
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
@@ -52,7 +61,7 @@ function createWindow(): void {
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
show: false,
|
||||
show: false, // Start hidden, show later if not keep-alive
|
||||
minWidth: 600,
|
||||
minHeight: 400,
|
||||
//autoHideMenuBar: true,
|
||||
@@ -192,7 +201,14 @@ function createWindow(): void {
|
||||
label: "Open Log File",
|
||||
click: (): void => {
|
||||
/* 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",
|
||||
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",
|
||||
},
|
||||
|
||||
// {
|
||||
// label: "Decode Hardcoded Estimate",
|
||||
// click: (): void => {
|
||||
// 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",
|
||||
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);
|
||||
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
|
||||
globalShortcut.register("CommandOrControl+Shift+T", () => {
|
||||
@@ -266,7 +345,7 @@ function createWindow(): void {
|
||||
const fileMenu = template.find((item) => item.label === "Application");
|
||||
// @ts-ignore
|
||||
const hiddenItem = fileMenu?.submenu?.find(
|
||||
(item) => item.id === "development",
|
||||
(item: { id: string }) => item.id === "development",
|
||||
);
|
||||
//Adjust the development menu as well.
|
||||
|
||||
@@ -289,7 +368,9 @@ function createWindow(): void {
|
||||
mainWindow.on("moved", storeWindowState);
|
||||
|
||||
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 local HTTP server
|
||||
try {
|
||||
@@ -307,16 +388,22 @@ function createWindow(): void {
|
||||
});
|
||||
|
||||
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" };
|
||||
});
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
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 {
|
||||
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) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
@@ -341,7 +428,8 @@ app.whenReady().then(async () => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
|
||||
let isDefaultProtocolClient;
|
||||
let isDefaultProtocolClient: boolean;
|
||||
|
||||
// remove so we can register each time as we run the app.
|
||||
app.removeAsDefaultProtocolClient(protocol);
|
||||
|
||||
@@ -369,11 +457,15 @@ app.whenReady().then(async () => {
|
||||
openMainWindow();
|
||||
const url = argv.find((arg) => arg.startsWith(`${protocol}://`));
|
||||
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.
|
||||
try {
|
||||
const module = await import("./ipc/ipcMainConfig");
|
||||
@@ -453,6 +545,12 @@ app.whenReady().then(async () => {
|
||||
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.
|
||||
createWindow();
|
||||
|
||||
@@ -464,7 +562,13 @@ app.whenReady().then(async () => {
|
||||
app.on("open-url", (event: Electron.Event, url: string) => {
|
||||
event.preventDefault();
|
||||
//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
|
||||
@@ -518,5 +622,7 @@ function openMainWindow(): void {
|
||||
function openInExplorer(url: string): void {
|
||||
const folderPath: string = decodeURIComponent(url.split(`${protocol}://`)[1]);
|
||||
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