mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
fix(startup): recover from core port 7788 conflict automatically (#2626)
This commit is contained in:
@@ -14,7 +14,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { type BootCheckResult, runBootCheck } from '../../lib/bootCheck';
|
||||
import { useT } from '../../lib/i18n/I18nContext';
|
||||
import { bootCheckTransport } from '../../services/bootCheckService';
|
||||
import { bootCheckTransport, recoverPortConflict } from '../../services/bootCheckService';
|
||||
import {
|
||||
clearCoreRpcTokenCache,
|
||||
clearCoreRpcUrlCache,
|
||||
@@ -407,16 +407,31 @@ function ResultScreen({
|
||||
if (result.kind === 'match') return null;
|
||||
|
||||
if (result.kind === 'unreachable') {
|
||||
const isPortConflict = result.portConflict === true;
|
||||
return (
|
||||
<Panel>
|
||||
<h2 className="text-xl font-semibold text-stone-900 dark:text-neutral-100">
|
||||
{t('bootCheck.cannotReach')}
|
||||
{isPortConflict ? t('bootCheck.portConflictTitle') : t('bootCheck.cannotReach')}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-stone-600 dark:text-neutral-300">
|
||||
{result.reason || t('bootCheck.cannotReachDesc')}
|
||||
{isPortConflict
|
||||
? t('bootCheck.portConflictBody')
|
||||
: result.reason || t('bootCheck.cannotReachDesc')}
|
||||
</p>
|
||||
{actionError && <p className="mt-3 text-xs text-red-600 font-medium">{actionError}</p>}
|
||||
<div className="mt-5 flex gap-3">
|
||||
<div className="mt-5 flex gap-3 flex-wrap">
|
||||
{isPortConflict && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAction}
|
||||
disabled={actionBusy}
|
||||
data-testid="fix-automatically-btn"
|
||||
className="rounded-lg bg-primary-500 px-4 py-2 text-sm font-medium text-white hover:bg-primary-600 disabled:opacity-60">
|
||||
{actionBusy
|
||||
? t('bootCheck.portConflictFixing')
|
||||
: t('bootCheck.portConflictFixButton')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRetry}
|
||||
@@ -427,13 +442,15 @@ function ResultScreen({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSwitchMode}
|
||||
className="rounded-lg border border-stone-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-4 py-2 text-sm text-stone-700 dark:text-neutral-200 hover:bg-stone-50 dark:hover:bg-neutral-800/60">
|
||||
disabled={actionBusy}
|
||||
className="rounded-lg border border-stone-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-4 py-2 text-sm text-stone-700 dark:text-neutral-200 hover:bg-stone-50 dark:hover:bg-neutral-800/60 disabled:opacity-60">
|
||||
{t('bootCheck.switchMode')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onQuit}
|
||||
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700">
|
||||
disabled={actionBusy}
|
||||
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-60">
|
||||
{t('bootCheck.quit')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -728,6 +745,18 @@ export default function BootCheckGate({ children }: BootCheckGateProps) {
|
||||
log('[boot-check] gate — triggering cloud core update');
|
||||
await transport.callRpc('openhuman.update_run', {});
|
||||
log('[boot-check] gate — cloud core update triggered');
|
||||
} else if (result.kind === 'unreachable' && result.portConflict) {
|
||||
log('[boot-check-gate] port conflict — invoking recover_port_conflict');
|
||||
const recovery = await recoverPortConflict();
|
||||
log(
|
||||
'[boot-check-gate] recovery result: success=%s message=%s',
|
||||
recovery.success,
|
||||
recovery.message
|
||||
);
|
||||
if (!recovery.success) {
|
||||
setActionError(t('bootCheck.portConflictFixFailed'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-run the full check after the action.
|
||||
|
||||
@@ -32,6 +32,15 @@ vi.mock('../../../lib/bootCheck', () => ({
|
||||
runBootCheck: (...args: unknown[]) => mockRunBootCheck(...args),
|
||||
}));
|
||||
|
||||
const mockRecoverPortConflict = vi.fn();
|
||||
vi.mock('../../../services/bootCheckService', async importOriginal => {
|
||||
const actual = await importOriginal<typeof import('../../../services/bootCheckService')>();
|
||||
return {
|
||||
...actual,
|
||||
recoverPortConflict: (...args: unknown[]) => mockRecoverPortConflict(...args),
|
||||
};
|
||||
});
|
||||
|
||||
const mockTestCoreRpcConnection = vi.fn();
|
||||
vi.mock('../../../services/coreRpcClient', () => ({
|
||||
callCoreRpc: vi.fn(),
|
||||
@@ -572,6 +581,125 @@ describe('BootCheckGate — pre-set mode (subsequent launches)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('BootCheckGate — port conflict recovery', () => {
|
||||
beforeEach(() => {
|
||||
mockRecoverPortConflict.mockReset();
|
||||
mockRunBootCheck.mockReset();
|
||||
});
|
||||
|
||||
it('shows "Fix Automatically" button when portConflict=true', async () => {
|
||||
mockRunBootCheck.mockResolvedValue({
|
||||
kind: 'unreachable',
|
||||
reason: 'port conflict',
|
||||
portConflict: true,
|
||||
});
|
||||
|
||||
renderGate();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('fix-automatically-btn')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('fix-automatically-btn').textContent).toBe('Fix Automatically');
|
||||
});
|
||||
|
||||
it('does not show "Fix Automatically" button when portConflict is not set', async () => {
|
||||
mockRunBootCheck.mockResolvedValue({ kind: 'unreachable', reason: 'some other error' });
|
||||
|
||||
renderGate();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Can't Reach the Runtime")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByTestId('fix-automatically-btn')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls recoverPortConflict when "Fix Automatically" is clicked', async () => {
|
||||
mockRunBootCheck
|
||||
.mockResolvedValueOnce({ kind: 'unreachable', reason: 'port conflict', portConflict: true })
|
||||
.mockResolvedValue({ kind: 'match' });
|
||||
mockRecoverPortConflict.mockResolvedValue({ success: true, message: 'ok', new_port: 7789 });
|
||||
|
||||
renderGate();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('fix-automatically-btn')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('fix-automatically-btn'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRecoverPortConflict).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('re-runs boot check after successful recovery', async () => {
|
||||
mockRunBootCheck
|
||||
.mockResolvedValueOnce({ kind: 'unreachable', reason: 'port conflict', portConflict: true })
|
||||
.mockResolvedValue({ kind: 'match' });
|
||||
mockRecoverPortConflict.mockResolvedValue({ success: true, message: 'ok', new_port: 7789 });
|
||||
|
||||
renderGate();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('fix-automatically-btn')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('fix-automatically-btn'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('app-content')).toBeInTheDocument();
|
||||
});
|
||||
expect(mockRunBootCheck).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('shows portConflictFixFailed message when recovery fails', async () => {
|
||||
mockRunBootCheck.mockResolvedValue({
|
||||
kind: 'unreachable',
|
||||
reason: 'port conflict',
|
||||
portConflict: true,
|
||||
});
|
||||
mockRecoverPortConflict.mockResolvedValue({
|
||||
success: false,
|
||||
message: 'still busy',
|
||||
new_port: undefined,
|
||||
});
|
||||
|
||||
renderGate();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('fix-automatically-btn')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('fix-automatically-btn'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Automatic fix didn't work. Please restart your computer and try again.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('"Pick a Different Runtime" still renders as secondary for port conflict', async () => {
|
||||
mockRunBootCheck.mockResolvedValue({
|
||||
kind: 'unreachable',
|
||||
reason: 'port conflict',
|
||||
portConflict: true,
|
||||
});
|
||||
|
||||
renderGate();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Pick a Different Runtime' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('BootCheckGate — picker (web build, !isTauri)', () => {
|
||||
beforeEach(() => {
|
||||
mockedIsTauri.mockReturnValue(false);
|
||||
|
||||
@@ -143,6 +143,11 @@ describe('runBootCheck — local mode', () => {
|
||||
vi.useRealTimers();
|
||||
|
||||
expect(result.kind).toBe('unreachable');
|
||||
// start_core_process succeeded (invokeCmd resolves) so portConflict must NOT be set —
|
||||
// the timeout alone is not evidence of a port conflict.
|
||||
if (result.kind === 'unreachable') {
|
||||
expect(result.portConflict).toBeFalsy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -214,6 +219,87 @@ describe('runBootCheck — unset mode', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Port conflict auto-recovery tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('runBootCheck — port conflict auto-recovery', () => {
|
||||
it('auto-recovery succeeds: start fails, recovery succeeds, second start succeeds', async () => {
|
||||
const appVersion = (await import('../../utils/config')).APP_VERSION;
|
||||
|
||||
let startCallCount = 0;
|
||||
const transport: BootCheckTransport = {
|
||||
callRpc: rpcResponder({
|
||||
'core.ping': {},
|
||||
'openhuman.service_status': { installed: false, running: false },
|
||||
'openhuman.update_version': { result: { version: appVersion } },
|
||||
}),
|
||||
invokeCmd: vi.fn(async (cmd: string) => {
|
||||
if (cmd === 'start_core_process') {
|
||||
startCallCount += 1;
|
||||
if (startCallCount === 1) throw new Error('port in use');
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}) as BootCheckTransport['invokeCmd'],
|
||||
recoverPortConflict: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ success: true, message: 'recovered', new_port: 7789 }),
|
||||
};
|
||||
|
||||
const result = await runBootCheck({ kind: 'local' }, transport);
|
||||
expect(result.kind).toBe('match');
|
||||
expect(transport.recoverPortConflict).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns unreachable with portConflict=true when both start and recovery fail', async () => {
|
||||
const transport: BootCheckTransport = {
|
||||
callRpc: vi.fn(),
|
||||
invokeCmd: vi.fn().mockRejectedValue(new Error('port in use')),
|
||||
recoverPortConflict: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ success: false, message: 'port still busy', new_port: undefined }),
|
||||
};
|
||||
|
||||
const result = await runBootCheck({ kind: 'local' }, transport);
|
||||
expect(result.kind).toBe('unreachable');
|
||||
if (result.kind === 'unreachable') {
|
||||
expect(result.portConflict).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('clears RPC URL cache and retries waitForCore on timeout', async () => {
|
||||
const appVersion = (await import('../../utils/config')).APP_VERSION;
|
||||
|
||||
let pingCallCount = 0;
|
||||
const transport: BootCheckTransport = {
|
||||
invokeCmd: vi.fn().mockResolvedValue(undefined),
|
||||
callRpc: vi.fn(async (method: string) => {
|
||||
if (method === 'core.ping') {
|
||||
pingCallCount += 1;
|
||||
// waitForCore(10_000) makes ~12 attempts with 200→1000ms exponential backoff.
|
||||
// Fail exactly those 12 so the initial call times out; ping 13 succeeds so
|
||||
// the cache-clear retry waitForCore(5_000) returns true on its first attempt.
|
||||
if (pingCallCount <= 12) throw new Error('timeout');
|
||||
return {};
|
||||
}
|
||||
if (method === 'openhuman.service_status') return { installed: false, running: false };
|
||||
if (method === 'openhuman.update_version') return { result: { version: appVersion } };
|
||||
throw new Error(`Unexpected RPC: ${method}`);
|
||||
}) as BootCheckTransport['callRpc'],
|
||||
};
|
||||
|
||||
vi.useFakeTimers();
|
||||
const promise = runBootCheck({ kind: 'local' }, transport);
|
||||
await vi.runAllTimersAsync();
|
||||
const result = await promise;
|
||||
vi.useRealTimers();
|
||||
|
||||
// Initial waitForCore timed out → cache cleared → second waitForCore succeeded.
|
||||
expect(result.kind).toBe('match');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Edge-case branches surfaced by the diff-coverage gate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -31,7 +31,7 @@ export type BootCheckResult =
|
||||
| { kind: 'outdatedLocal' }
|
||||
| { kind: 'outdatedCloud' }
|
||||
| { kind: 'noVersionMethod' }
|
||||
| { kind: 'unreachable'; reason: string };
|
||||
| { kind: 'unreachable'; reason: string; portConflict?: boolean };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Transport interface (injectable for tests)
|
||||
@@ -42,6 +42,8 @@ export interface BootCheckTransport {
|
||||
callRpc: <T>(method: string, params?: Record<string, unknown>) => Promise<T>;
|
||||
/** Invoke a Tauri command. */
|
||||
invokeCmd: <T>(cmd: string, args?: Record<string, unknown>) => Promise<T>;
|
||||
/** Attempt to auto-recover from a port conflict. Optional — only wired in desktop builds. */
|
||||
recoverPortConflict?: () => Promise<{ success: boolean; message: string; new_port?: number }>;
|
||||
}
|
||||
|
||||
// The production transport lives in `app/src/services/bootCheckService.ts`
|
||||
@@ -201,22 +203,62 @@ export async function runBootCheck(
|
||||
if (mode.kind === 'local') {
|
||||
log('[boot-check] local mode — starting core process');
|
||||
|
||||
let startFailed = false;
|
||||
try {
|
||||
await invokeCmd<void>('start_core_process', {});
|
||||
log('[boot-check] start_core_process invoked successfully');
|
||||
} catch (err) {
|
||||
startFailed = true;
|
||||
logError('[boot-check] start_core_process failed: %o', err);
|
||||
return {
|
||||
kind: 'unreachable',
|
||||
reason: `Failed to start local core: ${err instanceof Error ? err.message : String(err)}`,
|
||||
};
|
||||
}
|
||||
|
||||
// If start failed, attempt port-conflict auto-recovery before giving up.
|
||||
if (startFailed && transport.recoverPortConflict) {
|
||||
log('[boot-check] start_core_process failed — attempting port-conflict auto-recovery');
|
||||
try {
|
||||
const recovery = await transport.recoverPortConflict();
|
||||
log(
|
||||
'[boot-check] port-conflict recovery result: success=%s message=%s',
|
||||
recovery.success,
|
||||
recovery.message
|
||||
);
|
||||
if (!recovery.success) {
|
||||
logError('[boot-check] port-conflict recovery failed: %s', recovery.message);
|
||||
return {
|
||||
kind: 'unreachable',
|
||||
reason: `Failed to start local core — port conflict recovery failed: ${recovery.message}`,
|
||||
portConflict: true,
|
||||
};
|
||||
}
|
||||
// Recovery succeeded — clear the URL cache so we pick up the new port.
|
||||
clearCoreRpcUrlCache();
|
||||
log('[boot-check] port-conflict recovery succeeded, RPC URL cache cleared');
|
||||
} catch (recoveryErr) {
|
||||
logError('[boot-check] port-conflict recovery threw: %o', recoveryErr);
|
||||
return {
|
||||
kind: 'unreachable',
|
||||
reason: `Failed to start local core — recovery error: ${recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr)}`,
|
||||
portConflict: true,
|
||||
};
|
||||
}
|
||||
} else if (startFailed) {
|
||||
return { kind: 'unreachable', reason: `Failed to start local core` };
|
||||
}
|
||||
|
||||
// Wait for the embedded core to be reachable.
|
||||
const reachable = await waitForCore(callRpc);
|
||||
let reachable = await waitForCore(callRpc);
|
||||
if (!reachable) {
|
||||
logError('[boot-check] local core unreachable after retries');
|
||||
return { kind: 'unreachable', reason: 'Local core did not respond in time' };
|
||||
logError('[boot-check] local core unreachable after retries — trying cache clear + retry');
|
||||
clearCoreRpcUrlCache();
|
||||
reachable = await waitForCore(callRpc, 5_000);
|
||||
}
|
||||
if (!reachable) {
|
||||
logError('[boot-check] local core unreachable after retries and cache clear');
|
||||
return {
|
||||
kind: 'unreachable',
|
||||
reason: 'Local core did not respond in time',
|
||||
portConflict: startFailed,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for a legacy background daemon that should be removed.
|
||||
|
||||
@@ -185,6 +185,13 @@ const ar3: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': 'إعادة تشغيل / تحديث بيئة التشغيل',
|
||||
'bootCheck.unexpectedError': 'خطأ غير متوقع في فحص بدء التشغيل',
|
||||
'bootCheck.actionFailed': 'حدث خطأ ما. يرجى المحاولة مرة أخرى.',
|
||||
'bootCheck.portConflictTitle': 'تعذّر تشغيل محرّك التطبيق',
|
||||
'bootCheck.portConflictBody':
|
||||
'هناك عملية أخرى تستخدم منفذ الشبكة الذي يحتاجه OpenHuman. سنحاول إصلاح ذلك تلقائيًا.',
|
||||
'bootCheck.portConflictFixButton': 'إصلاح تلقائي',
|
||||
'bootCheck.portConflictFixing': 'جارٍ الإصلاح…',
|
||||
'bootCheck.portConflictFixFailed':
|
||||
'لم ينجح الإصلاح التلقائي. يُرجى إعادة تشغيل الكمبيوتر والمحاولة مجددًا.',
|
||||
'notifications.justNow': 'الآن',
|
||||
'notifications.minAgo': 'منذ {n} د',
|
||||
'notifications.hrAgo': 'منذ {n} س',
|
||||
|
||||
@@ -190,6 +190,13 @@ const bn3: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': 'রানটাইম রিস্টার্ট / আপডেট করুন',
|
||||
'bootCheck.unexpectedError': 'অপ্রত্যাশিত বুট-চেক ত্রুটি',
|
||||
'bootCheck.actionFailed': 'কিছু একটা ভুল হয়েছে। আবার চেষ্টা করুন।',
|
||||
'bootCheck.portConflictTitle': 'অ্যাপ ইঞ্জিন চালু করা যায়নি',
|
||||
'bootCheck.portConflictBody':
|
||||
'অন্য একটি প্রক্রিয়া OpenHuman-এর প্রয়োজনীয় নেটওয়ার্ক পোর্ট ব্যবহার করছে। আমরা স্বয়ংক্রিয়ভাবে এটি ঠিক করার চেষ্টা করব।',
|
||||
'bootCheck.portConflictFixButton': 'স্বয়ংক্রিয়ভাবে ঠিক করুন',
|
||||
'bootCheck.portConflictFixing': 'ঠিক করা হচ্ছে…',
|
||||
'bootCheck.portConflictFixFailed':
|
||||
'স্বয়ংক্রিয় সংশোধন কাজ করেনি। অনুগ্রহ করে আপনার কম্পিউটার পুনরায় চালু করুন এবং আবার চেষ্টা করুন।',
|
||||
'notifications.justNow': 'এইমাত্র',
|
||||
'notifications.minAgo': '{n} মিনিট আগে',
|
||||
'notifications.hrAgo': '{n} ঘণ্টা আগে',
|
||||
|
||||
@@ -195,6 +195,13 @@ const de3: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': 'Runtime neu starten/aktualisieren',
|
||||
'bootCheck.unexpectedError': 'Unerwarteter Boot-Check-Fehler',
|
||||
'bootCheck.actionFailed': 'Etwas ist schief gelaufen. Bitte versuche es erneut.',
|
||||
'bootCheck.portConflictTitle': 'App-Engine konnte nicht gestartet werden',
|
||||
'bootCheck.portConflictBody':
|
||||
'Ein anderer Prozess nutzt den Netzwerkport, den OpenHuman benötigt. Wir versuchen, das automatisch zu beheben.',
|
||||
'bootCheck.portConflictFixButton': 'Automatisch beheben',
|
||||
'bootCheck.portConflictFixing': 'Wird behoben…',
|
||||
'bootCheck.portConflictFixFailed':
|
||||
'Automatische Behebung fehlgeschlagen. Bitte starten Sie Ihren Computer neu und versuchen Sie es erneut.',
|
||||
'notifications.justNow': 'gerade jetzt',
|
||||
'notifications.minAgo': 'Vor {n}m',
|
||||
'notifications.hrAgo': 'Vor {n}h',
|
||||
|
||||
@@ -189,6 +189,13 @@ const en3: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': 'Restart / Update Runtime',
|
||||
'bootCheck.unexpectedError': 'Unexpected Boot-Check Error',
|
||||
'bootCheck.actionFailed': 'Something went wrong. Please try again.',
|
||||
'bootCheck.portConflictTitle': "Couldn't Start the App Engine",
|
||||
'bootCheck.portConflictBody':
|
||||
"Another process is using the network port OpenHuman needs. We'll try to fix this automatically.",
|
||||
'bootCheck.portConflictFixButton': 'Fix Automatically',
|
||||
'bootCheck.portConflictFixing': 'Fixing…',
|
||||
'bootCheck.portConflictFixFailed':
|
||||
"Automatic fix didn't work. Please restart your computer and try again.",
|
||||
'notifications.justNow': 'just now',
|
||||
'notifications.minAgo': '{n}m ago',
|
||||
'notifications.hrAgo': '{n}h ago',
|
||||
|
||||
@@ -193,6 +193,13 @@ const es3: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': 'Reiniciar / Actualizar runtime',
|
||||
'bootCheck.unexpectedError': 'Error inesperado en verificación de arranque',
|
||||
'bootCheck.actionFailed': 'Algo salió mal. Inténtalo de nuevo.',
|
||||
'bootCheck.portConflictTitle': 'No se pudo iniciar el motor de la aplicación',
|
||||
'bootCheck.portConflictBody':
|
||||
'Otro proceso está usando el puerto de red que OpenHuman necesita. Intentaremos solucionarlo automáticamente.',
|
||||
'bootCheck.portConflictFixButton': 'Corregir automáticamente',
|
||||
'bootCheck.portConflictFixing': 'Corrigiendo…',
|
||||
'bootCheck.portConflictFixFailed':
|
||||
'La corrección automática no funcionó. Reinicia tu equipo e inténtalo de nuevo.',
|
||||
'notifications.justNow': 'justo ahora',
|
||||
'notifications.minAgo': 'hace {n}m',
|
||||
'notifications.hrAgo': 'hace {n}h',
|
||||
|
||||
@@ -194,6 +194,13 @@ const fr3: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': 'Redémarrer / Mettre à jour le runtime',
|
||||
'bootCheck.unexpectedError': 'Erreur inattendue lors de la vérification au démarrage',
|
||||
'bootCheck.actionFailed': "Une erreur s'est produite. Réessaie.",
|
||||
'bootCheck.portConflictTitle': "Impossible de démarrer le moteur de l'application",
|
||||
'bootCheck.portConflictBody':
|
||||
'Un autre processus utilise le port réseau dont OpenHuman a besoin. Nous allons tenter de corriger cela automatiquement.',
|
||||
'bootCheck.portConflictFixButton': 'Corriger automatiquement',
|
||||
'bootCheck.portConflictFixing': 'Correction en cours…',
|
||||
'bootCheck.portConflictFixFailed':
|
||||
"La correction automatique n'a pas fonctionné. Veuillez redémarrer votre ordinateur et réessayer.",
|
||||
'notifications.justNow': "à l'instant",
|
||||
'notifications.minAgo': 'il y a {n} min',
|
||||
'notifications.hrAgo': 'il y a {n} h',
|
||||
|
||||
@@ -189,6 +189,13 @@ const hi3: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': 'रनटाइम रीस्टार्ट / अपडेट करें',
|
||||
'bootCheck.unexpectedError': 'अनपेक्षित बूट-चेक एरर',
|
||||
'bootCheck.actionFailed': 'कुछ गड़बड़ हो गई। दोबारा कोशिश करें।',
|
||||
'bootCheck.portConflictTitle': 'ऐप इंजन शुरू नहीं हो सका',
|
||||
'bootCheck.portConflictBody':
|
||||
'कोई अन्य प्रक्रिया उस नेटवर्क पोर्ट का उपयोग कर रही है जो OpenHuman को चाहिए। हम इसे स्वचालित रूप से ठीक करने का प्रयास करेंगे।',
|
||||
'bootCheck.portConflictFixButton': 'स्वचालित रूप से ठीक करें',
|
||||
'bootCheck.portConflictFixing': 'ठीक हो रहा है…',
|
||||
'bootCheck.portConflictFixFailed':
|
||||
'स्वचालित सुधार काम नहीं आया। कृपया अपना कंप्यूटर पुनः आरंभ करें और पुनः प्रयास करें।',
|
||||
'notifications.justNow': 'अभी-अभी',
|
||||
'notifications.minAgo': '{n}मि. पहले',
|
||||
'notifications.hrAgo': '{n}घं. पहले',
|
||||
|
||||
@@ -190,6 +190,13 @@ const id3: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': 'Mulai Ulang / Perbarui Runtime',
|
||||
'bootCheck.unexpectedError': 'Kesalahan Boot-Check Tak Terduga',
|
||||
'bootCheck.actionFailed': 'Terjadi kesalahan. Silakan coba lagi.',
|
||||
'bootCheck.portConflictTitle': 'Tidak dapat memulai mesin aplikasi',
|
||||
'bootCheck.portConflictBody':
|
||||
'Proses lain sedang menggunakan port jaringan yang dibutuhkan OpenHuman. Kami akan mencoba memperbaikinya secara otomatis.',
|
||||
'bootCheck.portConflictFixButton': 'Perbaiki Otomatis',
|
||||
'bootCheck.portConflictFixing': 'Memperbaiki…',
|
||||
'bootCheck.portConflictFixFailed':
|
||||
'Perbaikan otomatis tidak berhasil. Silakan restart komputer Anda dan coba lagi.',
|
||||
'notifications.justNow': 'baru saja',
|
||||
'notifications.minAgo': '{n}m lalu',
|
||||
'notifications.hrAgo': '{n}j lalu',
|
||||
|
||||
@@ -193,6 +193,13 @@ const it3: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': 'Riavvia / aggiorna runtime',
|
||||
'bootCheck.unexpectedError': 'Errore inatteso del controllo di avvio',
|
||||
'bootCheck.actionFailed': 'Qualcosa è andato storto. Riprova.',
|
||||
'bootCheck.portConflictTitle': "Impossibile avviare il motore dell'app",
|
||||
'bootCheck.portConflictBody':
|
||||
'Un altro processo sta usando la porta di rete necessaria a OpenHuman. Tenteremo di risolvere il problema automaticamente.',
|
||||
'bootCheck.portConflictFixButton': 'Correzione automatica',
|
||||
'bootCheck.portConflictFixing': 'Correzione in corso…',
|
||||
'bootCheck.portConflictFixFailed':
|
||||
'La correzione automatica non ha funzionato. Riavvia il computer e riprova.',
|
||||
'notifications.justNow': 'adesso',
|
||||
'notifications.minAgo': '{n}m fa',
|
||||
'notifications.hrAgo': '{n}h fa',
|
||||
|
||||
@@ -188,6 +188,13 @@ const ko3: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': '런타임 다시 시작 / 업데이트',
|
||||
'bootCheck.unexpectedError': '예상치 못한 부트 체크 오류',
|
||||
'bootCheck.actionFailed': '문제가 발생했습니다. 다시 시도해 주세요.',
|
||||
'bootCheck.portConflictTitle': '앱 엔진을 시작할 수 없습니다',
|
||||
'bootCheck.portConflictBody':
|
||||
'다른 프로세스가 OpenHuman에 필요한 네트워크 포트를 사용 중입니다. 자동으로 문제를 해결해 드리겠습니다.',
|
||||
'bootCheck.portConflictFixButton': '자동 수정',
|
||||
'bootCheck.portConflictFixing': '수정 중…',
|
||||
'bootCheck.portConflictFixFailed':
|
||||
'자동 수정에 실패했습니다. 컴퓨터를 재시작한 후 다시 시도해 주세요.',
|
||||
'notifications.justNow': '방금 전',
|
||||
'notifications.minAgo': '{n}분 전',
|
||||
'notifications.hrAgo': '{n}시간 전',
|
||||
|
||||
@@ -192,6 +192,13 @@ const pt3: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': 'Reiniciar / Atualizar Runtime',
|
||||
'bootCheck.unexpectedError': 'Erro Inesperado na Verificação de Inicialização',
|
||||
'bootCheck.actionFailed': 'Algo deu errado. Por favor, tente novamente.',
|
||||
'bootCheck.portConflictTitle': 'Não foi possível iniciar o motor do aplicativo',
|
||||
'bootCheck.portConflictBody':
|
||||
'Outro processo está usando a porta de rede que o OpenHuman precisa. Tentaremos corrigir isso automaticamente.',
|
||||
'bootCheck.portConflictFixButton': 'Corrigir automaticamente',
|
||||
'bootCheck.portConflictFixing': 'Corrigindo…',
|
||||
'bootCheck.portConflictFixFailed':
|
||||
'A correção automática não funcionou. Reinicie o computador e tente novamente.',
|
||||
'notifications.justNow': 'agora mesmo',
|
||||
'notifications.minAgo': '{n}min atrás',
|
||||
'notifications.hrAgo': '{n}h atrás',
|
||||
|
||||
@@ -190,6 +190,13 @@ const ru3: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': 'Перезапустить / обновить среду',
|
||||
'bootCheck.unexpectedError': 'Неожиданная ошибка при загрузке',
|
||||
'bootCheck.actionFailed': 'Что-то пошло не так. Попробуй ещё раз.',
|
||||
'bootCheck.portConflictTitle': 'Не удалось запустить движок приложения',
|
||||
'bootCheck.portConflictBody':
|
||||
'Другой процесс использует сетевой порт, необходимый OpenHuman. Попробуем устранить это автоматически.',
|
||||
'bootCheck.portConflictFixButton': 'Исправить автоматически',
|
||||
'bootCheck.portConflictFixing': 'Исправление…',
|
||||
'bootCheck.portConflictFixFailed':
|
||||
'Автоматическое исправление не сработало. Перезагрузите компьютер и попробуйте снова.',
|
||||
'notifications.justNow': 'только что',
|
||||
'notifications.minAgo': '{n} мин назад',
|
||||
'notifications.hrAgo': '{n} ч назад',
|
||||
|
||||
@@ -181,6 +181,12 @@ const zhCN3: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': '重启 / 更新核心',
|
||||
'bootCheck.unexpectedError': '意外的启动检查错误',
|
||||
'bootCheck.actionFailed': '操作失败 — 请重试。',
|
||||
'bootCheck.portConflictTitle': '无法启动应用引擎',
|
||||
'bootCheck.portConflictBody':
|
||||
'另一个进程正在占用 OpenHuman 所需的网络端口。我们将尝试自动修复此问题。',
|
||||
'bootCheck.portConflictFixButton': '自动修复',
|
||||
'bootCheck.portConflictFixing': '修复中…',
|
||||
'bootCheck.portConflictFixFailed': '自动修复未成功。请重启您的计算机后重试。',
|
||||
'notifications.justNow': '刚刚',
|
||||
'notifications.minAgo': '{n} 分钟前',
|
||||
'notifications.hrAgo': '{n} 小时前',
|
||||
|
||||
@@ -1754,6 +1754,13 @@ const en: TranslationMap = {
|
||||
'bootCheck.restartUpdateCore': 'Restart / Update Runtime',
|
||||
'bootCheck.unexpectedError': 'Unexpected Boot-Check Error',
|
||||
'bootCheck.actionFailed': 'Something went wrong. Please try again.',
|
||||
'bootCheck.portConflictTitle': "Couldn't Start the App Engine",
|
||||
'bootCheck.portConflictBody':
|
||||
"Another process is using the network port OpenHuman needs. We'll try to fix this automatically.",
|
||||
'bootCheck.portConflictFixButton': 'Fix Automatically',
|
||||
'bootCheck.portConflictFixing': 'Fixing…',
|
||||
'bootCheck.portConflictFixFailed':
|
||||
"Automatic fix didn't work. Please restart your computer and try again.",
|
||||
|
||||
// Notifications: category labels & timestamps
|
||||
'notifications.justNow': 'just now',
|
||||
|
||||
@@ -36,3 +36,23 @@ describe('bootCheckTransport', () => {
|
||||
expect(invokeMock).toHaveBeenCalledWith('start_core_process', {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('recoverPortConflict', () => {
|
||||
it('calls the recover_port_conflict Tauri command and returns the result', async () => {
|
||||
const fakeOutcome = { success: true, message: 'Core recovered on port 7789', new_port: 7789 };
|
||||
invokeMock.mockResolvedValueOnce(fakeOutcome);
|
||||
|
||||
const { recoverPortConflict } = await import('./bootCheckService');
|
||||
const result = await recoverPortConflict();
|
||||
|
||||
expect(result).toEqual(fakeOutcome);
|
||||
expect(invokeMock).toHaveBeenCalledWith('recover_port_conflict', undefined);
|
||||
});
|
||||
|
||||
it('propagates errors from the Tauri command', async () => {
|
||||
invokeMock.mockRejectedValueOnce(new Error('IPC failure'));
|
||||
|
||||
const { recoverPortConflict } = await import('./bootCheckService');
|
||||
await expect(recoverPortConflict()).rejects.toThrow('IPC failure');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,4 +20,16 @@ async function invokeCmd<T>(cmd: string, args?: Record<string, unknown>): Promis
|
||||
return invoke<T>(cmd, args);
|
||||
}
|
||||
|
||||
export const bootCheckTransport: BootCheckTransport = { callRpc, invokeCmd };
|
||||
/**
|
||||
* Invoke the `recover_port_conflict` Tauri command to reap stale OpenHuman
|
||||
* processes and restart the embedded core on any available port.
|
||||
*/
|
||||
export async function recoverPortConflict(): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
new_port?: number;
|
||||
}> {
|
||||
return invokeCmd('recover_port_conflict');
|
||||
}
|
||||
|
||||
export const bootCheckTransport: BootCheckTransport = { callRpc, invokeCmd, recoverPortConflict };
|
||||
|
||||
Reference in New Issue
Block a user