fix(startup): recover from core port 7788 conflict automatically (#2626)

This commit is contained in:
Mega Mind
2026-05-25 20:09:08 +05:30
committed by GitHub
parent d997394116
commit cdee8f73fb
27 changed files with 1209 additions and 45 deletions
@@ -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);
+86
View File
@@ -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
// ---------------------------------------------------------------------------
+50 -8
View File
@@ -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.
+7
View File
@@ -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} س',
+7
View File
@@ -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} ঘণ্টা আগে',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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}घं. पहले',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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}시간 전',
+7
View File
@@ -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',
+7
View File
@@ -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} ч назад',
+6
View File
@@ -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} 小时前',
+7
View File
@@ -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',
+20
View File
@@ -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');
});
});
+13 -1
View File
@@ -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 };