fix: keep one goosed client per BrowswerWindow (#4805)
This commit is contained in:
@@ -37,7 +37,7 @@ vi.mock('./utils/costDatabase', () => ({
|
|||||||
initializeCostDatabase: vi.fn().mockResolvedValue(undefined),
|
initializeCostDatabase: vi.fn().mockResolvedValue(undefined),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./api/sdk.gen', () => {
|
vi.mock('./api', () => {
|
||||||
const test_chat = {
|
const test_chat = {
|
||||||
data: {
|
data: {
|
||||||
session_id: 'test',
|
session_id: 'test',
|
||||||
@@ -303,7 +303,7 @@ describe('App Component - Brand New State', () => {
|
|||||||
|
|
||||||
it('should handle config recovery gracefully', async () => {
|
it('should handle config recovery gracefully', async () => {
|
||||||
// Mock config error that triggers recovery
|
// Mock config error that triggers recovery
|
||||||
const { readAllConfig, recoverConfig } = await import('./api/sdk.gen');
|
const { readAllConfig, recoverConfig } = await import('./api');
|
||||||
console.log(recoverConfig);
|
console.log(recoverConfig);
|
||||||
vi.mocked(readAllConfig).mockRejectedValueOnce(new Error('Config read error'));
|
vi.mocked(readAllConfig).mockRejectedValueOnce(new Error('Config read error'));
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import ExtensionConfigFields from './ExtensionConfigFields';
|
|||||||
import { PlusIcon, Edit, Trash2, AlertTriangle } from 'lucide-react';
|
import { PlusIcon, Edit, Trash2, AlertTriangle } from 'lucide-react';
|
||||||
import ExtensionInfoFields from './ExtensionInfoFields';
|
import ExtensionInfoFields from './ExtensionInfoFields';
|
||||||
import ExtensionTimeoutField from './ExtensionTimeoutField';
|
import ExtensionTimeoutField from './ExtensionTimeoutField';
|
||||||
import { upsertConfig } from '../../../../api/sdk.gen';
|
import { upsertConfig } from '../../../../api';
|
||||||
import { ConfirmationModal } from '../../../ui/ConfirmationModal';
|
import { ConfirmationModal } from '../../../ui/ConfirmationModal';
|
||||||
|
|
||||||
interface ExtensionModalProps {
|
interface ExtensionModalProps {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { App } from 'electron';
|
|||||||
import { Buffer } from 'node:buffer';
|
import { Buffer } from 'node:buffer';
|
||||||
|
|
||||||
import { status } from './api';
|
import { status } from './api';
|
||||||
import { client } from './api/client.gen';
|
import { Client } from './api/client';
|
||||||
|
|
||||||
// Find an available port to start goosed on
|
// Find an available port to start goosed on
|
||||||
export const findAvailablePort = (): Promise<number> => {
|
export const findAvailablePort = (): Promise<number> => {
|
||||||
@@ -26,14 +26,13 @@ export const findAvailablePort = (): Promise<number> => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Goose process manager. Take in the app, port, and directory to start goosed in.
|
|
||||||
// Check if goosed server is ready by polling the status endpoint
|
// Check if goosed server is ready by polling the status endpoint
|
||||||
export const checkServerStatus = async (): Promise<boolean> => {
|
export const checkServerStatus = async (client: Client): Promise<boolean> => {
|
||||||
const interval = 100;
|
const interval = 100;
|
||||||
const maxAttempts = 200;
|
const maxAttempts = 200;
|
||||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
try {
|
try {
|
||||||
await status({ throwOnError: true });
|
await status({ client, throwOnError: true });
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
if (attempt === maxAttempts) {
|
if (attempt === maxAttempts) {
|
||||||
@@ -47,25 +46,10 @@ export const checkServerStatus = async (): Promise<boolean> => {
|
|||||||
|
|
||||||
const connectToExternalBackend = async (
|
const connectToExternalBackend = async (
|
||||||
workingDir: string,
|
workingDir: string,
|
||||||
port: number = 3000,
|
port: number = 3000
|
||||||
serverSecret: string
|
|
||||||
): Promise<[number, string, ChildProcess]> => {
|
): Promise<[number, string, ChildProcess]> => {
|
||||||
log.info(`Using external goosed backend on port ${port}`);
|
log.info(`Using external goosed backend on port ${port}`);
|
||||||
|
|
||||||
// Configure the client BEFORE checking server status
|
|
||||||
client.setConfig({
|
|
||||||
baseUrl: `http://127.0.0.1:${port}`,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Secret-Key': serverSecret,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const isReady = await checkServerStatus();
|
|
||||||
if (!isReady) {
|
|
||||||
throw new Error(`External goosed server not accessible on port ${port}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockProcess = {
|
const mockProcess = {
|
||||||
pid: undefined,
|
pid: undefined,
|
||||||
kill: () => {
|
kill: () => {
|
||||||
@@ -104,7 +88,7 @@ export const startGoosed = async (
|
|||||||
dir = path.resolve(path.normalize(dir));
|
dir = path.resolve(path.normalize(dir));
|
||||||
|
|
||||||
if (process.env.GOOSE_EXTERNAL_BACKEND) {
|
if (process.env.GOOSE_EXTERNAL_BACKEND) {
|
||||||
return connectToExternalBackend(dir, 3000, serverSecret);
|
return connectToExternalBackend(dir, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that the directory actually exists and is a directory
|
// Validate that the directory actually exists and is a directory
|
||||||
@@ -262,14 +246,6 @@ export const startGoosed = async (
|
|||||||
throw err; // Propagate the error
|
throw err; // Propagate the error
|
||||||
});
|
});
|
||||||
|
|
||||||
client.setConfig({
|
|
||||||
baseUrl: `http://127.0.0.1:${port}`,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Secret-Key': serverSecret,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const try_kill_goose = () => {
|
const try_kill_goose = () => {
|
||||||
try {
|
try {
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
|
|||||||
+33
-13
@@ -50,13 +50,14 @@ import {
|
|||||||
import { UPDATES_ENABLED } from './updates';
|
import { UPDATES_ENABLED } from './updates';
|
||||||
import { Recipe } from './recipe';
|
import { Recipe } from './recipe';
|
||||||
import './utils/recipeHash';
|
import './utils/recipeHash';
|
||||||
import { decodeRecipe } from './api/sdk.gen';
|
import { decodeRecipe } from './api';
|
||||||
import { client } from './api/client.gen';
|
import { Client, createClient, createConfig } from './api/client';
|
||||||
|
|
||||||
async function decodeRecipeMain(deeplink: string): Promise<Recipe | null> {
|
async function decodeRecipeMain(client: Client, deeplink: string): Promise<Recipe | null> {
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
await decodeRecipe({
|
await decodeRecipe({
|
||||||
|
client,
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
body: { deeplink },
|
body: { deeplink },
|
||||||
})
|
})
|
||||||
@@ -487,10 +488,10 @@ let appConfig = {
|
|||||||
GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true',
|
GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track windows by ID
|
|
||||||
let windowCounter = 0;
|
|
||||||
const windowMap = new Map<number, BrowserWindow>();
|
const windowMap = new Map<number, BrowserWindow>();
|
||||||
|
|
||||||
|
const goosedClients = new Map<number, Client>();
|
||||||
|
|
||||||
// Track power save blockers per window
|
// Track power save blockers per window
|
||||||
const windowPowerSaveBlockers = new Map<number, number>(); // windowId -> blockerId
|
const windowPowerSaveBlockers = new Map<number, number>(); // windowId -> blockerId
|
||||||
|
|
||||||
@@ -507,7 +508,7 @@ const createChat = async (
|
|||||||
) => {
|
) => {
|
||||||
// Initialize variables for process and configuration
|
// Initialize variables for process and configuration
|
||||||
let port = 0;
|
let port = 0;
|
||||||
let working_dir = '';
|
let workingDir = '';
|
||||||
let goosedProcess: import('child_process').ChildProcess | null = null;
|
let goosedProcess: import('child_process').ChildProcess | null = null;
|
||||||
|
|
||||||
if (viewType === 'recipeEditor') {
|
if (viewType === 'recipeEditor') {
|
||||||
@@ -521,7 +522,7 @@ const createChat = async (
|
|||||||
);
|
);
|
||||||
if (config) {
|
if (config) {
|
||||||
port = config.GOOSE_PORT;
|
port = config.GOOSE_PORT;
|
||||||
working_dir = config.GOOSE_WORKING_DIR;
|
workingDir = config.GOOSE_WORKING_DIR;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to get config from localStorage:', e);
|
console.error('Failed to get config from localStorage:', e);
|
||||||
@@ -551,7 +552,7 @@ const createChat = async (
|
|||||||
envVars
|
envVars
|
||||||
);
|
);
|
||||||
port = newPort;
|
port = newPort;
|
||||||
working_dir = newWorkingDir;
|
workingDir = newWorkingDir;
|
||||||
goosedProcess = newGoosedProcess;
|
goosedProcess = newGoosedProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,7 +593,7 @@ const createChat = async (
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
...appConfig,
|
...appConfig,
|
||||||
GOOSE_PORT: port,
|
GOOSE_PORT: port,
|
||||||
GOOSE_WORKING_DIR: working_dir,
|
GOOSE_WORKING_DIR: workingDir,
|
||||||
REQUEST_DIR: dir,
|
REQUEST_DIR: dir,
|
||||||
GOOSE_BASE_URL_SHARE: baseUrlShare,
|
GOOSE_BASE_URL_SHARE: baseUrlShare,
|
||||||
GOOSE_VERSION: version,
|
GOOSE_VERSION: version,
|
||||||
@@ -603,6 +604,17 @@ const createChat = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const goosedClient = createClient(
|
||||||
|
createConfig({
|
||||||
|
baseUrl: `http://127.0.0.1:${port}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Secret-Key': SERVER_SECRET,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
goosedClients.set(mainWindow.id, goosedClient);
|
||||||
|
|
||||||
// Let windowStateKeeper manage the window
|
// Let windowStateKeeper manage the window
|
||||||
mainWindowState.manage(mainWindow);
|
mainWindowState.manage(mainWindow);
|
||||||
|
|
||||||
@@ -660,7 +672,7 @@ const createChat = async (
|
|||||||
shell.openExternal(url);
|
shell.openExternal(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
const windowId = ++windowCounter;
|
const windowId = mainWindow.id;
|
||||||
const url = MAIN_WINDOW_VITE_DEV_SERVER_URL
|
const url = MAIN_WINDOW_VITE_DEV_SERVER_URL
|
||||||
? new URL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
|
? new URL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
|
||||||
: pathToFileURL(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
|
: pathToFileURL(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
|
||||||
@@ -738,7 +750,7 @@ const createChat = async (
|
|||||||
console.log('[Main] Starting background recipe decoding for:', recipeDeeplink);
|
console.log('[Main] Starting background recipe decoding for:', recipeDeeplink);
|
||||||
|
|
||||||
// Decode recipe asynchronously after window is created
|
// Decode recipe asynchronously after window is created
|
||||||
decodeRecipeMain(recipeDeeplink)
|
decodeRecipeMain(goosedClient, recipeDeeplink)
|
||||||
.then((decodedRecipe) => {
|
.then((decodedRecipe) => {
|
||||||
if (decodedRecipe) {
|
if (decodedRecipe) {
|
||||||
console.log('[Main] Recipe decoded successfully, updating window config');
|
console.log('[Main] Recipe decoded successfully, updating window config');
|
||||||
@@ -1061,8 +1073,16 @@ ipcMain.handle('get-secret-key', () => {
|
|||||||
return SERVER_SECRET;
|
return SERVER_SECRET;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-goosed-host-port', async () => {
|
ipcMain.handle('get-goosed-host-port', async (event) => {
|
||||||
await checkServerStatus();
|
const windowId = BrowserWindow.fromWebContents(event.sender)?.id;
|
||||||
|
if (!windowId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const client = goosedClients.get(windowId);
|
||||||
|
if (!client) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await checkServerStatus(client);
|
||||||
return client.getConfig().baseUrl || null;
|
return client.getConfig().baseUrl || null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user