fix(linux): single-line description and correct desktop.entry for valid .desktop file (#325)

This commit is contained in:
paisley
2026-03-06 16:22:25 +08:00
committed by GitHub
Unverified
parent e7d4cf73d5
commit 3ce4b5d17a
5 changed files with 62 additions and 9 deletions

View File

@@ -157,10 +157,7 @@ linux:
maintainer: ClawX Team <public@valuecell.ai> maintainer: ClawX Team <public@valuecell.ai>
vendor: ClawX vendor: ClawX
synopsis: AI Assistant powered by OpenClaw synopsis: AI Assistant powered by OpenClaw
description: | description: ClawX is a graphical AI assistant application that integrates with OpenClaw Gateway to provide intelligent automation and assistance across multiple messaging platforms.
ClawX is a graphical AI assistant application that integrates with
OpenClaw Gateway to provide intelligent automation and assistance
across multiple messaging platforms.
desktop: desktop:
entry: entry:
Name: ClawX Name: ClawX

View File

@@ -436,6 +436,14 @@ export class GatewayManager extends EventEmitter {
logger.debug('No existing Gateway found, starting new process...'); logger.debug('No existing Gateway found, starting new process...');
// On Windows, TCP TIME_WAIT can hold the port for up to 2 minutes
// after the previous Gateway process exits, preventing the new one
// from binding. Wait for the port to be free before proceeding.
if (process.platform === 'win32') {
await this.waitForPortFree(this.status.port);
this.assertLifecycleEpoch(startEpoch, 'start/wait-port');
}
// Start new Gateway process // Start new Gateway process
await this.startProcess(); await this.startProcess();
this.assertLifecycleEpoch(startEpoch, 'start/start-process'); this.assertLifecycleEpoch(startEpoch, 'start/start-process');
@@ -1015,6 +1023,45 @@ export class GatewayManager extends EventEmitter {
* Start Gateway process * Start Gateway process
* Uses OpenClaw npm package from node_modules (dev) or resources (production) * Uses OpenClaw npm package from node_modules (dev) or resources (production)
*/ */
/**
* Wait until the gateway port is no longer held by the OS.
* On Windows, TCP TIME_WAIT can keep a port occupied for up to 2 minutes
* after the owning process exits, causing the new Gateway to hang on bind.
*/
private async waitForPortFree(port: number, timeoutMs = 30000): Promise<void> {
const net = await import('net');
const start = Date.now();
const pollInterval = 500;
let logged = false;
while (Date.now() - start < timeoutMs) {
const available = await new Promise<boolean>((resolve) => {
const server = net.createServer();
server.once('error', () => resolve(false));
server.once('listening', () => {
server.close(() => resolve(true));
});
server.listen(port, '127.0.0.1');
});
if (available) {
const elapsed = Date.now() - start;
if (elapsed > pollInterval) {
logger.info(`Port ${port} became available after ${elapsed}ms`);
}
return;
}
if (!logged) {
logger.info(`Waiting for port ${port} to become available (Windows TCP TIME_WAIT)...`);
logged = true;
}
await new Promise(r => setTimeout(r, pollInterval));
}
logger.warn(`Port ${port} still occupied after ${timeoutMs}ms, proceeding anyway`);
}
private async startProcess(): Promise<void> { private async startProcess(): Promise<void> {
// Ensure no system-managed gateway service will compete with our process. // Ensure no system-managed gateway service will compete with our process.
await this.unloadLaunchctlService(); await this.unloadLaunchctlService();

View File

@@ -46,11 +46,16 @@ export type DeferredRestartAction = 'none' | 'wait' | 'drop' | 'execute';
/** /**
* Decide what to do with a pending deferred restart once lifecycle changes. * Decide what to do with a pending deferred restart once lifecycle changes.
*
* A deferred restart is an explicit restart() call that was postponed because
* the manager was mid-startup/reconnect. When the in-flight operation settles
* we must honour the request — even if the gateway is now running — because
* the caller may have changed config (e.g. provider switch) that the current
* process hasn't picked up.
*/ */
export function getDeferredRestartAction(context: DeferredRestartActionContext): DeferredRestartAction { export function getDeferredRestartAction(context: DeferredRestartActionContext): DeferredRestartAction {
if (!context.hasPendingRestart) return 'none'; if (!context.hasPendingRestart) return 'none';
if (shouldDeferRestart(context)) return 'wait'; if (shouldDeferRestart(context)) return 'wait';
if (!context.shouldReconnect) return 'drop'; if (!context.shouldReconnect) return 'drop';
if (context.state === 'running') return 'drop';
return 'execute'; return 'execute';
} }

View File

@@ -995,10 +995,14 @@ function registerDeviceOAuthHandlers(mainWindow: BrowserWindow): void {
* Provider-related IPC handlers * Provider-related IPC handlers
*/ */
function registerProviderHandlers(gatewayManager: GatewayManager): void { function registerProviderHandlers(gatewayManager: GatewayManager): void {
// Listen for OAuth success to automatically restart the Gateway with new tokens/configs // Listen for OAuth success to automatically restart the Gateway with new tokens/configs.
// Use a longer debounce (8s) so that provider:setDefault — which writes the full config
// and then calls debouncedRestart(2s) — has time to fire and coalesce into a single
// restart. Without this, the OAuth restart fires first with stale config, and the
// subsequent provider:setDefault restart is deferred and dropped.
deviceOAuthManager.on('oauth:success', (providerType) => { deviceOAuthManager.on('oauth:success', (providerType) => {
logger.info(`[IPC] Scheduling Gateway restart after ${providerType} OAuth success...`); logger.info(`[IPC] Scheduling Gateway restart after ${providerType} OAuth success...`);
gatewayManager.debouncedRestart(); gatewayManager.debouncedRestart(8000);
}); });
// Get all providers with key info // Get all providers with key info

View File

@@ -65,7 +65,7 @@ describe('gateway process policy helpers', () => {
expect(shouldDeferRestart({ state: 'error', startLock: false })).toBe(false); expect(shouldDeferRestart({ state: 'error', startLock: false })).toBe(false);
}); });
it('drops deferred restart once lifecycle recovers to running', () => { it('executes deferred restart even after lifecycle recovers to running', () => {
expect( expect(
getDeferredRestartAction({ getDeferredRestartAction({
hasPendingRestart: true, hasPendingRestart: true,
@@ -73,7 +73,7 @@ describe('gateway process policy helpers', () => {
startLock: false, startLock: false,
shouldReconnect: true, shouldReconnect: true,
}) })
).toBe('drop'); ).toBe('execute');
}); });
it('waits deferred restart while lifecycle is still busy', () => { it('waits deferred restart while lifecycle is still busy', () => {