///////////////////////////////////////////////////////////// // // pgAdmin 4 - PostgreSQL Tools // // Copyright (C) 2013 - 2025, The pgAdmin Development Team // This software is released under the PostgreSQL Licence // ////////////////////////////////////////////////////////////// import { app, BrowserWindow, dialog, ipcMain, Menu, shell, screen, autoUpdater } from 'electron'; import axios from 'axios'; import Store from 'electron-store'; import fs from 'fs'; import path from 'path'; import * as misc from './misc.js'; import { spawn } from 'child_process'; import { fileURLToPath } from 'url'; import { setupMenu } from './menu.js'; import contextMenu from 'electron-context-menu'; import { setupDownloader } from './downloader.js'; import { setupAutoUpdater, updateConfigAndMenus } from './autoUpdaterHandler.js'; const configStore = new Store({ defaults: { fixedPort: false, portNo: 5050, connectionTimeout: 180, openDocsInBrowser: true, }, }); let pgadminServerProcess = null; let startPageUrl = null; let serverCheckUrl = null; let pgAdminMainScreen = null; let configureWindow = null, viewLogWindow = null; let serverPort = 5050; let UUID = crypto.randomUUID(); let appStartTime = (new Date()).getTime(); const __dirname = path.dirname(fileURLToPath(import.meta.url)); let baseUrl = `http://127.0.0.1:${serverPort}`; let docsURLSubStrings = ['www.enterprisedb.com', 'www.postgresql.org', 'www.pgadmin.org', 'help/help']; process.env['ELECTRON_ENABLE_SECURITY_WARNINGS'] = false; // Paths to the rest of the app let [pythonPath, pgadminFile] = misc.getAppPaths(__dirname); const menuCallbacks = { 'check_for_updates': ()=>{ pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', {check_version_update: true}); }, 'restart_to_update': ()=>{ forceQuitAndInstallUpdate(); }, 'view_logs': ()=>{ if(viewLogWindow === null || viewLogWindow?.isDestroyed()) { viewLogWindow = new BrowserWindow({ show: false, width: 800, height: 460, position: 'center', resizable: false, parent: pgAdminMainScreen, icon: '../../assets/pgAdmin4.png', webPreferences: { preload: path.join(__dirname, 'other_preload.js'), }, }); viewLogWindow.loadFile('./src/html/view_log.html'); viewLogWindow.once('ready-to-show', ()=>{ viewLogWindow.show(); }); } else { viewLogWindow.hide(); viewLogWindow.show(); } }, 'configure': openConfigure, 'reloadApp': reloadApp, }; // Do not allow a second instance of pgAdmin to run. const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); } else { app.on('second-instance', () => { // Someone tried to run a second instance, we should focus our window. if (pgAdminMainScreen) { if (pgAdminMainScreen.isMinimized()) pgAdminMainScreen.restore(); pgAdminMainScreen.focus(); } }); } // Override the paths above, if a developer needs to if (fs.existsSync('dev_config.json')) { try { let dev_config = JSON.parse(fs.readFileSync('dev_config.json')); pythonPath = path.resolve(dev_config['pythonPath']); pgadminFile = path.resolve(dev_config['pgadminFile']); } catch (error) { console.error('Failed to load dev_config', error); } } contextMenu({ showInspectElement: false, showSearchWithGoogle: false, showLookUpSelection: false, showSelectAll: true, }); Menu.setApplicationMenu(null); // Check if the given position is within the display bounds. // pgAdmin tried to open the window on the display where the it // was last closed. function isWithinDisplayBounds(pos) { const displays = screen.getAllDisplays(); return displays.reduce((result, display) => { const area = display.workArea; return ( result || (pos.x >= area.x && pos.y >= area.y && pos.x < area.x + area.width && pos.y < area.y + area.height) ); }, false); } function openConfigure() { if (configureWindow === null || configureWindow?.isDestroyed()) { configureWindow = new BrowserWindow({ show: false, width: 600, height: 580, position: 'center', resizable: false, parent: pgAdminMainScreen, icon: '../../assets/pgAdmin4.png', webPreferences: { preload: path.join(__dirname, 'other_preload.js'), }, }); configureWindow.loadFile('./src/html/configure.html'); configureWindow.once('ready-to-show', ()=>{ configureWindow.show(); }); } else { configureWindow.hide(); configureWindow.show(); } } function showErrorDialog(intervalID) { if(!splashWindow.isVisible()) { return; } clearInterval(intervalID); splashWindow.close(); new BrowserWindow({ 'frame': true, 'width': 800, 'height': 450, 'position': 'center', 'resizable': false, 'focus': true, 'show': true, icon: '../../assets/pgAdmin4.png', webPreferences: { preload: path.join(__dirname, 'other_preload.js'), }, }).loadFile('./src/html/server_error.html'); } function reloadApp() { const currWin = BrowserWindow.getFocusedWindow(); const preventUnload = (event) => { event.preventDefault(); currWin.webContents.off('will-prevent-unload', preventUnload); }; currWin.webContents.on('will-prevent-unload', preventUnload); currWin.webContents.reload(); } // Remove auto_update_enabled from configStore on app close or quit function cleanupAutoUpdateFlag() { if (configStore.has('auto_update_enabled')) { configStore.delete('auto_update_enabled'); } } // This function will force quit and install update and restart the app function forceQuitAndInstallUpdate() { // Disable beforeunload handlers const preventUnload = (event) => { event.preventDefault(); pgAdminMainScreen.webContents.off('will-prevent-unload', preventUnload); }; pgAdminMainScreen.webContents.on('will-prevent-unload', preventUnload); // Set flag to show notification after restart configStore.set('update_installed', true); cleanupAutoUpdateFlag(); autoUpdater.quitAndInstall(); } // This functions is used to start the pgAdmin4 server by spawning a // separate process. function startDesktopMode() { // Return if pgAdmin server process is already spawned // Added check for debugging purpose. if (pgadminServerProcess != null) return; let pingIntervalID; // Set the environment variables so that pgAdmin 4 server // starts listening on the appropriate port. process.env.PGADMIN_INT_PORT = serverPort; process.env.PGADMIN_INT_KEY = UUID; process.env.PGADMIN_SERVER_MODE = 'OFF'; // Start Page URL baseUrl = `http://127.0.0.1:${serverPort}`; startPageUrl = `${baseUrl}/?key=${UUID}`; serverCheckUrl = `${baseUrl}/misc/ping?key=${UUID}`; // Write Python Path, pgAdmin file path and command in log file. misc.writeServerLog('pgAdmin Runtime Environment'); misc.writeServerLog('--------------------------------------------------------'); let command = pythonPath + ' -s ' + pgadminFile; misc.writeServerLog('Python Path: "' + pythonPath + '"'); misc.writeServerLog('Runtime Config File: "' + path.resolve(configStore.path) + '"'); misc.writeServerLog('Webapp Path: "' + pgadminFile + '"'); misc.writeServerLog('pgAdmin Command: "' + command + '"'); misc.writeServerLog('Environment: '); Object.keys(process.env).forEach(function (key) { // Below code is included only for Mac OS as default path for azure CLI // installation path is not included in PATH variable while spawning // runtime environment. if (process.platform === 'darwin' && key === 'PATH') { let updated_path = process.env[key] + ':/usr/local/bin'; process.env[key] = updated_path; } if (process.platform === 'win32' && key.toUpperCase() === 'PATH') { let _libpq_path = path.join(path.dirname(path.dirname(path.resolve(pgadminFile))), 'runtime'); process.env[key] = _libpq_path + ';' + process.env[key]; } misc.writeServerLog(' - ' + key + ': ' + process.env[key]); }); misc.writeServerLog('--------------------------------------------------------\n'); // Spawn the process to start pgAdmin4 server. let spawnStartTime = (new Date).getTime(); pgadminServerProcess = spawn(pythonPath, ['-s', pgadminFile]); pgadminServerProcess.on('error', function (err) { // Log the error into the log file if process failed to launch misc.writeServerLog('Failed to launch pgAdmin4. Error:'); misc.writeServerLog(err); showErrorDialog(pingIntervalID); }); let spawnEndTime = (new Date).getTime(); misc.writeServerLog('Total spawn time to start the pgAdmin4 server: ' + (spawnEndTime - spawnStartTime) / 1000 + ' Sec'); pgadminServerProcess.stdout.setEncoding('utf8'); pgadminServerProcess.stdout.on('data', (chunk) => { misc.writeServerLog(chunk); }); pgadminServerProcess.stderr.setEncoding('utf8'); pgadminServerProcess.stderr.on('data', (chunk) => { misc.writeServerLog(chunk); }); // This function is used to ping the pgAdmin4 server whether it // it is started or not. function pingServer() { return axios.get(serverCheckUrl); } let connectionTimeout = configStore.get('connectionTimeout', 180) * 1000; let currentTime = (new Date).getTime(); let endTime = currentTime + connectionTimeout; let midTime1 = currentTime + (connectionTimeout / 2); let midTime2 = currentTime + (connectionTimeout * 2 / 3); let pingInProgress = false; // ping pgAdmin server every 1 second. let pingStartTime = (new Date).getTime(); pingIntervalID = setInterval(function () { // If ping request is already send and response is not // received no need to send another request. if (pingInProgress) return; pingServer().then(() => { pingInProgress = false; splashWindow.webContents.executeJavaScript('document.getElementById(\'loader-text-status\').innerHTML = \'pgAdmin 4 started\';', true); // Set the pgAdmin process object to misc misc.setProcessObject(pgadminServerProcess); clearInterval(pingIntervalID); let appEndTime = (new Date).getTime(); misc.writeServerLog('------------------------------------------'); misc.writeServerLog('Total time taken to ping pgAdmin4 server: ' + (appEndTime - pingStartTime) / 1000 + ' Sec'); misc.writeServerLog('------------------------------------------'); misc.writeServerLog('Total launch time of pgAdmin4: ' + (appEndTime - appStartTime) / 1000 + ' Sec'); misc.writeServerLog('------------------------------------------'); launchPgAdminWindow(); }).catch(() => { pingInProgress = false; let curTime = (new Date).getTime(); // if the connection timeout has lapsed then throw an error // and stop pinging the server. if (curTime >= endTime) { showErrorDialog(pingIntervalID); } if (curTime > midTime1) { if (curTime < midTime2) { splashWindow.webContents.executeJavaScript('document.getElementById(\'loader-text-status\').innerHTML = \'Taking longer than usual...\';', true); } else { splashWindow.webContents.executeJavaScript('document.getElementById(\'loader-text-status\').innerHTML = \'Almost there...\';', true); } } }); pingInProgress = true; }, 1000); } // This function is used to hide the splash screen and create/launch // new window to render pgAdmin4 page. function launchPgAdminWindow() { // Create and launch new window and open pgAdmin url misc.writeServerLog('Application Server URL: ' + startPageUrl); pgAdminMainScreen = new BrowserWindow({ 'id': 'pgadmin-main', 'icon': '../../assets/pgAdmin4.png', 'frame': true, 'position': 'center', 'resizable': true, 'minWidth': 640, 'minHeight': 480, 'width': 1024, 'height': 768, 'focus': true, 'show': false, webPreferences: { nodeIntegrationInSubFrames: true, preload: path.join(__dirname, 'pgadmin_preload.js'), }, }); splashWindow.close(); pgAdminMainScreen.webContents.session.clearCache(); setupMenu(pgAdminMainScreen, configStore, menuCallbacks); setupDownloader() pgAdminMainScreen.loadURL(startPageUrl); const bounds = configStore.get('bounds'); (bounds && isWithinDisplayBounds({x: bounds.x, y: bounds.y})) ? pgAdminMainScreen.setBounds(bounds) : pgAdminMainScreen.setBounds({x: 0, y: 0, width: 1024, height: 768}); pgAdminMainScreen.show(); setupAutoUpdater({ pgAdminMainScreen, configStore, menuCallbacks, baseUrl, UUID, forceQuitAndInstallUpdate, }); pgAdminMainScreen.webContents.setWindowOpenHandler(({url})=>{ let openDocsInBrowser = configStore.get('openDocsInBrowser', true); let isDocURL = false; docsURLSubStrings.forEach(function (key) { if (url.indexOf(key) >= 0) { isDocURL = true; } }); if (openDocsInBrowser && isDocURL) { // Do not open the window shell.openExternal(url); return { action: 'deny' }; } else { return { action: 'allow', overrideBrowserWindowOptions: { 'position': 'center', 'minWidth': 640, 'minHeight': 480, icon: '../../assets/pgAdmin4.png', ...pgAdminMainScreen.getBounds(), webPreferences: { preload: path.join(__dirname, 'pgadmin_preload.js'), }, }, }; } }); pgAdminMainScreen.on('closed', ()=>{ cleanupAutoUpdateFlag(); misc.cleanupAndQuitApp(); }); pgAdminMainScreen.on('close', () => { configStore.set('bounds', pgAdminMainScreen.getBounds()); updateConfigAndMenus('error-close', configStore, pgAdminMainScreen, menuCallbacks); pgAdminMainScreen.removeAllListeners('close'); pgAdminMainScreen.close(); }); // Notify if update was installed (fix: always check after main window is ready) notifyUpdateInstalled(); } let splashWindow; // Helper to notify update installed after restart function notifyUpdateInstalled() { if (configStore.get('update_installed')) { try { // Notify renderer if (pgAdminMainScreen) { misc.writeServerLog('[Auto-Updater]: Update installed successfully.'); setTimeout(() => { pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', {update_installed: true}); }, 10000); } else { // If main screen not ready, wait and send after it's created app.once('browser-window-created', (event, window) => { misc.writeServerLog('[Auto-Updater]: Update installed successfully.'); setTimeout(() => { pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', {update_installed: true}); }, 10000); }); } // Reset the flag configStore.set('update_installed', false); } catch (err) { misc.writeServerLog(`[Auto-Updater]: ${err}`); } } } // setup preload events. ipcMain.handle('showOpenDialog', (e, options) => dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender), options)); ipcMain.handle('showSaveDialog', (e, options) => dialog.showSaveDialog(BrowserWindow.fromWebContents(e.sender), options)); ipcMain.handle('showMessageBox', (e, options) => dialog.showMessageBox(BrowserWindow.fromWebContents(e.sender), options)); ipcMain.handle('getStoreData', (_e, key) => key ? configStore.get(key) : configStore.store); ipcMain.handle('setStoreData', (_e, newValues) => { configStore.store = { ...configStore.store, ...newValues, }; }); ipcMain.handle('getServerLogFile', () => misc.getServerLogFile()); ipcMain.handle('readServerLog', () => misc.readServerLog()); ipcMain.on('restartApp', ()=>{ app.relaunch(); app.exit(0); }); ipcMain.on('log', (_e, text) => { misc.writeServerLog(text); }); ipcMain.on('focus', (e) => { app.focus({steal: true}); const callerWindow = BrowserWindow.fromWebContents(e.sender); if (callerWindow) { if (callerWindow.isMinimized()) callerWindow.restore(); callerWindow.focus(); } }); ipcMain.on('reloadApp', reloadApp); ipcMain.handle('checkPortAvailable', async (_e, fixedPort)=>{ try { await misc.getAvailablePort(fixedPort); return true; } catch { return false; } }); ipcMain.handle('openConfigure', openConfigure); app.whenReady().then(() => { splashWindow = new BrowserWindow({ transparent: true, width: 750, height: 600, frame: false, movable: true, focusable: true, resizable: false, show: false, icon: '../../assets/pgAdmin4.png', }); splashWindow.loadFile('./src/html/splash.html'); splashWindow.center(); splashWindow.on('show', function () { let fixedPortCheck = configStore.get('fixedPort', false); if (fixedPortCheck) { serverPort = configStore.get('portNo'); //Start the pgAdmin in Desktop mode. startDesktopMode(); } else { // get the available TCP port by sending port no to 0. misc.getAvailablePort(0) .then((pythonApplicationPort) => { serverPort = pythonApplicationPort; //Start the pgAdmin in Desktop mode. startDesktopMode(); }) .catch((errCode) => { if (errCode === 'EADDRINUSE') { dialog.showErrorBox('Error', 'The port specified is already in use. Please enter a free port number.'); } else { dialog.showErrorBox('Error', errCode.toString()); } splashWindow.close(); }); } }); splashWindow.show(); });