Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: app should only have one instance #99

Merged
merged 8 commits into from Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 29 additions & 13 deletions src/electron/background.ts
@@ -1,5 +1,6 @@
import { app } from "electron";

import { isTest } from "#/flags";
import { main } from "@/main";
import logger from "@/services/logger";
import { watchStores } from "@/stores/watchers";
Expand Down Expand Up @@ -56,21 +57,36 @@ import "@/ipc/vector-store";
// Import keys
import "@/ipc/keys";

// Obtain a lock to check if the app is locked (primary instance)
// If false, we close the app, as we want to prevent multiple instances of our app
// https://github.com/electron/electron/blob/v30.0.0-nightly.20240221/docs/api/app.md#apprequestsingleinstancelockadditionaldata
const gotTheLock = app.requestSingleInstanceLock();

let unsubscribe: (() => Promise<void>) | undefined;
// Initialize the application by calling the main function.
// Upon completion, log to the console indicating the application has started.
main().then(() => {
logger.info("Application started successfully");
unsubscribe = watchStores();
});

// Listen for the 'window-all-closed' event on the Electron app object.
// This event is emitted when all windows of the application have been closed.
// In response, quit the application to free up resources, adhering to typical desktop application
// behavior.
app.on("window-all-closed", () => {

if (gotTheLock || isTest) {
// Initialize the application by calling the main function.
// Upon completion, log to the console indicating the application has started.
TimPietrusky marked this conversation as resolved.
Show resolved Hide resolved
main().then(() => {
logger.info("Application started successfully");
unsubscribe = watchStores();
});

// Listen for the 'window-all-closed' event on the Electron app object.
// This event is emitted when all windows of the application have been closed.
// In response, quit the application to free up resources, adhering to typical desktop application
// behavior.
app.on("window-all-closed", () => {
app.quit();
if (unsubscribe) {
unsubscribe();
}
});
} else {
// The app is locked, so we force quit the new instance
console.log("App is locked by another instance. Closing app");
app.quit();
if (unsubscribe) {
unsubscribe();
}
});
}
23 changes: 23 additions & 0 deletions src/electron/future/main.ts
Expand Up @@ -337,6 +337,18 @@ export async function main() {
logger.info(`main(): listened to :open`);

if (isUpToDate && isReady) {
app.on("second-instance", async () => {
apps.core ||= await createCoreWindow();

// Someone tried to run a second instance, we should focus our window.
if (apps.core) {
if (apps.core.isMinimized()) {
apps.core.restore();
}

apps.core.focus();
}
});
// Start the vector store and fill it with data
await initialize();
await reset();
Expand All @@ -353,6 +365,17 @@ export async function main() {
// Create and show installer window
const installerWindow = await createInstallerWindow();

app.on("second-instance", async () => {
apps.core ||= await createCoreWindow();

// Someone tried to run a second instance, we should focus our window.
if (installerWindow.isMinimized()) {
installerWindow.restore();
}

installerWindow.focus();
});

// When the installer is done we open the prompt window
ipcMain.on(buildKey([ID.APP], { suffix: ":ready" }), async () => {
await runStartup();
Expand Down
3 changes: 3 additions & 0 deletions src/electron/future/utils/__tests__/path-helpers.test.ts
Expand Up @@ -24,6 +24,7 @@ jest.mock("electron", () => ({

jest.mock("#/flags", () => ({
isDevelopment: jest.requireActual("#/flags").isDevelopment,
isTest: jest.requireActual("#/flags").isTest,
}));

describe("Path Utilities", () => {
Expand All @@ -41,6 +42,7 @@ describe("Path Utilities", () => {
it("correctly sets resourcesDirectory in development mode", async () => {
jest.mock("#/flags", () => ({
isDevelopment: true,
isTest: true,
}));
const expectedDevelopmentPath = path.join(process.cwd(), "resources");
const { resourcesDirectory } = await import("../path-helpers");
Expand All @@ -50,6 +52,7 @@ describe("Path Utilities", () => {
it("correctly sets resourcesDirectory in production mode", async () => {
jest.mock("#/flags", () => ({
isDevelopment: false,
isTest: false,
}));

const { resourcesDirectory } = await import("../path-helpers");
Expand Down
9 changes: 5 additions & 4 deletions src/electron/future/utils/path-helpers.ts
Expand Up @@ -6,11 +6,12 @@ import path from "path";

import { app } from "electron";

import { isDevelopment } from "#/flags";
import { isDevelopment, isTest } from "#/flags";

export const resourcesDirectory = isDevelopment
? path.join(process.cwd(), "resources")
: path.join(app.getPath("exe"), "..", "resources", "app.asar.unpacked", "resources");
export const resourcesDirectory =
isTest || isDevelopment
? path.join(process.cwd(), "resources")
: path.join(app.getPath("exe"), "..", "resources", "app.asar.unpacked", "resources");

/**
* Combines the resources directory path with additional subpaths.
Expand Down
5 changes: 3 additions & 2 deletions src/shared/flags.ts
@@ -1,6 +1,7 @@
export const isProduction = process.env.NODE_ENV === "production";
export const isDevelopment =
export const isTest =
process.env.TEST_ENV === "local" ||
process.env.TEST_ENV === "test" ||
process.env.NODE_ENV === "development" ||
process.env.NODE_ENV === "test";

export const isDevelopment = process.env.NODE_ENV === "development";