| |
| |
| |
| |
| |
|
|
| import { debugLogger, type Config } from '@google/gemini-cli-core'; |
| import WebSocket from 'ws'; |
| import { |
| initActivityLogger, |
| addNetworkTransport, |
| ActivityLogger, |
| } from './activityLogger.js'; |
|
|
| interface IDevTools { |
| start(): Promise<string>; |
| stop(): Promise<void>; |
| getPort(): number; |
| } |
|
|
| const DEFAULT_DEVTOOLS_PORT = 25417; |
| const DEFAULT_DEVTOOLS_HOST = '127.0.0.1'; |
| const MAX_PROMOTION_ATTEMPTS = 3; |
| let promotionAttempts = 0; |
| let serverStartPromise: Promise<string> | null = null; |
| let connectedUrl: string | null = null; |
|
|
| |
| |
| |
| |
| function probeDevTools(host: string, port: number): Promise<boolean> { |
| return new Promise((resolve) => { |
| const ws = new WebSocket(`ws://${host}:${port}/ws`); |
| const timer = setTimeout(() => { |
| ws.close(); |
| resolve(false); |
| }, 500); |
|
|
| ws.on('open', () => { |
| clearTimeout(timer); |
| ws.close(); |
| resolve(true); |
| }); |
|
|
| ws.on('error', () => { |
| clearTimeout(timer); |
| ws.close(); |
| resolve(false); |
| }); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| async function startOrJoinDevTools( |
| defaultHost: string, |
| defaultPort: number, |
| ): Promise<{ host: string; port: number }> { |
| const mod = await import('@google/gemini-cli-devtools'); |
| const devtools: IDevTools = mod.DevTools.getInstance(); |
| const url = await devtools.start(); |
| const actualPort = devtools.getPort(); |
|
|
| if (actualPort === defaultPort) { |
| |
| debugLogger.log(`DevTools available at: ${url}`); |
| return { host: defaultHost, port: actualPort }; |
| } |
|
|
| |
| |
| const winnerAlive = await probeDevTools(defaultHost, defaultPort); |
| if (winnerAlive) { |
| await devtools.stop(); |
| debugLogger.log( |
| `DevTools (existing) at: http://${defaultHost}:${defaultPort}`, |
| ); |
| return { host: defaultHost, port: defaultPort }; |
| } |
|
|
| |
| debugLogger.log(`DevTools available at: ${url}`); |
| return { host: defaultHost, port: actualPort }; |
| } |
|
|
| |
| |
| |
| |
| async function handlePromotion(config: Config) { |
| promotionAttempts++; |
| if (promotionAttempts > MAX_PROMOTION_ATTEMPTS) { |
| debugLogger.debug( |
| `Giving up on DevTools promotion after ${MAX_PROMOTION_ATTEMPTS} attempts`, |
| ); |
| return; |
| } |
|
|
| try { |
| const result = await startOrJoinDevTools( |
| DEFAULT_DEVTOOLS_HOST, |
| DEFAULT_DEVTOOLS_PORT, |
| ); |
| addNetworkTransport(config, result.host, result.port, () => |
| handlePromotion(config), |
| ); |
| } catch (err) { |
| debugLogger.debug('Failed to promote to DevTools server:', err); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function setupInitialActivityLogger(config: Config) { |
| const target = process.env['GEMINI_CLI_ACTIVITY_LOG_TARGET']; |
|
|
| if (target) { |
| if (!config.storage) return; |
| initActivityLogger(config, { mode: 'file', filePath: target }); |
| } else { |
| |
| initActivityLogger(config, { mode: 'buffer' }); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function startDevToolsServer(config: Config): Promise<string> { |
| if (connectedUrl) return Promise.resolve(connectedUrl); |
| if (serverStartPromise) return serverStartPromise; |
| serverStartPromise = startDevToolsServerImpl(config).catch((err) => { |
| serverStartPromise = null; |
| throw err; |
| }); |
| return serverStartPromise; |
| } |
|
|
| async function startDevToolsServerImpl(config: Config): Promise<string> { |
| const onReconnectFailed = () => handlePromotion(config); |
|
|
| |
| const existing = await probeDevTools( |
| DEFAULT_DEVTOOLS_HOST, |
| DEFAULT_DEVTOOLS_PORT, |
| ); |
|
|
| let host = DEFAULT_DEVTOOLS_HOST; |
| let port = DEFAULT_DEVTOOLS_PORT; |
|
|
| if (existing) { |
| debugLogger.log( |
| `DevTools (existing) at: http://${DEFAULT_DEVTOOLS_HOST}:${DEFAULT_DEVTOOLS_PORT}`, |
| ); |
| } else { |
| |
| try { |
| const result = await startOrJoinDevTools( |
| DEFAULT_DEVTOOLS_HOST, |
| DEFAULT_DEVTOOLS_PORT, |
| ); |
| host = result.host; |
| port = result.port; |
| } catch (err) { |
| debugLogger.debug('Failed to start DevTools:', err); |
| throw err; |
| } |
| } |
|
|
| |
| addNetworkTransport(config, host, port, onReconnectFailed); |
| const capture = ActivityLogger.getInstance(); |
| capture.enableNetworkLogging(); |
|
|
| const url = `http://localhost:${port}`; |
| connectedUrl = url; |
| return url; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function toggleDevToolsPanel( |
| config: Config, |
| isOpen: boolean, |
| toggle: () => void, |
| setOpen: () => void, |
| ): Promise<void> { |
| if (isOpen) { |
| toggle(); |
| return; |
| } |
|
|
| try { |
| const { openBrowserSecurely, shouldLaunchBrowser } = await import( |
| '@google/gemini-cli-core' |
| ); |
| const url = await startDevToolsServer(config); |
| if (shouldLaunchBrowser()) { |
| try { |
| await openBrowserSecurely(url); |
| |
| return; |
| } catch (e) { |
| debugLogger.warn('Failed to open browser securely:', e); |
| } |
| } |
| |
| setOpen(); |
| } catch (e) { |
| setOpen(); |
| debugLogger.error('Failed to start DevTools server:', e); |
| } |
| } |
|
|
| |
| export function resetForTesting() { |
| promotionAttempts = 0; |
| serverStartPromise = null; |
| connectedUrl = null; |
| } |
|
|