Compare commits

...

6 Commits

Author SHA1 Message Date
bincxz
986f552779 Closes settings window with main window exit
Some checks failed
build-packages / build-windows-latest (push) Has been cancelled
build-packages / build-macos-latest (push) Has been cancelled
build-packages / release (push) Has been cancelled
Ensures the settings window closes automatically when the main window is closed, preventing orphaned settings windows and improving overall window lifecycle management.
2026-01-07 00:35:01 +08:00
bincxz
42647e3572 Removes parent window assignment for settings window
Avoids rendering issues on macOS and prevents unintended main window closure on Windows by not assigning a parent window when opening the settings window.
2026-01-07 00:29:50 +08:00
陈大猫
5930d1601a Merge pull request #31 from binaricat:copilot/fix-port-forwarding-status
Some checks failed
build-packages / build-macos-latest (push) Has been cancelled
build-packages / build-windows-latest (push) Has been cancelled
build-packages / release (push) Has been cancelled
Fix port forwarding status sync on app restart
2026-01-06 20:36:10 +08:00
copilot-swe-agent[bot]
df3d507e2b Improve tunnel ID parsing with clearer UUID validation
Address code review feedback:
- Extract parseRuleIdFromTunnelId helper function with clear UUID validation
- Use constants for tunnel ID prefix and UUID regex pattern
- Simplify syncWithBackend to return void since return value wasn't used

Co-authored-by: binaricat <16399091+binaricat@users.noreply.github.com>
2026-01-06 12:17:20 +00:00
copilot-swe-agent[bot]
f8c7a9081b Fix port forwarding status sync on app restart
- Add stopAllPortForwards() to portForwardingBridge.cjs to cleanup tunnels on app quit
- Call port forwarding cleanup in main.cjs will-quit handler
- Add syncWithBackend() to portForwardingService.ts to query backend for active tunnels
- Update usePortForwardingState.ts to sync with backend on mount

Co-authored-by: binaricat <16399091+binaricat@users.noreply.github.com>
2026-01-06 12:15:50 +00:00
copilot-swe-agent[bot]
d8cfb0f1d9 Initial plan 2026-01-06 12:10:04 +00:00
5 changed files with 130 additions and 22 deletions

View File

@@ -11,6 +11,7 @@ import {
getActiveRuleIds,
startPortForward,
stopPortForward,
syncWithBackend,
} from "../../infrastructure/services/portForwardingService";
import { useStoredViewMode, ViewMode } from "./useStoredViewMode";
@@ -78,25 +79,32 @@ export const usePortForwardingState = (): UsePortForwardingStateResult => {
localStorageAdapter.writeBoolean(STORAGE_KEY_PF_PREFER_FORM_MODE, prefer);
}, []);
// Load rules from storage on mount
// Load rules from storage on mount and sync with backend
useEffect(() => {
const saved = localStorageAdapter.read<PortForwardingRule[]>(
STORAGE_KEY_PORT_FORWARDING,
);
if (saved && Array.isArray(saved)) {
// Sync status with active connections in the service layer
const _activeRuleIds = getActiveRuleIds();
const withSyncedStatus = saved.map((r) => {
const conn = getActiveConnection(r.id);
if (conn) {
// This rule has an active connection, preserve its status
return { ...r, status: conn.status, error: conn.error };
}
// No active connection, reset to inactive
return { ...r, status: "inactive" as const, error: undefined };
});
setRules(withSyncedStatus);
}
const loadAndSync = async () => {
// First, sync with backend to get any active tunnels
await syncWithBackend();
const saved = localStorageAdapter.read<PortForwardingRule[]>(
STORAGE_KEY_PORT_FORWARDING,
);
if (saved && Array.isArray(saved)) {
// Sync status with active connections in the service layer
const _activeRuleIds = getActiveRuleIds();
const withSyncedStatus = saved.map((r) => {
const conn = getActiveConnection(r.id);
if (conn) {
// This rule has an active connection, preserve its status
return { ...r, status: conn.status, error: conn.error };
}
// No active connection, reset to inactive
return { ...r, status: "inactive" as const, error: undefined };
});
setRules(withSyncedStatus);
}
};
void loadAndSync();
}, []);
// Persist rules to storage whenever they change

View File

@@ -313,6 +313,28 @@ async function listPortForwards() {
return list;
}
/**
* Stop all active port forwards (cleanup on app quit)
*/
function stopAllPortForwards() {
console.log(`[PortForward] Stopping all ${portForwardingTunnels.size} active tunnels...`);
for (const [tunnelId, tunnel] of portForwardingTunnels) {
try {
if (tunnel.server) {
tunnel.server.close();
}
if (tunnel.conn) {
tunnel.conn.end();
}
console.log(`[PortForward] Stopped tunnel ${tunnelId}`);
} catch (err) {
console.warn(`[PortForward] Failed to stop tunnel ${tunnelId}:`, err.message);
}
}
portForwardingTunnels.clear();
console.log('[PortForward] All tunnels stopped');
}
/**
* Register IPC handlers for port forwarding operations
*/
@@ -329,4 +351,5 @@ module.exports = {
stopPortForward,
getPortForwardStatus,
listPortForwards,
stopAllPortForwards,
};

View File

@@ -615,6 +615,8 @@ async function createWindow(electronModule, options) {
if (saveStateTimer) clearTimeout(saveStateTimer);
const state = getWindowBoundsState(win, lastNormalBounds);
if (state) saveWindowState(state);
// Close settings window when main window closes
closeSettingsWindow();
});
win.on("enter-full-screen", () => {
@@ -731,9 +733,10 @@ async function openSettingsWindow(electronModule, options) {
backgroundColor,
icon: appIcon,
fullscreenable: !isMac,
// NOTE: Do NOT set parent on Windows - it can cause the main window to close
// when the settings window is closed in some edge cases.
parent: isMac ? mainWindow : undefined,
// NOTE: Do NOT set parent - on macOS this causes rendering issues when dragging
// the window to a different screen (the window becomes invisible while still
// appearing in "Show All Windows" in the Dock). On Windows it can cause the
// main window to close when the settings window is closed.
modal: false,
show: false,
frame: isMac,

View File

@@ -539,13 +539,18 @@ app.on("window-all-closed", () => {
}
});
// Cleanup all PTY sessions before quitting to prevent node-pty assertion errors
// Cleanup all PTY sessions and port forwarding tunnels before quitting
app.on("will-quit", () => {
try {
terminalBridge.cleanupAllSessions();
} catch (err) {
console.warn("Error during terminal cleanup:", err);
}
try {
portForwardingBridge.stopAllPortForwards();
} catch (err) {
console.warn("Error during port forwarding cleanup:", err);
}
});
// Export for testing

View File

@@ -35,6 +35,75 @@ export const getActiveRuleIds = (): string[] => {
.map(([ruleId]) => ruleId);
};
// Tunnel ID prefix and UUID regex pattern for parsing
const TUNNEL_ID_PREFIX = 'pf-';
// UUID format: 8-4-4-4-12 hexadecimal characters
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/**
* Parse rule ID from tunnel ID
* Tunnel ID format is "pf-{ruleId}-{timestamp}" where ruleId is a UUID
*/
const parseRuleIdFromTunnelId = (tunnelId: string): string | null => {
if (!tunnelId.startsWith(TUNNEL_ID_PREFIX)) {
return null;
}
// Remove prefix and split remaining parts
const withoutPrefix = tunnelId.slice(TUNNEL_ID_PREFIX.length);
const parts = withoutPrefix.split('-');
// UUID has 5 parts (8-4-4-4-12), so we need at least 6 parts (5 UUID + timestamp)
if (parts.length < 6) {
return null;
}
// Reconstruct the UUID from first 5 parts
const ruleId = parts.slice(0, 5).join('-');
// Validate it's a proper UUID format
if (!UUID_REGEX.test(ruleId)) {
return null;
}
return ruleId;
};
/**
* Sync active connections with backend
* Called on app startup to restore state of tunnels that may still be running
* This updates the local activeConnections map to match the backend state.
*/
export const syncWithBackend = async (): Promise<void> => {
const bridge = netcattyBridge.get();
if (!bridge?.listPortForwards) {
logger.warn('[PortForwardingService] Backend not available for sync');
return;
}
try {
const activeTunnels = await bridge.listPortForwards();
logger.info(`[PortForwardingService] Backend reports ${activeTunnels.length} active tunnels`);
for (const tunnel of activeTunnels) {
const ruleId = parseRuleIdFromTunnelId(tunnel.tunnelId);
if (ruleId) {
// Update local connection tracking
activeConnections.set(ruleId, {
ruleId,
tunnelId: tunnel.tunnelId,
status: 'active',
});
logger.info(`[PortForwardingService] Synced active tunnel for rule ${ruleId}`);
}
}
} catch (err) {
logger.error('[PortForwardingService] Failed to sync with backend:', err);
}
};
/**
* Start a port forwarding tunnel
*/