fix(linux): single-line description and correct desktop.entry for valid .desktop file (#325)
This commit is contained in:
committed by
GitHub
Unverified
parent
e7d4cf73d5
commit
3ce4b5d17a
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user