2025-11-05 10:36:16 -06:00
const {
app ,
BrowserWindow ,
2026-10-20 16:03:29 -05:00
WebContentsView ,
2025-11-05 10:36:16 -06:00
shell ,
ipcMain ,
dialog ,
Menu ,
2026-05-06 15:12:07 -05:00
session ,
safeStorage ,
2026-03-08 18:02:14 -05:00
Tray ,
2026-05-06 15:12:07 -05:00
clipboard ,
2026-06-29 13:28:26 -05:00
nativeImage ,
2025-11-05 10:36:16 -06:00
} = require ( "electron" );
2025-09-12 14:42:00 -05:00
const path = require ( "path" );
2026-08-06 14:41:39 -05:00
const { getUnpackedAppRoot } = require ( "./backend-paths.cjs" );
2026-09-20 14:56:39 -05:00
const { classifyBackendFailure } = require ( "./backend-failure.cjs" );
2025-09-12 14:42:00 -05:00
const fs = require ( "fs" );
2026-03-08 18:02:14 -05:00
const http = require ( "http" );
2026-05-06 15:12:07 -05:00
const net = require ( "net" );
2026-08-19 14:12:06 -05:00
const tls = require ( "tls" );
const zlib = require ( "zlib" );
const crypto = require ( "crypto" );
2026-09-20 14:56:39 -05:00
const { URL , pathToFileURL } = require ( "url" );
2026-10-20 16:03:29 -05:00
const { fork } = require ( "child_process" );
2026-10-01 16:20:16 -05:00
const linkedServer = require ( "./linked-server.cjs" );
const desktopSettings = require ( "./desktop-settings.cjs" );
2026-10-12 11:35:48 -05:00
const autoUpdates = require ( "./auto-updater.cjs" );
2026-08-19 14:12:06 -05:00
const { isCloseActiveTabInput } = require ( "./keyboard-shortcuts.cjs" );
const { quitApp } = require ( "./app-quit.cjs" );
const { selectLinuxPasswordStore } = require ( "./linux-password-store.cjs" );
2026-10-20 16:03:29 -05:00
// Saved by the file manager after the user picks an editor in a file dialog.
const EXTERNAL_EDITOR_SETTING = "fileManager.externalEditorPath" ;
// The height of the title bar the app draws itself. The renderer uses the
// same number, so the bar and the window controls always line up.
const TITLE_BAR_HEIGHT = 40 ;
2026-10-18 00:34:50 -05:00
const { isExternalLoginUrl , isCallbackPort } = require ( "./external-login.cjs" );
2026-08-19 14:12:06 -05:00
2026-10-20 16:03:29 -05:00
// The main process's Node.js networking (the global `fetch`) only
2026-08-19 14:12:06 -05:00
// trusts Node's bundled Mozilla CA list by default, not the OS/system trust
// store. Chromium (the renderer, i.e. the web app and the login iframe) uses
// the OS trust store instead, so a certificate that's valid in-browser --
// e.g. one issued by a reverse proxy's internal/corporate CA, or a system
// CA installed via Keychain/certmgr -- can still fail main-process requests
// with UNABLE_TO_VERIFY_LEAF_SIGNATURE. Merge the system store in so remote
// sync and the connection health check see the same trust as the browser.
try {
if ( typeof tls . setDefaultCACertificates === "function" ) {
tls . setDefaultCACertificates ([
... tls . getCACertificates ( "default" ),
... tls . getCACertificates ( "system" ),
]);
}
} catch ( error ) {
console . error ( "Failed to merge system CA certificates:" , error );
}
2026-03-08 18:02:14 -05:00
2026-06-21 15:55:41 -05:00
// Portable mode: if a `.portable` marker exists next to the executable,
// store all data in a `data` folder beside the exe instead of %APPDATA%.
( function setupPortableDataDir () {
const exeDir = path . dirname ( process . execPath );
const markerPath = path . join ( exeDir , ".portable" );
const envOverride = process . env . TERMIX_DATA_DIR ;
let portableDataDir = null ;
if ( envOverride ) {
portableDataDir = envOverride ;
} else if ( app . isPackaged && fs . existsSync ( markerPath )) {
portableDataDir = path . join ( exeDir , "data" );
}
if ( portableDataDir ) {
try {
if ( ! fs . existsSync ( portableDataDir )) {
fs . mkdirSync ( portableDataDir , { recursive : true });
}
app . setPath ( "userData" , portableDataDir );
} catch {
// Fall back to default userData if we can't create the directory.
}
}
})();
2026-03-08 18:02:14 -05:00
const logFile = path . join ( app . getPath ( "userData" ), "termix-main.log" );
2026-05-06 15:12:07 -05:00
const electronAuthCookiesPath = path . join (
app . getPath ( "userData" ),
"electron-auth-cookies.json" ,
);
const electronAuthCookies = new Map ();
2026-03-08 18:02:14 -05:00
function logToFile (... args ) {
const timestamp = new Date (). toISOString ();
const msg = args
. map (( a ) => ( typeof a === "object" ? JSON . stringify ( a ) : String ( a )))
. join ( " " );
const line = `[ ${ timestamp } ] ${ msg } \n` ;
try {
fs . appendFileSync ( logFile , line );
} catch {
// ignore
}
console . log (... args );
}
2026-05-06 15:12:07 -05:00
function getCookieOrigin ( url ) {
try {
const parsedUrl = new URL ( url );
const protocol =
parsedUrl . protocol === "ws:"
? "http:"
: parsedUrl . protocol === "wss:"
? "https:"
: parsedUrl . protocol ;
return ` ${ protocol } // ${ parsedUrl . host } ` ;
} catch {
return null ;
}
}
function parseCookieTarget ( url ) {
try {
const parsedUrl = new URL ( url );
if ( parsedUrl . protocol === "ws:" ) {
parsedUrl . protocol = "http:" ;
} else if ( parsedUrl . protocol === "wss:" ) {
parsedUrl . protocol = "https:" ;
}
return parsedUrl ;
} catch {
return null ;
}
}
function getElectronAuthCookieKey ( name , origin ) {
return origin ? ` ${ origin } | ${ name } ` : null ;
}
function getSafeStorageAvailable () {
try {
return safeStorage . isEncryptionAvailable ();
} catch {
return false ;
}
}
function encodeElectronAuthCookieValue ( value ) {
return {
encrypted : true ,
value : safeStorage . encryptString ( value ). toString ( "base64" ),
};
}
function decodeElectronAuthCookieValue ( record ) {
if ( ! record . encrypted || ! getSafeStorageAvailable ()) {
return null ;
}
try {
return safeStorage . decryptString ( Buffer . from ( record . value , "base64" ));
} catch ( error ) {
logToFile (
"Failed to decrypt persisted Electron auth cookie:" ,
error . message ,
);
return null ;
}
}
function isElectronAuthCookieExpired ( cookie ) {
return Number . isFinite ( cookie . expiresAt ) && cookie . expiresAt <= Date . now ();
}
function saveElectronAuthCookiesToDisk () {
try {
if ( ! getSafeStorageAvailable ()) {
if ( fs . existsSync ( electronAuthCookiesPath )) {
fs . rmSync ( electronAuthCookiesPath , { force : true });
}
return ;
}
const records = [];
for ( const [ key , cookie ] of electronAuthCookies . entries ()) {
if ( isElectronAuthCookieExpired ( cookie )) {
electronAuthCookies . delete ( key );
continue ;
}
records . push ({
key ,
name : cookie . name ,
origin : cookie . origin ,
path : cookie . path ,
expiresAt : cookie . expiresAt ,
... encodeElectronAuthCookieValue ( cookie . value ),
});
}
fs . writeFileSync (
electronAuthCookiesPath ,
JSON . stringify ({ version : 1 , records }, null , 2 ),
);
} catch ( error ) {
logToFile ( "Failed to persist Electron auth cookies:" , error . message );
}
}
function loadElectronAuthCookiesFromDisk () {
electronAuthCookies . clear ();
try {
if ( ! getSafeStorageAvailable ()) {
if ( fs . existsSync ( electronAuthCookiesPath )) {
fs . rmSync ( electronAuthCookiesPath , { force : true });
}
return ;
}
if ( ! fs . existsSync ( electronAuthCookiesPath )) {
return ;
}
const data = JSON . parse ( fs . readFileSync ( electronAuthCookiesPath , "utf8" ));
const records = Array . isArray ( data . records ) ? data . records : [];
for ( const record of records ) {
if (
! record ||
typeof record . key !== "string" ||
typeof record . name !== "string" ||
typeof record . origin !== "string"
) {
continue ;
}
const value = decodeElectronAuthCookieValue ( record );
if ( ! value ) {
continue ;
}
const cookie = {
name : record . name ,
value ,
origin : record . origin ,
path : typeof record . path === "string" ? record . path : "/" ,
expiresAt : Number . isFinite ( record . expiresAt ) ? record . expiresAt : null ,
};
if ( ! isElectronAuthCookieExpired ( cookie )) {
electronAuthCookies . set ( record . key , cookie );
}
}
saveElectronAuthCookiesToDisk ();
} catch ( error ) {
logToFile ( "Failed to load persisted Electron auth cookies:" , error . message );
}
}
function clearPersistedElectronAuthCookies () {
electronAuthCookies . clear ();
try {
if ( fs . existsSync ( electronAuthCookiesPath )) {
fs . rmSync ( electronAuthCookiesPath , { force : true });
}
} catch ( error ) {
logToFile (
"Failed to clear persisted Electron auth cookies:" ,
error . message ,
);
}
}
function parseSetCookieHeader ( header ) {
const [ cookiePair , ... attributes ] = String ( header || "" ). split ( ";" );
const separatorIndex = cookiePair . indexOf ( "=" );
if ( separatorIndex <= 0 ) return null ;
const parsed = {
name : cookiePair . slice ( 0 , separatorIndex ). trim (),
value : cookiePair . slice ( separatorIndex + 1 ). trim (),
path : "/" ,
maxAge : null ,
expires : null ,
};
for ( const attribute of attributes ) {
const [ rawName , ... rawValueParts ] = attribute . trim (). split ( "=" );
const attrName = rawName . toLowerCase ();
const attrValue = rawValueParts . join ( "=" );
if ( attrName === "path" && attrValue ) {
parsed . path = attrValue ;
} else if ( attrName === "max-age" && attrValue ) {
const maxAge = Number ( attrValue );
parsed . maxAge = Number . isFinite ( maxAge ) ? maxAge : null ;
} else if ( attrName === "expires" && attrValue ) {
const expires = Date . parse ( attrValue );
parsed . expires = Number . isFinite ( expires ) ? expires : null ;
}
}
return parsed ;
}
function rememberElectronAuthCookieFromHeader ( url , header ) {
const origin = getCookieOrigin ( url );
if ( ! origin ) return ;
const cookie = parseSetCookieHeader ( header );
if ( ! cookie || cookie . name !== "jwt" ) return ;
const key = getElectronAuthCookieKey ( cookie . name , origin );
if ( ! key ) return ;
const expired =
cookie . maxAge === 0 ||
( cookie . expires !== null && cookie . expires <= Date . now ());
if ( expired || ! cookie . value ) {
electronAuthCookies . delete ( key );
saveElectronAuthCookiesToDisk ();
return ;
}
const expiresAt =
cookie . maxAge !== null ? Date . now () + cookie . maxAge * 1000 : cookie . expires ;
electronAuthCookies . set ( key , {
name : cookie . name ,
value : cookie . value ,
origin ,
path : cookie . path ,
expiresAt ,
});
saveElectronAuthCookiesToDisk ();
}
function getRememberedElectronAuthCookie ( name , targetUrl ) {
const target = parseCookieTarget ( targetUrl );
if ( ! target ) return null ;
const exactKey = getElectronAuthCookieKey ( name , target . origin );
const exactCookie = exactKey ? electronAuthCookies . get ( exactKey ) : null ;
if ( exactCookie && ! isElectronAuthCookieExpired ( exactCookie )) {
return exactCookie ;
}
if ( target . protocol !== "https:" ) {
return null ;
}
const httpOrigin = `http:// ${ target . host } ` ;
const httpKey = getElectronAuthCookieKey ( name , httpOrigin );
const httpCookie = httpKey ? electronAuthCookies . get ( httpKey ) : null ;
return httpCookie && ! isElectronAuthCookieExpired ( httpCookie )
? httpCookie
: null ;
}
function getHeaderName ( headers , name ) {
const lowerName = name . toLowerCase ();
return Object . keys ( headers || {}). find (
( key ) => key . toLowerCase () === lowerName ,
);
}
function setCookieHeaderValue ( requestHeaders , name , value ) {
const headerName = getHeaderName ( requestHeaders , "Cookie" ) || "Cookie" ;
const existing = requestHeaders [ headerName ];
const existingValue = Array . isArray ( existing )
? existing . join ( "; " )
: existing ;
const nextCookie = ` ${ name } = ${ value } ` ;
const otherCookies = String ( existingValue || "" )
. split ( ";" )
. map (( cookie ) => cookie . trim ())
. filter (( cookie ) => cookie && ! cookie . startsWith ( ` ${ name } =` ));
requestHeaders [ headerName ] =
otherCookies . length > 0
? ` ${ otherCookies . join ( "; " ) } ; ${ nextCookie } `
: nextCookie ;
}
function parseSemver ( version ) {
const match = String ( version || "" ). match ( /(\d+)\.(\d+)(?:\.(\d+))?/ );
if ( ! match ) return null ;
return [ Number ( match [ 1 ]), Number ( match [ 2 ]), Number ( match [ 3 ] || 0 )];
}
function compareSemver ( a , b ) {
const parsedA = parseSemver ( a );
const parsedB = parseSemver ( b );
if ( ! parsedA || ! parsedB ) return null ;
for ( let i = 0 ; i < 3 ; i += 1 ) {
if ( parsedA [ i ] > parsedB [ i ]) return 1 ;
if ( parsedA [ i ] < parsedB [ i ]) return - 1 ;
}
return 0 ;
}
const INSECURE_MODE_VALUES = new Set ([ "true" , "1" , "yes" ]);
function isInsecureModeEnabled () {
return INSECURE_MODE_VALUES . has (
String ( process . env . ENABLE_INSECURE_MODE || "" )
. trim ()
. toLowerCase (),
);
}
2026-06-04 15:16:53 -04:00
function isPrivateNetworkHost ( hostname ) {
if (
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "::1"
) {
return true ;
}
const ipv4Match = hostname . match (
/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ ,
);
if ( ipv4Match ) {
const [, a , b ] = ipv4Match . map ( Number );
return (
a === 10 ||
( a === 172 && b >= 16 && b <= 31 ) ||
( a === 192 && b === 168 ) ||
( a === 169 && b === 254 )
);
}
if ( hostname . startsWith ( "fc" ) || hostname . startsWith ( "fd" )) {
return true ;
}
return false ;
}
function isInvalidCertificateAllowedForUrl ( url ) {
if ( isInsecureModeEnabled ()) return true ;
try {
const { hostname } = new URL ( url );
if ( isPrivateNetworkHost ( hostname )) return true ;
} catch {
// fall through
}
2026-10-01 16:20:16 -05:00
// Otherwise only the server this desktop is linked to, when its link
// allows a self-signed certificate.
const linked = linkedServer . getLinkedServer ();
if ( ! linked ? . allowInvalidCertificate ) return false ;
2026-10-18 00:34:50 -05:00
return linkedServer . isLinkedOrigin ( url );
2026-06-04 15:16:53 -04:00
}
function getTlsVerificationOptions ( url ) {
2026-05-06 15:12:07 -05:00
return {
2026-06-04 15:16:53 -04:00
rejectUnauthorized : ! isInvalidCertificateAllowedForUrl ( url ),
2026-10-20 16:03:29 -05:00
... linkedServer . clientCertOptionsFor ( url ),
2026-05-06 15:12:07 -05:00
};
}
2025-11-05 10:36:16 -06:00
if ( process . platform === "linux" ) {
2025-12-31 22:20:12 -06:00
app . commandLine . appendSwitch ( "--ozone-platform-hint=auto" );
2025-11-05 10:36:16 -06:00
2025-12-31 22:20:12 -06:00
app . commandLine . appendSwitch ( "--enable-features=VaapiVideoDecoder" );
2026-06-21 15:55:41 -05:00
// Fix click-coordinate misalignment with fractional display scaling on KDE/Wayland.
// Chromium's hit-testing uses unscaled coords while the compositor scales visually,
// so forcing scale factor 1 keeps them in sync. See: https://github.com/brave/brave-browser/issues/50028
app . commandLine . appendSwitch ( "--force-device-scale-factor" , "1" );
2026-08-19 14:12:06 -05:00
const passwordStore = selectLinuxPasswordStore ( app . commandLine , process . env );
if ( passwordStore ) {
logToFile (
`[safeStorage] Selected the ${ passwordStore } password store for this desktop.` ,
);
}
2025-11-05 10:36:16 -06:00
}
2026-09-20 14:56:39 -05:00
if ( process . platform === "win32" || process . env . ELECTRON_DISABLE_GPU === "1" ) {
2026-06-16 15:59:53 -05:00
app . disableHardwareAcceleration ();
}
2026-05-06 15:12:07 -05:00
if ( isInsecureModeEnabled ()) {
logToFile (
"[security] ENABLE_INSECURE_MODE is enabled; TLS certificate validation is disabled." ,
);
app . commandLine . appendSwitch ( "--ignore-certificate-errors" );
app . commandLine . appendSwitch ( "--ignore-ssl-errors" );
app . commandLine . appendSwitch ( "--ignore-certificate-errors-spki-list" );
}
2025-10-01 15:40:10 -05:00
app . commandLine . appendSwitch ( "--enable-features=NetworkService" );
2025-09-12 14:42:00 -05:00
let mainWindow = null ;
2026-10-01 16:20:16 -05:00
const { createIsolatedWindows } = require ( "./isolated-window.cjs" );
2026-10-20 16:03:29 -05:00
const { createEmbeddedViews } = require ( "./embedded-view.cjs" );
2026-10-01 16:20:16 -05:00
const isolatedWindows = createIsolatedWindows ({
2026-09-20 14:56:39 -05:00
BrowserWindow ,
session ,
getMainWindow : () => mainWindow ,
});
2026-10-01 16:20:16 -05:00
2026-10-20 16:03:29 -05:00
const embeddedViews = createEmbeddedViews ({
WebContentsView ,
session ,
getMainWindow : () => mainWindow ,
});
// Only the app's own page may place native views, never a frame inside it.
function fromMainFrame ( event ) {
return !! mainWindow && event . senderFrame === mainWindow . webContents . mainFrame ;
}
ipcMain . handle ( "embedded-view-open" , ( event , options ) => {
if ( ! fromMainFrame ( event )) throw new Error ( "Not allowed" );
return embeddedViews . open ( options );
});
ipcMain . handle ( "embedded-view-bounds" , ( event , id , bounds ) => {
if ( fromMainFrame ( event )) embeddedViews . setBounds ( String ( id ), bounds );
});
ipcMain . handle ( "embedded-view-visible" , ( event , id , visible ) => {
if ( fromMainFrame ( event )) embeddedViews . setVisible ( String ( id ), visible );
});
ipcMain . handle ( "embedded-view-close" , ( event , id ) => {
if ( fromMainFrame ( event )) embeddedViews . destroy ( String ( id ));
});
2026-10-01 16:20:16 -05:00
// Requests from the embedded backend over the fork IPC channel (the other
// direction from the "shutdown" message main already sends it). A plugin's
// ctx.desktop.openIsolatedWindow is the one caller today.
const BACKEND_REQUEST_HANDLERS = {
"open-isolated-window" : ( payload ) => isolatedWindows . open ( payload ),
2026-10-20 16:03:29 -05:00
// Plugins open OS clients themselves now (desktop:local), so none is wired here.
"launch-external-client" : ( payload ) => ({
success : false ,
error : `No external client for ${ payload ? . protocol ?? "this protocol" } ` ,
}),
// Generic shell actions for a plugin's desktop:local file work.
"shell-trash-item" : async ( payload ) => {
await shell . trashItem ( String ( payload ? . path ?? "" ));
return { success : true };
},
"shell-show-item" : ( payload ) => {
shell . showItemInFolder ( String ( payload ? . path ?? "" ));
return { success : true };
},
"shell-open-path" : async ( payload ) => {
const error = await shell . openPath ( String ( payload ? . path ?? "" ));
return error ? { success : false , error } : { success : true };
},
2026-10-01 16:20:16 -05:00
"sync-proxy-config" : ( payload ) => {
linkedServer . setLinkedServer ( payload );
return { success : true };
},
};
2026-10-10 14:58:27 -05:00
function isDevRunner () {
return (
process . env . TERMIX_DEV_RUNNER === "true" &&
typeof process . send === "function"
);
}
2026-10-01 16:20:16 -05:00
async function handleBackendRequest ( msg ) {
if ( ! msg || msg . type !== "backend-request" ) return ;
const { id , channel , payload } = msg ;
const handler = BACKEND_REQUEST_HANDLERS [ channel ];
2026-10-10 14:58:27 -05:00
// Under npm run dev:electron the runner owns the backend and relays these messages.
const target =
backendProcess && ! backendProcess . killed
? backendProcess
: isDevRunner ()
? process
: null ;
2026-10-01 16:20:16 -05:00
const reply = ( response ) => {
2026-10-10 14:58:27 -05:00
if ( ! target ) return ;
try {
target . send ({ type : "backend-response" , id , ... response });
} catch ( error ) {
logToFile ( "Failed to reply to backend request:" , error . message );
2026-10-01 16:20:16 -05:00
}
};
if ( ! handler ) {
reply ({ ok : false , error : `Unknown backend request channel: ${ channel } ` });
return ;
}
try {
const result = await handler ( payload );
reply ({ ok : true , result });
} catch ( error ) {
reply ({ ok : false , error : error . message || String ( error ) });
}
}
2026-03-08 18:02:14 -05:00
let backendProcess = null ;
2026-05-28 22:29:20 -05:00
let backendStartFailed = false ;
2026-09-20 14:56:39 -05:00
// Why the embedded backend died, once it has. Null while it is healthy
// or still starting. The renderer polls this to tell "still booting"
// (keep waiting) apart from "never coming back" (show the reason).
let backendFailure = null ;
// Set while we are deliberately tearing the backend down on quit, so a
// SIGKILL we sent ourselves is not reported to the user as a crash.
let backendStopRequested = false ;
// Tail of the embedded backend's stderr, kept only so a failed start can be
// classified after the fact.
const BACKEND_STDERR_TAIL_LIMIT = 8192 ;
let backendStderrTail = "" ;
2026-03-08 18:02:14 -05:00
let tray = null ;
let isQuitting = false ;
2026-06-29 13:28:26 -05:00
const tempFiles = new Map ();
2026-10-18 00:34:50 -05:00
// Paths the user picked in an open dialog this session. Only these may be
// saved as the external editor, so the renderer cannot name an executable.
const dialogChosenPaths = new Set ();
2025-09-12 14:42:00 -05:00
const isDev = process . env . NODE_ENV === "development" || ! app . isPackaged ;
2025-11-05 10:36:16 -06:00
const appRoot = isDev ? process . cwd () : path . join ( __dirname , ".." );
2026-06-29 13:28:26 -05:00
const windowsAppUserModelId = "com.karmaa.termix" ;
2026-05-06 15:12:07 -05:00
const electronCacheBuildPath = path . join (
app . getPath ( "userData" ),
"client-cache-build.json" ,
);
const termixSessionPartition = "persist:termix" ;
2026-06-29 13:28:26 -05:00
function getTempRoot () {
const tempRoot = path . join ( app . getPath ( "temp" ), "termix" );
fs . mkdirSync ( tempRoot , { recursive : true });
return tempRoot ;
}
function sanitizeFileName ( fileName ) {
const baseName = path . basename ( String ( fileName || "file" ));
return baseName . replace ( /[<>:"/\\|?*\x00-\x1f]/g , "_" ) || "file" ;
}
function decodeFileContent ( content , encoding ) {
if ( encoding === "base64" ) {
return Buffer . from ( String ( content || "" ), "base64" );
}
return Buffer . from ( String ( content || "" ), "utf8" );
}
function createManagedTempFile ( fileName , content , encoding = "utf8" ) {
const tempId = ` ${ Date . now () } - ${ Math . random (). toString ( 36 ). slice ( 2 ) } ` ;
const tempDir = fs . mkdtempSync ( path . join ( getTempRoot (), ` ${ tempId } -` ));
const filePath = path . join ( tempDir , sanitizeFileName ( fileName ));
fs . writeFileSync ( filePath , decodeFileContent ( content , encoding ));
tempFiles . set ( tempId , { path : filePath , dir : tempDir });
return { tempId , path : filePath };
}
function cleanupManagedTempFile ( tempId ) {
const temp = tempFiles . get ( tempId );
if ( ! temp ) return ;
try {
fs . rmSync ( temp . dir , { recursive : true , force : true });
} catch ( error ) {
logToFile ( "Failed to clean up temporary file:" , error . message );
}
tempFiles . delete ( tempId );
}
2026-09-20 14:56:39 -05:00
// Origins the renderer has explicitly marked as allowed to present an invalid
// certificate, for web endpoints whose ignoreCert is set. Session scoped and
// never persisted.
//
// Deliberately NOT wired into isInvalidCertificateAllowedForUrl: that function
// also governs this process's own outbound TLS via getTlsVerificationOptions,
// and widening it would grant more privilege than the feature needs.
//
// Entries expire rather than living until the app restarts (days, on a
// desktop app). Main has no host-database access, so it cannot verify a
// renderer's claim that an origin really is a configured web endpoint -- an
// unbounded allowance would make that an indefinite window rather than a
// momentary one. The renderer re-registers before each load, which refreshes
// the expiry, so a live endpoint keeps working while a stale entry lapses.
const WEB_ENDPOINT_CERTIFICATE_ALLOWLIST_TTL_MS = 5 * 60 * 1000 ;
const webEndpointCertificateAllowlist = new Map ();
function isWebEndpointCertificateAllowed ( url ) {
try {
const parsed = new URL ( url );
// The allowance exists solely to suppress a TLS certificate error, so a
// non-https origin in the map is meaningless at best.
if ( parsed . protocol !== "https:" ) return false ;
const expiresAt = webEndpointCertificateAllowlist . get ( parsed . origin );
if ( expiresAt === undefined ) return false ;
if ( Date . now () >= expiresAt ) {
webEndpointCertificateAllowlist . delete ( parsed . origin );
return false ;
}
return true ;
} catch {
return false ;
}
}
ipcMain . handle ( "allow-invalid-certificate-for-origin" , ( _event , origin ) => {
try {
// Store the PARSED origin, never the caller's string, so a path or a
// wildcard cannot widen the allowance.
const parsed = new URL ( String ( origin ));
if ( parsed . protocol !== "https:" ) {
return { success : false };
}
webEndpointCertificateAllowlist . set (
parsed . origin ,
Date . now () + WEB_ENDPOINT_CERTIFICATE_ALLOWLIST_TTL_MS ,
);
return { success : true };
} catch {
return { success : false };
}
});
2026-10-20 16:03:29 -05:00
// Mutual TLS for the linked server: Chromium asks which certificate from the
// OS store to present. Other origins keep Chromium's default (none).
const chosenClientCerts = new Map ();
app . on (
"select-client-certificate" ,
async ( event , _webContents , url , list , callback ) => {
if ( ! linkedServer . isLinkedOrigin ( url ) || ! list . length ) return ;
event . preventDefault ();
const origin = new URL ( url ). origin ;
const remembered = list . find (
( cert ) => cert . fingerprint === chosenClientCerts . get ( origin ),
);
let chosen = remembered || ( list . length === 1 ? list [ 0 ] : null );
if ( ! chosen ) {
const { response } = await dialog . showMessageBox ( mainWindow , {
type : "question" ,
title : "Client certificate" ,
message : "Choose the certificate for this server" ,
detail : origin ,
buttons : [
... list . slice ( 0 , 8 ). map (( cert ) => cert . subjectName ),
"Cancel" ,
],
cancelId : Math . min ( list . length , 8 ),
});
chosen = list [ response ] || null ;
}
if ( chosen ) chosenClientCerts . set ( origin , chosen . fingerprint );
callback ( chosen || undefined );
},
);
2026-06-04 15:16:53 -04:00
app . on (
"certificate-error" ,
( event , _webContents , url , error , certificate , callback ) => {
2026-09-20 14:56:39 -05:00
if (
2026-10-01 16:20:16 -05:00
isolatedWindows . handleCertificateError ( event , _webContents , url , callback )
2026-09-20 14:56:39 -05:00
)
return ;
2026-10-20 16:03:29 -05:00
if (
embeddedViews . handleCertificateError ( event , _webContents , url , callback )
)
return ;
2026-09-20 14:56:39 -05:00
if ( isWebEndpointCertificateAllowed ( url )) {
event . preventDefault ();
logToFile ( "Allowed invalid certificate for configured web endpoint" , {
url ,
error ,
issuer : certificate ? . issuerName ,
subject : certificate ? . subjectName ,
});
callback ( true );
return ;
}
2026-06-04 15:16:53 -04:00
if ( isInvalidCertificateAllowedForUrl ( url )) {
event . preventDefault ();
logToFile ( "Allowed invalid certificate for configured server" , {
url ,
error ,
issuer : certificate ? . issuerName ,
subject : certificate ? . subjectName ,
});
callback ( true );
return ;
}
callback ( false );
},
);
2026-10-01 16:20:16 -05:00
// A reverse proxy in front of the linked server may ask for basic auth; the
// link's saved username and password answer it. Anything else gets the
// default behaviour.
app . on ( "login" , ( event , _webContents , details , authInfo , callback ) => {
const credentials = linkedServer . answerLogin ( authInfo , details ? . url );
if ( ! credentials ) return ;
event . preventDefault ();
callback ( credentials . username , credentials . password || "" );
});
2026-05-06 15:12:07 -05:00
function getElectronBuildTimestamp () {
try {
const buildInfo = require ( "./build-info.cjs" );
if ( Number . isInteger ( buildInfo . buildTimestamp )) {
return buildInfo . buildTimestamp ;
}
} catch {
// Development runs may not have generated build metadata yet.
}
return 0 ;
}
async function clearElectronClientCacheIfBuildChanged () {
const buildTimestamp = getElectronBuildTimestamp ();
let cacheTimestamp = 0 ;
try {
if ( fs . existsSync ( electronCacheBuildPath )) {
const data = JSON . parse ( fs . readFileSync ( electronCacheBuildPath , "utf8" ));
cacheTimestamp = Number . isInteger ( data . buildTimestamp )
? data . buildTimestamp
: 0 ;
}
} catch ( error ) {
logToFile (
"Failed to read Electron client cache build info:" ,
error . message ,
);
}
if ( cacheTimestamp === buildTimestamp ) {
return ;
}
const clearStep = async ( label , action ) => {
try {
await action ();
} catch ( error ) {
logToFile ( `Failed to clear Electron ${ label } :` , error . message );
}
};
try {
const defaultSession = session . defaultSession ;
await clearStep ( "HTTP cache" , () => defaultSession . clearCache ());
await clearStep ( "code cache" , () =>
defaultSession . clearCodeCaches ({ urls : [] }),
);
await clearStep ( "auth cache" , () => defaultSession . clearAuthCache ());
await clearStep ( "storage data" , () =>
defaultSession . clearStorageData ({
storages : [
"appcache" ,
"cookies" ,
"filesystem" ,
"shadercache" ,
"websql" ,
"serviceworkers" ,
"cachestorage" ,
],
}),
);
fs . writeFileSync (
electronCacheBuildPath ,
JSON . stringify (
{
buildTimestamp ,
appVersion : app . getVersion (),
updatedAt : new Date (). toISOString (),
},
null ,
2 ,
),
);
logToFile ( "Electron client cache cleared for build change" , {
from : cacheTimestamp ,
to : buildTimestamp ,
appVersion : app . getVersion (),
});
} catch ( error ) {
logToFile ( "Failed to clear Electron client cache:" , error . message );
}
}
function getCookieRemovalUrl ( cookie ) {
const scheme = cookie . secure ? "https" : "http" ;
const domain = cookie . domain ? . startsWith ( "." )
? cookie . domain . slice ( 1 )
: cookie . domain || "localhost" ;
return ` ${ scheme } :// ${ domain }${ cookie . path || "/" } ` ;
}
async function clearElectronJwtCookiesAtStartup () {
loadElectronAuthCookiesFromDisk ();
const targetSessions = new Set ([
session . defaultSession ,
session . fromPartition ( termixSessionPartition ),
]);
for ( const targetSession of targetSessions ) {
try {
const cookies = await targetSession . cookies . get ({ name : "jwt" });
await Promise . all (
cookies . map (( cookie ) =>
targetSession . cookies . remove (
getCookieRemovalUrl ( cookie ),
cookie . name ,
),
),
);
if ( cookies . length > 0 ) {
logToFile ( "Cleared Electron JWT cookies from cookie store" , {
count : cookies . length ,
});
}
} catch ( error ) {
logToFile ( "Failed to clear Electron JWT cookies:" , error . message );
}
}
}
2025-09-12 14:42:00 -05:00
2026-05-12 23:50:25 -05:00
function getBackendPaths () {
2026-03-08 18:02:14 -05:00
if ( isDev ) {
2026-05-12 23:50:25 -05:00
const backendDir = path . join ( appRoot , "dist" , "backend" , "backend" );
2026-05-28 22:29:20 -05:00
return {
entryPath : path . join ( backendDir , "starter.js" ),
backendCwd : backendDir ,
};
2026-03-08 18:02:14 -05:00
}
2026-10-10 14:58:27 -05:00
// fork() does not go through Electron's asar redirector, use the unpacked path.
2026-06-29 13:28:26 -05:00
// On macOS multi-arch builds (mergeASARs: false), electron-builder names the ASAR
// app-arm64.asar / app-x64.asar instead of app.asar, so match all variants.
2026-08-06 14:41:39 -05:00
const unpackedRoot = getUnpackedAppRoot ( appRoot );
2026-05-12 23:50:25 -05:00
const backendDir = path . join ( unpackedRoot , "dist" , "backend" , "backend" );
2026-05-28 22:29:20 -05:00
return {
entryPath : path . join ( backendDir , "starter.js" ),
backendCwd : backendDir ,
};
2026-03-08 18:02:14 -05:00
}
function getBackendDataDir () {
const userDataPath = app . getPath ( "userData" );
const dataDir = path . join ( userDataPath , "server-data" );
if ( ! fs . existsSync ( dataDir )) {
fs . mkdirSync ( dataDir , { recursive : true });
}
return dataDir ;
}
2026-07-26 18:47:27 -05:00
function getBackendPidFilePath () {
return path . join ( app . getPath ( "userData" ), "backend.pid" );
}
// If the app was previously killed abnormally (crash, force-quit, Task
// Manager) rather than through the normal quit flow, will-quit never fires
// and stopBackendServer() never runs -- the forked backend child is a
// genuinely separate OS process on Windows/mac/Linux, so it keeps running
2026-10-01 16:20:16 -05:00
// and holding the port the backend binds (30001). Every subsequent
// launch's own backend then fails outright
2026-07-26 18:47:27 -05:00
// with EADDRINUSE and the app is stuck until something manually kills the
// orphan. Reap any such leftover process, identified by PID file, before
// spawning a new one.
function reapOrphanedBackendProcess () {
const pidFilePath = getBackendPidFilePath ();
let recordedPid ;
try {
recordedPid = parseInt ( fs . readFileSync ( pidFilePath , "utf8" ). trim (), 10 );
} catch {
return ;
}
if ( ! Number . isInteger ( recordedPid ) || recordedPid <= 0 ) return ;
try {
// Signal 0 does not kill the process -- it only checks whether a
// process with this PID exists and is signalable, throwing ESRCH if
// not. This avoids killing an unrelated process that happens to have
// reused the same PID since the last run.
process . kill ( recordedPid , 0 );
} catch {
// No live process at that PID; nothing to reap.
try {
fs . unlinkSync ( pidFilePath );
} catch {
// already absent
}
return ;
}
logToFile (
`Found orphaned backend process from a previous session (pid ${ recordedPid } ), terminating it before starting a new one` ,
);
try {
process . kill ( recordedPid , "SIGKILL" );
} catch {
// already gone
}
try {
fs . unlinkSync ( pidFilePath );
} catch {
// already absent
}
}
2026-10-18 00:34:50 -05:00
const DEFAULT_BACKEND_PORT = 30001 ;
let backendPort = DEFAULT_BACKEND_PORT ;
function canListenOn ( port ) {
return new Promise (( resolve ) => {
const probe = net . createServer ();
probe . once ( "error" , () => resolve ( null ));
probe . listen ( port , "127.0.0.1" , () => {
const { port : bound } = probe . address ();
probe . close (() => resolve ( bound ));
});
});
}
// 30001 when it is free, otherwise any free port. Only the desktop app talks
// to this backend, so the number itself does not matter.
async function findBackendPort () {
const preferred = await canListenOn ( DEFAULT_BACKEND_PORT );
if ( preferred ) return preferred ;
const fallback = await canListenOn ( 0 );
if ( fallback ) {
logToFile (
`Port ${ DEFAULT_BACKEND_PORT } is in use, starting the backend on ${ fallback } ` ,
);
return fallback ;
}
return DEFAULT_BACKEND_PORT ;
}
2026-03-08 18:02:14 -05:00
function startBackendServer () {
2026-07-26 18:47:27 -05:00
reapOrphanedBackendProcess ();
2026-09-20 14:56:39 -05:00
backendFailure = null ;
backendStopRequested = false ;
backendStderrTail = "" ;
2026-03-08 18:02:14 -05:00
return new Promise (( resolve ) => {
2026-05-12 23:50:25 -05:00
const { entryPath , backendCwd } = getBackendPaths ();
2026-03-08 18:02:14 -05:00
logToFile ( "isDev:" , isDev , "appRoot:" , appRoot );
logToFile ( "app.isPackaged:" , app . isPackaged );
logToFile ( "process.env.NODE_ENV:" , process . env . NODE_ENV );
if ( ! fs . existsSync ( entryPath )) {
logToFile ( "Backend entry not found:" , entryPath );
2026-09-20 14:56:39 -05:00
backendFailure = { reason : "crashed" , port : null };
2026-03-08 18:02:14 -05:00
resolve ( false );
return ;
}
const dataDir = getBackendDataDir ();
logToFile ( "Starting embedded backend server..." );
logToFile ( "Backend entry:" , entryPath );
logToFile ( "Data directory:" , dataDir );
2026-05-12 23:50:25 -05:00
logToFile ( "Backend cwd:" , backendCwd );
2026-03-08 18:02:14 -05:00
logToFile ( "Checking paths..." );
logToFile ( " entryPath exists:" , fs . existsSync ( entryPath ));
logToFile ( " dataDir exists:" , fs . existsSync ( dataDir ));
2026-05-12 23:50:25 -05:00
logToFile ( " backendCwd exists:" , fs . existsSync ( backendCwd ));
2026-03-08 18:02:14 -05:00
backendProcess = fork ( entryPath , [], {
2026-05-28 22:29:20 -05:00
cwd :
fs . existsSync ( backendCwd ) && fs . statSync ( backendCwd ). isDirectory ()
? backendCwd
: dataDir ,
2026-03-08 18:02:14 -05:00
env : {
... process . env ,
DATA_DIR : dataDir ,
NODE_ENV : "production" ,
ELECTRON_EMBEDDED : "true" ,
2026-10-18 00:34:50 -05:00
PORT : String ( backendPort ),
EMBEDDED_BACKEND_PORT : String ( backendPort ),
2026-10-20 16:03:29 -05:00
ELECTRON_USER_DATA : app . getPath ( "userData" ),
2026-07-26 18:47:27 -05:00
VERSION : app . getVersion (),
2026-03-08 18:02:14 -05:00
},
stdio : [ "pipe" , "pipe" , "pipe" , "ipc" ],
});
logToFile ( "Backend process spawned, pid:" , backendProcess . pid );
2026-07-26 18:47:27 -05:00
try {
fs . writeFileSync ( getBackendPidFilePath (), String ( backendProcess . pid ));
} catch {
// Non-fatal: only means a future crash won't self-heal via reap.
}
2026-03-08 18:02:14 -05:00
let resolved = false ;
const readyTimeout = setTimeout (() => {
if ( ! resolved ) {
resolved = true ;
logToFile ( "Backend ready timeout (15s), proceeding anyway..." );
resolve ( true );
}
}, 15000 );
backendProcess . stdout . on ( "data" , ( data ) => {
const msg = data . toString (). trim ();
logToFile ( "[backend]" , msg );
if ( ! resolved && msg . includes ( "started successfully" )) {
resolved = true ;
clearTimeout ( readyTimeout );
logToFile ( "Backend ready signal received" );
resolve ( true );
}
});
2026-09-20 14:56:39 -05:00
// The reason for a failed start is only ever visible in what the child
// wrote to stderr before dying, so keep a bounded tail of it. Bounded
// because a backend that stays up for days can otherwise log without
// limit into a buffer nothing ever drains.
2026-03-08 18:02:14 -05:00
backendProcess . stderr . on ( "data" , ( data ) => {
2026-09-20 14:56:39 -05:00
const chunk = data . toString ();
backendStderrTail = ( backendStderrTail + chunk ). slice (
- BACKEND_STDERR_TAIL_LIMIT ,
);
logToFile ( "[backend:stderr]" , chunk . trim ());
2026-03-08 18:02:14 -05:00
});
2026-10-01 16:20:16 -05:00
backendProcess . on ( "message" , ( msg ) => {
if ( ! resolved && msg ? . type === "backend-ready" ) {
resolved = true ;
clearTimeout ( readyTimeout );
logToFile ( "Backend ready signal received via IPC" );
resolve ( true );
}
void handleBackendRequest ( msg );
});
2026-03-08 18:02:14 -05:00
backendProcess . on ( "exit" , ( code , signal ) => {
logToFile ( `Backend process exited with code ${ code } , signal ${ signal } ` );
2026-05-28 22:29:20 -05:00
if ( ! resolved && code !== 0 ) {
backendStartFailed = true ;
}
2026-09-20 14:56:39 -05:00
// Classified whether or not the ready promise already settled: the
// 15s ready timeout resolves optimistically, so a backend that dies
// at second 20 still has to be reported rather than silently dropped.
// backendStopRequested is the only evidence that an exit was asked
// for, so it is the sole guard here -- past it, every exit means the
// backend is gone and the renderer must stop waiting for it.
if ( ! backendStopRequested ) {
backendFailure = classifyBackendFailure ( backendStderrTail );
logToFile ( "Backend failure classified as:" , backendFailure . reason );
}
2026-03-08 18:02:14 -05:00
backendProcess = null ;
2026-07-26 18:47:27 -05:00
clearBackendPidFile ();
2026-03-08 18:02:14 -05:00
if ( ! resolved ) {
resolved = true ;
clearTimeout ( readyTimeout );
resolve ( false );
}
});
backendProcess . on ( "error" , ( err ) => {
logToFile ( "Failed to start backend process:" , err . message );
2026-09-20 14:56:39 -05:00
backendFailure = { reason : "crashed" , port : null };
2026-03-08 18:02:14 -05:00
backendProcess = null ;
if ( ! resolved ) {
resolved = true ;
clearTimeout ( readyTimeout );
resolve ( false );
}
});
});
}
2026-07-26 18:47:27 -05:00
function clearBackendPidFile () {
try {
fs . unlinkSync ( getBackendPidFilePath ());
} catch {
// already absent
}
}
2026-03-08 18:02:14 -05:00
function stopBackendServer () {
if ( ! backendProcess ) return ;
console . log ( "Stopping embedded backend server..." );
2026-09-20 14:56:39 -05:00
backendStopRequested = true ;
2026-03-08 18:02:14 -05:00
try {
backendProcess . send ({ type : "shutdown" });
} catch {
// IPC channel may already be closed
}
const forceKillTimeout = setTimeout (() => {
if ( backendProcess ) {
console . log ( "Force killing backend process..." );
backendProcess . kill ( "SIGKILL" );
backendProcess = null ;
}
}, 5000 );
backendProcess . on ( "exit" , () => {
clearTimeout ( forceKillTimeout );
backendProcess = null ;
2026-07-26 18:47:27 -05:00
clearBackendPidFile ();
2026-03-08 18:02:14 -05:00
});
}
2026-10-01 16:20:16 -05:00
// App Sandbox denies the singleton socket bind, which reads as a held lock and
// quits the app on every launch. macOS reactivates bundled apps anyway.
const gotTheLock = process . mas || app . requestSingleInstanceLock ();
2025-09-12 14:42:00 -05:00
if ( ! gotTheLock ) {
console . log ( "Another instance is already running, quitting..." );
app . quit ();
process . exit ( 0 );
} else {
app . on ( "second-instance" , ( event , commandLine , workingDirectory ) => {
if ( mainWindow ) {
if ( mainWindow . isMinimized ()) mainWindow . restore ();
mainWindow . focus ();
mainWindow . show ();
}
});
}
2026-03-08 18:02:14 -05:00
function createTray () {
try {
const { nativeImage } = require ( "electron" );
2026-10-10 14:58:27 -05:00
// Native APIs (Tray, nativeImage) can't load files from inside app.asar,
2026-05-12 23:54:51 -05:00
// use the unpacked path so the OS sees a real file.
const publicRoot = isDev
? path . join ( appRoot , "public" )
2026-06-29 13:28:26 -05:00
: path . join (
appRoot . replace (
/app(-[a-z0-9]+)?\.asar(?!\.unpacked)/ ,
"app.asar.unpacked" ,
),
"public" ,
);
2026-05-12 23:54:51 -05:00
2026-03-08 18:02:14 -05:00
let trayIcon ;
if ( process . platform === "darwin" ) {
2026-05-12 23:54:51 -05:00
const iconPath = path . join ( publicRoot , "icons" , "16x16.png" );
2026-10-29 17:59:55 -05:00
// The logo is full color on a solid tile. As a template image macOS
// keeps only its outline, which showed as an empty square.
2026-03-08 18:02:14 -05:00
trayIcon = nativeImage . createFromPath ( iconPath );
} else if ( process . platform === "win32" ) {
2026-05-12 23:54:51 -05:00
trayIcon = path . join ( publicRoot , "icon.ico" );
2026-03-08 18:02:14 -05:00
} else {
2026-10-01 16:20:16 -05:00
trayIcon = loadLinuxTrayIcon ( nativeImage , publicRoot );
if ( ! trayIcon ) {
logToFile ( "Tray icon not found, skipping the system tray" );
return ;
}
if ( /gnome/i . test ( process . env . XDG_CURRENT_DESKTOP || "" )) {
logToFile (
"GNOME shows tray icons only with the AppIndicator extension installed" ,
);
}
2026-03-08 18:02:14 -05:00
}
tray = new Tray ( trayIcon );
tray . setToolTip ( "Termix" );
const contextMenu = Menu . buildFromTemplate ([
{
label : "Show Window" ,
click : () => {
if ( mainWindow ) {
mainWindow . show ();
mainWindow . focus ();
}
},
},
{
label : "Quit" ,
click : () => {
isQuitting = true ;
2026-08-19 14:12:06 -05:00
quitApp ( app , mainWindow );
2026-03-08 18:02:14 -05:00
},
},
]);
tray . setContextMenu ( contextMenu );
tray . on ( "click" , () => {
if ( mainWindow ) {
if ( mainWindow . isVisible ()) {
mainWindow . hide ();
} else {
mainWindow . show ();
mainWindow . focus ();
}
}
});
2026-10-01 16:20:16 -05:00
logToFile ( "System tray created" );
2026-03-08 18:02:14 -05:00
} catch ( err ) {
2026-10-01 16:20:16 -05:00
tray = null ;
logToFile ( "Failed to create system tray:" , err ? . message || String ( err ));
2026-03-08 18:02:14 -05:00
}
}
2026-10-01 16:20:16 -05:00
function loadLinuxTrayIcon ( nativeImage , publicRoot ) {
const roots = [
publicRoot ,
path . join ( process . resourcesPath || "" , "app.asar.unpacked" , "public" ),
];
for ( const root of roots ) {
for ( const size of [ "32x32" , "24x24" , "48x48" ]) {
const image = nativeImage . createFromPath (
path . join ( root , "icons" , ` ${ size } .png` ),
);
if ( ! image . isEmpty ()) return image ;
}
}
return null ;
}
2025-09-12 14:42:00 -05:00
function createWindow () {
2025-11-05 10:36:16 -06:00
const appVersion = app . getVersion ();
const electronVersion = process . versions . electron ;
const platform =
process . platform === "win32"
? "Windows"
: process . platform === "darwin"
? "macOS"
: "Linux" ;
2026-10-20 16:03:29 -05:00
const isMac = process . platform === "darwin" ;
2025-09-12 14:42:00 -05:00
mainWindow = new BrowserWindow ({
width : 1200 ,
height : 800 ,
minWidth : 800 ,
minHeight : 600 ,
title : "Termix" ,
2026-10-20 16:03:29 -05:00
// The app draws its own title bar so it follows the active theme. macOS
// keeps the traffic lights on top of it, Windows and Linux get the
// native controls as an overlay whose colors the renderer keeps in sync.
titleBarStyle : isMac ? "hiddenInset" : "hidden" ,
...( isMac
? { trafficLightPosition : { x : 14 , y : 16 } }
: {
titleBarOverlay : {
color : "#0f0f14" ,
symbolColor : "#e6e6ea" ,
height : TITLE_BAR_HEIGHT ,
},
}),
2026-06-29 13:28:26 -05:00
icon : path . join (
appRoot ,
"public" ,
process . platform === "win32" ? "icon.ico" : "icon.png" ,
),
2025-09-12 14:42:00 -05:00
webPreferences : {
nodeIntegration : false ,
contextIsolation : true ,
2026-09-20 14:56:39 -05:00
sandbox : true ,
webSecurity : true ,
2025-09-12 14:42:00 -05:00
preload : path . join ( __dirname , "preload.js" ),
2026-10-18 00:34:50 -05:00
additionalArguments : [ `--termix-backend-port= ${ backendPort } ` ],
2026-05-06 15:12:07 -05:00
partition : termixSessionPartition ,
2026-09-20 14:56:39 -05:00
allowRunningInsecureContent : false ,
webviewTag : false ,
2025-11-05 10:36:16 -06:00
offscreen : false ,
2025-09-12 14:42:00 -05:00
},
2025-11-05 10:36:16 -06:00
show : true ,
2025-09-12 14:42:00 -05:00
});
2026-03-08 18:02:14 -05:00
mainWindow . webContents . session . setPermissionRequestHandler (
( webContents , permission , callback ) => {
if (
permission === "clipboard-read" ||
2026-05-06 15:12:07 -05:00
permission === "clipboard-write" ||
2026-03-08 18:02:14 -05:00
permission === "clipboard-sanitized-write"
) {
callback ( true );
return ;
}
2026-05-06 15:12:07 -05:00
callback ( false );
2026-03-08 18:02:14 -05:00
},
);
2025-09-12 14:42:00 -05:00
if ( process . platform !== "darwin" ) {
mainWindow . setMenuBarVisibility ( false );
}
2025-11-05 10:36:16 -06:00
const customUserAgent = `Termix-Desktop/ ${ appVersion } ( ${ platform } ; Electron/ ${ electronVersion } )` ;
mainWindow . webContents . setUserAgent ( customUserAgent );
2026-08-19 14:12:06 -05:00
mainWindow . webContents . on ( "before-input-event" , ( event , input ) => {
if ( process . platform !== "win32" || ! isCloseActiveTabInput ( input )) return ;
event . preventDefault ();
mainWindow . webContents . send ( "close-active-tab" );
});
2025-11-05 10:36:16 -06:00
mainWindow . webContents . session . webRequest . onBeforeSendHeaders (
( details , callback ) => {
details . requestHeaders [ "X-Electron-App" ] = "true" ;
details . requestHeaders [ "User-Agent" ] = customUserAgent ;
2026-10-10 14:58:27 -05:00
// YouTube refuses embeds without a Referer, which file:// never sends.
if (
details . url . startsWith ( "https://www.youtube-nocookie.com/embed/" ) &&
! details . requestHeaders . Referer
) {
details . requestHeaders . Referer = "https://termix.site/" ;
}
2026-05-06 15:12:07 -05:00
const rememberedJwt = getRememberedElectronAuthCookie ( "jwt" , details . url );
if ( rememberedJwt ) {
setCookieHeaderValue (
details . requestHeaders ,
rememberedJwt . name ,
rememberedJwt . value ,
);
}
2026-10-01 16:20:16 -05:00
callback ({
requestHeaders : linkedServer . applyLinkHeaders (
details . url ,
details . requestHeaders ,
),
});
2025-11-05 10:36:16 -06:00
},
);
2025-09-12 14:42:00 -05:00
if ( isDev ) {
mainWindow . loadURL ( "http://localhost:5173" );
mainWindow . webContents . openDevTools ();
} else {
2025-11-05 10:36:16 -06:00
const indexPath = path . join ( appRoot , "dist" , "index.html" );
mainWindow . loadFile ( indexPath ). catch (( err ) => {
console . error ( "Failed to load file:" , err );
});
2025-09-12 14:42:00 -05:00
}
2025-11-05 10:36:16 -06:00
mainWindow . webContents . session . webRequest . onHeadersReceived (
( details , callback ) => {
const headers = details . responseHeaders ;
if ( headers ) {
delete headers [ "x-frame-options" ];
delete headers [ "X-Frame-Options" ];
if ( headers [ "content-security-policy" ]) {
headers [ "content-security-policy" ] = headers [
"content-security-policy"
]
. map (( value ) => value . replace ( /frame-ancestors[^;]*/gi , "" ))
. filter (( value ) => value . trim (). length > 0 );
if ( headers [ "content-security-policy" ]. length === 0 ) {
delete headers [ "content-security-policy" ];
}
}
if ( headers [ "Content-Security-Policy" ]) {
headers [ "Content-Security-Policy" ] = headers [
"Content-Security-Policy"
]
. map (( value ) => value . replace ( /frame-ancestors[^;]*/gi , "" ))
. filter (( value ) => value . trim (). length > 0 );
if ( headers [ "Content-Security-Policy" ]. length === 0 ) {
delete headers [ "Content-Security-Policy" ];
}
}
2026-05-06 15:12:07 -05:00
const setCookieHeaderName = getHeaderName ( headers , "Set-Cookie" );
if ( setCookieHeaderName ) {
const setCookieHeaders = Array . isArray ( headers [ setCookieHeaderName ])
? headers [ setCookieHeaderName ]
: [ headers [ setCookieHeaderName ]];
setCookieHeaders . forEach (( cookie ) => {
rememberElectronAuthCookieFromHeader ( details . url , cookie );
});
headers [ setCookieHeaderName ] = setCookieHeaders . map (( cookie ) => {
2025-11-05 10:36:16 -06:00
let modified = cookie . replace (
/;\s*SameSite=Strict/gi ,
"; SameSite=None" ,
);
modified = modified . replace (
/;\s*SameSite=Lax/gi ,
"; SameSite=None" ,
);
if ( ! modified . includes ( "SameSite=" )) {
modified += "; SameSite=None" ;
}
if (
! modified . includes ( "Secure" ) &&
details . url . startsWith ( "https" )
) {
modified += "; Secure" ;
}
return modified ;
});
}
}
callback ({ responseHeaders : headers });
},
);
2025-09-12 14:42:00 -05:00
mainWindow . once ( "ready-to-show" , () => {
mainWindow . show ();
2026-05-28 22:05:25 -04:00
mainWindow . focus ();
2025-09-12 14:42:00 -05:00
});
2025-11-05 10:36:16 -06:00
setTimeout (() => {
if ( mainWindow && ! mainWindow . isVisible ()) {
mainWindow . show ();
}
}, 3000 );
2025-09-12 14:42:00 -05:00
mainWindow . webContents . on (
"did-fail-load" ,
( event , errorCode , errorDescription , validatedURL ) => {
console . error (
"Failed to load:" ,
errorCode ,
errorDescription ,
validatedURL ,
);
},
);
mainWindow . webContents . on ( "did-finish-load" , () => {
console . log ( "Frontend loaded successfully" );
});
2026-10-01 16:20:16 -05:00
mainWindow . webContents . on ( "will-prevent-unload" , ( event ) => {
// Explicit Quit must not be vetoed by the renderer's connection guard.
// In this Electron event, preventDefault allows the unload to proceed.
if ( isQuitting ) event . preventDefault ();
});
2026-03-08 18:02:14 -05:00
mainWindow . on ( "close" , ( event ) => {
if ( ! isQuitting && tray && ! tray . isDestroyed ()) {
event . preventDefault ();
2026-10-01 16:20:16 -05:00
// Stock GNOME has no tray to bring a hidden window back from.
if (
process . platform === "linux" &&
/gnome/i . test ( process . env . XDG_CURRENT_DESKTOP || "" )
) {
mainWindow . minimize ();
} else {
mainWindow . hide ();
}
2026-03-08 18:02:14 -05:00
}
});
2026-10-20 16:03:29 -05:00
mainWindow . webContents . on ( "did-start-navigation" , ( details ) => {
if ( details . isMainFrame && ! details . isSameDocument ) {
embeddedViews . destroyAll ();
}
});
const sendTitleBarVisible = () => {
if ( ! mainWindow || mainWindow . isDestroyed ()) return ;
mainWindow . webContents . send (
"title-bar-visible-changed" ,
titleBarVisible ( mainWindow ),
);
};
mainWindow . on ( "enter-full-screen" , sendTitleBarVisible );
mainWindow . on ( "leave-full-screen" , sendTitleBarVisible );
2025-09-12 14:42:00 -05:00
mainWindow . on ( "closed" , () => {
2026-10-20 16:03:29 -05:00
embeddedViews . destroyAll ();
2025-09-12 14:42:00 -05:00
mainWindow = null ;
});
mainWindow . webContents . setWindowOpenHandler (({ url }) => {
2026-05-28 22:29:20 -05:00
try {
const parsed = new URL ( url );
if ( parsed . protocol === "http:" || parsed . protocol === "https:" ) {
shell . openExternal ( url );
}
} catch {
// invalid URL, ignore
}
2025-09-12 14:42:00 -05:00
return { action : "deny" };
});
2026-09-20 14:56:39 -05:00
mainWindow . webContents . on ( "will-navigate" , ( event , url ) => {
const allowedUrl = isDev
? url . startsWith ( "http://localhost:5173/" )
: url === pathToFileURL ( path . join ( appRoot , "dist" , "index.html" )). href ;
if ( ! allowedUrl ) event . preventDefault ();
});
2025-09-12 14:42:00 -05:00
}
ipcMain . handle ( "get-app-version" , () => {
return app . getVersion ();
});
2026-10-20 16:03:29 -05:00
/** The window an IPC call came from, so the main window and the extra
* windows (isolated views, sign-in) can each answer for themselves. */
function windowFor ( event ) {
const win = BrowserWindow . fromWebContents ( event . sender );
if ( win && ! win . isDestroyed ()) return win ;
if ( mainWindow && ! mainWindow . isDestroyed ()) return mainWindow ;
return null ;
}
2025-09-12 14:42:00 -05:00
ipcMain . handle ( "get-platform" , () => {
return process . platform ;
});
2026-10-20 16:03:29 -05:00
// Whether the app-drawn title bar shows in the window that asks. Only the
// main window hides the native bar, and in full screen macOS hides the
// traffic lights, so the bar goes away there too.
function titleBarVisible ( win ) {
return !! win && win === mainWindow && ! win . isFullScreen ();
}
ipcMain . handle ( "get-title-bar-visible" , ( event ) => {
return titleBarVisible ( windowFor ( event ));
});
// The renderer sends the colors of the active theme so the window controls
// the system draws (Windows and Linux) match the app.
ipcMain . handle ( "set-title-bar-overlay" , ( event , colors ) => {
const win = windowFor ( event );
if ( ! win || process . platform === "darwin" ) return ;
const options = {};
if ( typeof colors ? . color === "string" && colors . color ) {
options . color = colors . color ;
}
if ( typeof colors ? . symbolColor === "string" && colors . symbolColor ) {
options . symbolColor = colors . symbolColor ;
}
if ( Object . keys ( options ). length === 0 ) return ;
try {
win . setTitleBarOverlay ( options );
} catch ( error ) {
logToFile ( "setTitleBarOverlay failed:" , error );
}
});
2026-03-08 18:02:14 -05:00
ipcMain . handle ( "get-embedded-server-status" , () => {
return {
2026-05-28 22:29:20 -05:00
running :
backendProcess !== null && ! backendProcess . killed && ! backendStartFailed ,
2026-09-20 14:56:39 -05:00
// A backend that is merely slow to boot reports no failure, so the
// renderer keeps waiting for it. Only a classified failure means the
// process is gone for good and waiting is pointless.
failure : backendFailure ,
2026-03-08 18:02:14 -05:00
dataDir : isDev ? null : getBackendDataDir (),
};
});
2026-10-01 16:20:16 -05:00
// External login through the system browser (RFC 8252)
2026-05-28 22:29:20 -05:00
ipcMain . handle (
2026-10-01 16:20:16 -05:00
"external-browser-login" ,
2026-05-28 22:29:20 -05:00
async ( _event , authUrl , callbackPort ) => {
2026-10-18 00:34:50 -05:00
if ( ! isExternalLoginUrl ( authUrl )) {
return { success : false , error : "Invalid authentication URL" };
}
if ( ! isCallbackPort ( callbackPort )) {
return { success : false , error : "Invalid callback port" };
}
2026-05-28 22:29:20 -05:00
const http = require ( "http" );
2026-06-29 13:28:26 -05:00
return new Promise (( resolve ) => {
let timeout ;
let settled = false ;
2026-05-28 22:29:20 -05:00
const server = http . createServer (( req , res ) => {
2026-06-29 13:28:26 -05:00
const url = new URL ( req . url || "/" , `http://localhost: ${ callbackPort } ` );
2026-05-28 22:29:20 -05:00
if ( url . pathname === "/oidc-callback" ) {
const success = url . searchParams . get ( "success" );
const error = url . searchParams . get ( "error" );
const token = url . searchParams . get ( "token" );
2026-10-01 16:20:16 -05:00
const secondFactor = url . searchParams . get ( "second_factor" ) === "1" ;
2026-05-28 22:29:20 -05:00
res . writeHead ( 200 , { "Content-Type" : "text/html" });
res . end (
2026-10-01 16:20:16 -05:00
`<html><body><h2> ${ success === "true" || secondFactor ? "Authentication successful!" : "Authentication failed." } </h2><p>You can close this tab and return to Termix.</p><script>window.close()</script></body></html>` ,
2026-05-28 22:29:20 -05:00
);
2026-10-01 16:20:16 -05:00
if ( secondFactor ) {
finish ({
success : false ,
secondFactor : true ,
tempToken : url . searchParams . get ( "temp_token" ),
factors : url . searchParams . get ( "second_factors" ),
});
} else if ( success === "true" ) {
2026-06-29 13:28:26 -05:00
finish ({ success : true , token });
2026-05-28 22:29:20 -05:00
} else {
2026-06-29 13:28:26 -05:00
finish ({
2026-05-28 22:29:20 -05:00
success : false ,
error : error || "Authentication failed" ,
});
}
2026-06-29 13:28:26 -05:00
return ;
}
res . writeHead ( 404 , { "Content-Type" : "text/plain" });
res . end ( "Not found" );
});
const finish = ( result ) => {
if ( settled ) return ;
settled = true ;
if ( timeout ) clearTimeout ( timeout );
try {
server . close ();
} catch {
// Server may not have started yet.
}
resolve ( result );
};
const fail = ( error ) => {
finish ({
success : false ,
error : error instanceof Error ? error . message : String ( error ),
});
};
server . once ( "error" , fail );
2026-07-19 12:29:52 -05:00
server . listen ( callbackPort , "localhost" , async () => {
2026-06-29 13:28:26 -05:00
try {
await shell . openExternal ( authUrl );
} catch ( error ) {
fail ( error );
2026-05-28 22:29:20 -05:00
}
});
// Timeout after 5 minutes
2026-06-29 13:28:26 -05:00
timeout = setTimeout (
2026-05-28 22:29:20 -05:00
() => {
2026-10-01 16:20:16 -05:00
fail ( new Error ( "External login timed out" ));
2026-05-28 22:29:20 -05:00
},
5 * 60 * 1000 ,
);
});
},
);
2026-10-01 16:20:16 -05:00
// Desktop-only settings, such as whether hosts connect from this device or
// through the linked server by default.
2026-07-26 18:47:27 -05:00
ipcMain . handle ( "get-desktop-settings" , () => {
2026-10-01 16:20:16 -05:00
return desktopSettings . getDesktopSettings ();
2026-07-26 18:47:27 -05:00
});
ipcMain . handle ( "save-desktop-settings" , ( _event , settings ) => {
2026-10-01 16:20:16 -05:00
return desktopSettings . saveDesktopSettings ( settings );
2026-07-26 18:47:27 -05:00
});
2026-10-12 11:35:48 -05:00
let appUpdater = null ;
function startAppUpdater () {
const kind = autoUpdates . getInstallKind ({
platform : process . platform ,
arch : process . arch ,
isPackaged : app . isPackaged ,
mas : !! process . mas ,
env : process . env ,
execPath : process . execPath ,
resourcesPath : process . resourcesPath ,
exists : ( target ) => fs . existsSync ( target ),
readFile : ( target ) => fs . readFileSync ( target , "utf8" ),
});
logToFile ( "Updater install kind:" , kind );
appUpdater = autoUpdates . createUpdater ({
currentVersion : app . getVersion (),
platform : process . platform ,
arch : process . arch ,
kind ,
getMode : () => desktopSettings . getDesktopSettings (). updateMode ,
saveMode : ( mode ) =>
desktopSettings . saveDesktopSettings ({ updateMode : mode }),
fetchReleases : async () => {
const { net } = require ( "electron" );
const response = await net . fetch ( autoUpdates . RELEASES_API , {
headers : { Accept : "application/vnd.github+json" },
});
if ( ! response . ok ) {
throw new Error ( `GitHub releases returned ${ response . status } ` );
}
return response . json ();
},
loadAutoUpdater : () => require ( "electron-updater" ). autoUpdater ,
send : ( state ) => {
if ( mainWindow && ! mainWindow . isDestroyed ()) {
mainWindow . webContents . send ( "desktop-update-state" , state );
}
},
log : logToFile ,
beforeInstall : () => {
isQuitting = true ;
},
});
appUpdater . start ();
}
ipcMain . handle (
"desktop-update-get-state" ,
() => appUpdater ? . getState () ?? null ,
);
ipcMain . handle ( "desktop-update-check" , () => appUpdater ? . check () ?? null );
ipcMain . handle ( "desktop-update-download" , () => appUpdater ? . download () ?? null );
ipcMain . handle ( "desktop-update-install" , () => appUpdater ? . install () ?? false );
ipcMain . handle (
"desktop-update-set-mode" ,
( _event , mode ) => appUpdater ? . setMode ( mode ) ?? null ,
);
2026-10-01 16:20:16 -05:00
// The server an older version of the app was pointed at, offered again when
// linking so upgraded installs do not have to look it up.
ipcMain . handle ( "get-previous-server-url" , () => {
return { serverUrl : desktopSettings . getPreviousServerUrl () };
2026-07-26 18:47:27 -05:00
});
2026-10-18 00:34:50 -05:00
function readSetting ( key ) {
const settingsPath = path . join ( app . getPath ( "userData" ), "settings.json" );
if ( ! fs . existsSync ( settingsPath )) return null ;
const settings = JSON . parse ( fs . readFileSync ( settingsPath , "utf8" ));
return settings [ key ] !== undefined ? settings [ key ] : null ;
}
2025-11-17 09:46:05 -06:00
ipcMain . handle ( "get-setting" , ( event , key ) => {
try {
2026-10-18 00:34:50 -05:00
return readSetting ( key );
2025-11-17 09:46:05 -06:00
} catch ( error ) {
console . error ( "Error reading setting:" , error );
return null ;
}
});
ipcMain . handle ( "set-setting" , ( event , key , value ) => {
2026-10-18 00:34:50 -05:00
if ( typeof key !== "string" ) {
return { success : false , error : "Invalid setting key" };
}
if (
key === EXTERNAL_EDITOR_SETTING &&
value &&
! dialogChosenPaths . has ( value )
) {
return { success : false , error : "Choose the editor in the file dialog" };
}
2025-11-17 09:46:05 -06:00
try {
const userDataPath = app . getPath ( "userData" );
const settingsPath = path . join ( userDataPath , "settings.json" );
if ( ! fs . existsSync ( userDataPath )) {
fs . mkdirSync ( userDataPath , { recursive : true });
}
let settings = {};
if ( fs . existsSync ( settingsPath )) {
const settingsData = fs . readFileSync ( settingsPath , "utf8" );
settings = JSON . parse ( settingsData );
}
settings [ key ] = value ;
fs . writeFileSync ( settingsPath , JSON . stringify ( settings , null , 2 ));
return { success : true };
} catch ( error ) {
console . error ( "Error saving setting:" , error );
return { success : false , error : error . message };
}
});
2026-03-08 18:02:14 -05:00
ipcMain . handle ( "clear-session-cookies" , async () => {
try {
2026-05-06 15:12:07 -05:00
clearPersistedElectronAuthCookies ();
2026-03-08 18:02:14 -05:00
const ses = mainWindow ? . webContents ? . session ;
if ( ses ) {
const cookies = await ses . cookies . get ({});
for ( const cookie of cookies ) {
2026-05-06 15:12:07 -05:00
await ses . cookies . remove ( getCookieRemovalUrl ( cookie ), cookie . name );
2026-03-08 18:02:14 -05:00
}
}
} catch ( error ) {
console . error ( "Failed to clear session cookies:" , error );
}
});
2026-05-06 15:12:07 -05:00
ipcMain . handle ( "clipboard-write-text" , ( _event , text ) => {
clipboard . writeText ( typeof text === "string" ? text : String ( text ?? "" ));
return true ;
});
ipcMain . handle ( "clipboard-read-text" , () => clipboard . readText ());
2026-06-29 13:28:26 -05:00
ipcMain . handle ( "show-save-dialog" , async ( _event , options ) => {
return dialog . showSaveDialog ( mainWindow , options || {});
});
ipcMain . handle ( "show-open-dialog" , async ( _event , options ) => {
2026-10-18 00:34:50 -05:00
const result = await dialog . showOpenDialog ( mainWindow , options || {});
for ( const filePath of result . filePaths ) dialogChosenPaths . add ( filePath );
return result ;
2026-06-29 13:28:26 -05:00
});
ipcMain . handle ( "create-temp-file" , async ( _event , fileData ) => {
try {
const result = createManagedTempFile (
fileData ? . fileName ,
fileData ? . content ,
fileData ? . encoding ,
);
return { success : true , ... result };
} catch ( error ) {
return { success : false , error : error . message };
}
});
ipcMain . handle ( "create-temp-folder" , async ( _event , folderData ) => {
try {
const tempId = ` ${ Date . now () } - ${ Math . random (). toString ( 36 ). slice ( 2 ) } ` ;
const tempDir = fs . mkdtempSync ( path . join ( getTempRoot (), ` ${ tempId } -` ));
const folderPath = path . join (
tempDir ,
sanitizeFileName ( folderData ? . folderName || "files" ),
);
fs . mkdirSync ( folderPath , { recursive : true });
for ( const file of folderData ? . files || []) {
const relativePath = String ( file . relativePath || "" )
. split ( /[\\/]+/ )
. map ( sanitizeFileName )
. filter ( Boolean )
. join ( path . sep );
if ( ! relativePath ) continue ;
const targetPath = path . join ( folderPath , relativePath );
fs . mkdirSync ( path . dirname ( targetPath ), { recursive : true });
fs . writeFileSync (
targetPath ,
decodeFileContent ( file . content , file . encoding ),
);
}
tempFiles . set ( tempId , { path : folderPath , dir : tempDir });
return { success : true , tempId , path : folderPath };
} catch ( error ) {
return { success : false , error : error . message };
}
});
ipcMain . handle ( "start-drag-to-desktop" , ( event , dragData ) => {
try {
const temp = tempFiles . get ( dragData ? . tempId );
if ( ! temp ) return { success : false , error : "Temporary file not found" };
event . sender . startDrag ({
file : temp . path ,
icon : nativeImage . createEmpty (),
});
return { success : true };
} catch ( error ) {
return { success : false , error : error . message };
}
});
ipcMain . handle ( "cleanup-temp-file" , ( _event , tempId ) => {
try {
cleanupManagedTempFile ( tempId );
return { success : true };
} catch ( error ) {
return { success : false , error : error . message };
}
});
2025-11-05 10:36:16 -06:00
function createMenu () {
if ( process . platform === "darwin" ) {
const template = [
{
label : app . name ,
submenu : [
{ role : "about" },
{ type : "separator" },
{ role : "services" },
{ type : "separator" },
{ role : "hide" },
{ role : "hideOthers" },
{ role : "unhide" },
{ type : "separator" },
{ role : "quit" },
],
},
{
label : "Edit" ,
submenu : [
{ role : "undo" },
{ role : "redo" },
{ type : "separator" },
{ role : "cut" },
{ role : "copy" },
{ role : "paste" },
{ role : "selectAll" },
],
},
{
label : "View" ,
submenu : [
{ role : "reload" },
{ role : "forceReload" },
{ role : "toggleDevTools" },
{ type : "separator" },
{ role : "resetZoom" },
{ role : "zoomIn" },
{ role : "zoomOut" },
{ type : "separator" },
{ role : "togglefullscreen" },
],
},
{
label : "Window" ,
submenu : [
{ role : "minimize" },
{ role : "zoom" },
{ type : "separator" },
{ role : "front" },
{ type : "separator" },
{ role : "window" },
],
},
];
const menu = Menu . buildFromTemplate ( template );
Menu . setApplicationMenu ( menu );
}
}
2026-03-08 18:02:14 -05:00
app . whenReady (). then ( async () => {
logToFile ( "=== App ready ===" );
logToFile (
"isDev:" ,
isDev ,
"platform:" ,
process . platform ,
"arch:" ,
process . arch ,
);
2026-06-29 13:28:26 -05:00
if ( process . platform === "win32" ) {
app . setAppUserModelId ( windowsAppUserModelId );
}
2025-11-05 10:36:16 -06:00
createMenu ();
2026-05-06 15:12:07 -05:00
await clearElectronClientCacheIfBuildChanged ();
await clearElectronJwtCookiesAtStartup ();
2026-03-08 18:02:14 -05:00
if ( ! isDev ) {
2026-10-18 00:34:50 -05:00
backendPort = await findBackendPort ();
2026-03-08 18:02:14 -05:00
const result = await startBackendServer ();
logToFile ( "startBackendServer result:" , result );
} else {
logToFile (
2026-10-10 14:58:27 -05:00
"Skipping embedded backend (isDev=true) - expecting the dev runner's backend" ,
2026-03-08 18:02:14 -05:00
);
2026-10-10 14:58:27 -05:00
if ( isDevRunner ()) {
process . on ( "message" , ( msg ) => void handleBackendRequest ( msg ));
}
2026-03-08 18:02:14 -05:00
}
createTray ();
2025-09-12 14:42:00 -05:00
createWindow ();
2026-10-12 11:35:48 -05:00
startAppUpdater ();
2026-10-01 16:20:16 -05:00
desktopSettings . removeOldSyncFiles ();
2026-03-08 18:02:14 -05:00
logToFile ( "=== Startup complete ===" );
2025-09-12 14:42:00 -05:00
});
app . on ( "window-all-closed" , () => {
2026-03-08 18:02:14 -05:00
if ( ! tray || tray . isDestroyed ()) {
app . quit ();
}
2025-09-12 14:42:00 -05:00
});
app . on ( "activate" , () => {
if ( BrowserWindow . getAllWindows (). length === 0 ) {
createWindow ();
2026-05-28 22:05:25 -04:00
} else if ( mainWindow ) {
mainWindow . show ();
mainWindow . focus ();
2025-09-12 14:42:00 -05:00
}
});
2026-03-08 18:02:14 -05:00
app . on ( "before-quit" , () => {
isQuitting = true ;
});
2025-09-12 14:42:00 -05:00
app . on ( "will-quit" , () => {
console . log ( "App will quit..." );
2026-06-29 13:28:26 -05:00
for ( const tempId of tempFiles . keys ()) {
cleanupManagedTempFile ( tempId );
}
2026-03-08 18:02:14 -05:00
stopBackendServer ();
2025-09-12 14:42:00 -05:00
});
process . on ( "uncaughtException" , ( error ) => {
console . error ( "Uncaught Exception:" , error );
});
process . on ( "unhandledRejection" , ( reason , promise ) => {
console . error ( "Unhandled Rejection at:" , promise , "reason:" , reason );
});