codekingpro/portable-devtools
115k
1/////////////////////////////////////////////////////////////2//3// pgAdmin 4 - PostgreSQL Tools4//5// Copyright (C) 2013 - 2024, The pgAdmin Development Team6// This software is released under the PostgreSQL Licence7//8//////////////////////////////////////////////////////////////9const axios = require('axios');10const fs = require('fs');11const path = require('path');12const misc = require('../js/misc.js');13const spawn = require('child_process').spawn;14const {EOL} = require('os');15 16let pgadminServerProcess = null;17let startPageUrl = null;18let serverCheckUrl = null;19let addMenuCompleted = false;20let pgAdminMainScreen = null;21 22let serverPort = 5050;23let appStartTime = (new Date()).getTime();24 25let docsURLSubStrings = ['www.enterprisedb.com', 'www.postgresql.org', 'www.pgadmin.org', 'help/help'];26 27// Paths to the rest of the app28let pythonPath = misc.getPythonPath();29let pgadminFile = '../web/pgAdmin4.py';30let configFile = '../web/config.py';31 32// Override the paths above, if a developer needs to33if (fs.existsSync('dev_config.json')) {34 try {35 let dev_config = JSON.parse(fs.readFileSync('dev_config.json'));36 pythonPath = dev_config['pythonPath'];37 pgadminFile = dev_config['pgadminFile'];38 } catch (error) {39 // Meh.40 }41}42 43// This functions is used to start the pgAdmin4 server by spawning a44// separate process.45function startDesktopMode() {46 // Return if pgAdmin server process is already spawned47 // Added check for debugging purpose.48 if (pgadminServerProcess != null)49 return;50 51 let UUID = crypto.randomUUID();52 // Set the environment variables so that pgAdmin 4 server53 // starts listening on the appropriate port.54 process.env.PGADMIN_INT_PORT = serverPort;55 process.env.PGADMIN_INT_KEY = UUID;56 process.env.PGADMIN_SERVER_MODE = 'OFF';57 58 // Start Page URL59 startPageUrl = 'http://127.0.0.1:' + serverPort + '/?key=' + UUID;60 serverCheckUrl = 'http://127.0.0.1:' + serverPort + '/misc/ping?key=' + UUID;61 62 document.getElementById('loader-text-status').innerHTML = 'Starting pgAdmin 4...';63 64 // Write Python Path, pgAdmin file path and command in log file.65 misc.writeServerLog('pgAdmin Runtime Environment');66 misc.writeServerLog('--------------------------------------------------------');67 let command = path.resolve(pythonPath) + ' -s ' + path.resolve(pgadminFile);68 misc.writeServerLog('Python Path: "' + path.resolve(pythonPath) + '"');69 misc.writeServerLog('Runtime Config File: "' + path.resolve(misc.getRunTimeConfigFile()) + '"');70 misc.writeServerLog('pgAdmin Config File: "' + path.resolve(configFile) + '"');71 misc.writeServerLog('Webapp Path: "' + path.resolve(pgadminFile) + '"');72 misc.writeServerLog('pgAdmin Command: "' + command + '"');73 misc.writeServerLog('Environment: ');74 Object.keys(process.env).forEach(function (key) {75 // Below code is included only for Mac OS as default path for azure CLI76 // installation path is not included in PATH variable while spawning77 // runtime environment.78 if (platform() === 'darwin' && key === 'PATH') {79 let updated_path = process.env[key] + ':/usr/local/bin';80 process.env[key] = updated_path;81 }82 83 if (platform() === 'win32' && key.toUpperCase() === 'PATH') {84 let _libpq_path = path.join(path.dirname(path.dirname(path.resolve(pgadminFile))), 'runtime');85 process.env[key] = _libpq_path + ';' + process.env[key];86 }87 88 misc.writeServerLog(' - ' + key + ': ' + process.env[key]);89 });90 misc.writeServerLog('--------------------------------------------------------\n');91 92 // Spawn the process to start pgAdmin4 server.93 let spawnStartTime = (new Date).getTime();94 pgadminServerProcess = spawn(path.resolve(pythonPath), ['-s', path.resolve(pgadminFile)]);95 pgadminServerProcess.on('error', function (err) {96 // Log the error into the log file if process failed to launch97 misc.writeServerLog('Failed to launch pgAdmin4. Error:');98 misc.writeServerLog(err);99 });100 let spawnEndTime = (new Date).getTime();101 misc.writeServerLog('Total spawn time to start the pgAdmin4 server: ' + (spawnEndTime - spawnStartTime) / 1000 + ' Sec');102 103 pgadminServerProcess.stdout.setEncoding('utf8');104 pgadminServerProcess.stdout.on('data', (chunk) => {105 misc.writeServerLog(chunk);106 });107 108 pgadminServerProcess.stderr.setEncoding('utf8');109 pgadminServerProcess.stderr.on('data', (chunk) => {110 misc.writeServerLog(chunk);111 });112 113 // This function is used to ping the pgAdmin4 server whether it114 // it is started or not.115 function pingServer() {116 return axios.get(serverCheckUrl);117 }118 119 let connectionTimeout = misc.ConfigureStore.get('connectionTimeout', 90) * 1000;120 let currentTime = (new Date).getTime();121 let endTime = currentTime + connectionTimeout;122 let midTime1 = currentTime + (connectionTimeout / 2);123 let midTime2 = currentTime + (connectionTimeout * 2 / 3);124 let pingInProgress = false;125 126 // ping pgAdmin server every 1 second.127 let pingStartTime = (new Date).getTime();128 let intervalID = setInterval(function () {129 // If ping request is already send and response is not130 // received no need to send another request.131 if (pingInProgress)132 return;133 134 pingServer().then(() => {135 pingInProgress = false;136 document.getElementById('loader-text-status').innerHTML = 'pgAdmin 4 started';137 // Set the pgAdmin process object to misc138 misc.setProcessObject(pgadminServerProcess);139 140 clearInterval(intervalID);141 let appEndTime = (new Date).getTime();142 misc.writeServerLog('------------------------------------------');143 misc.writeServerLog('Total time taken to ping pgAdmin4 server: ' + (appEndTime - pingStartTime) / 1000 + ' Sec');144 misc.writeServerLog('------------------------------------------');145 misc.writeServerLog('Total launch time of pgAdmin4: ' + (appEndTime - appStartTime) / 1000 + ' Sec');146 misc.writeServerLog('------------------------------------------');147 launchPgAdminWindow();148 }).catch(() => {149 pingInProgress = false;150 let curTime = (new Date).getTime();151 // if the connection timeout has lapsed then throw an error152 // and stop pinging the server.153 if (curTime >= endTime) {154 clearInterval(intervalID);155 splashWindow.hide();156 157 nw.Window.open('src/html/server_error.html', {158 'frame': true,159 'width': 790,160 'height': 430,161 'position': 'center',162 'resizable': false,163 'focus': true,164 'show': true,165 });166 }167 168 if (curTime > midTime1) {169 if (curTime < midTime2) {170 document.getElementById('loader-text-status').innerHTML = 'Taking longer than usual...';171 } else {172 document.getElementById('loader-text-status').innerHTML = 'Almost there...';173 }174 } else {175 document.getElementById('loader-text-status').innerHTML = 'Waiting for pgAdmin 4 to start...';176 }177 });178 179 pingInProgress = true;180 }, 250);181}182 183// This function is used to hide the splash screen and create/launch184// new window to render pgAdmin4 page.185function launchPgAdminWindow() {186 // Create and launch new window and open pgAdmin url187 misc.writeServerLog('Application Server URL: ' + startPageUrl);188 nw.Window.open(startPageUrl, {189 'id': 'pgadmin-main',190 'icon': '../../assets/pgAdmin4.png',191 'frame': true,192 'position': 'center',193 'resizable': true,194 'min_width': 640,195 'min_height': 480,196 'width': 1024,197 'height': 768,198 'focus': true,199 'show': false,200 }, (pgadminWindow) => {201 pgAdminMainScreen = pgadminWindow;202 // Set pgAdmin4 Windows Object203 misc.setPgAdminWindowObject(pgadminWindow);204 205 // Set the zoom level stored in the config file.206 pgadminWindow.zoomLevel = misc.ConfigureStore.get('zoomLevel', 0);207 208 // Set zoom in and out events.209 misc.setZoomEvents();210 211 pgadminWindow.on('closed', function () {212 misc.cleanupAndQuitApp();213 });214 215 // set up handler for new-win-policy event.216 // Set the width and height for the new window.217 pgadminWindow.on('new-win-policy', function (frame, url, policy) {218 if (!frame) {219 let openDocsInBrowser = misc.ConfigureStore.get('openDocsInBrowser', true);220 let isDocURL = false;221 docsURLSubStrings.forEach(function (key) {222 if (url.indexOf(key) >= 0) {223 isDocURL = true;224 }225 226 });227 228 229 if (openDocsInBrowser && isDocURL) {230 // Do not open the window231 policy.ignore();232 // Open URL in the external browser.233 nw.Shell.openExternal(url);234 } else {235 policy.setNewWindowManifest({236 'icon': '../../assets/pgAdmin4.png',237 'frame': true,238 'position': 'center',239 'min_width': 640,240 'min_height': 480,241 'width': pgadminWindow.width,242 'height': pgadminWindow.height,243 });244 }245 }246 });247 248 pgadminWindow.on('loaded', function () {249 /* Make the new window opener to null as it is250 * nothing but a splash screen. We will have to make it null,251 * so that open in new browser tab will work.252 */253 pgadminWindow.window.hookConsole((method, args)=>{254 misc.writeServerLog(255 `--------------[UI ${method}]---------------${EOL}${misc.parseConsoleArgs(method, args)}${EOL}------------[UI End]----------------`);256 });257 pgadminWindow.window.opener = null;258 259 // Show new window260 pgadminWindow.show();261 pgadminWindow.focus();262 263 nativeMenu = new gui.Menu({ type: 'menubar' });264 // Create Mac Builtin Menu265 if (platform() === 'darwin') {266 nativeMenu.createMacBuiltin('pgAdmin 4');267 // Remove 'About pgAdmin 4' submenu268 nativeMenu?.items[0].submenu.removeAt(0);269 // Remove 'Close Window' submenu270 nativeMenu?.items[2].submenu.removeAt(1);271 pgAdminMainScreen.menu = nativeMenu;272 }273 274 try {275 pgAdminMainScreen.isCustomMenusAdded = false;276 let addMenuInterval = setInterval(() => {277 if (pgadminWindow?.window?.pgAdmin?.Browser?.Events && pgadminWindow?.window?.pgAdmin?.Browser?.MainMenus?.length > 0) {278 pgadminWindow.window.pgAdmin.Browser.Events.on('pgadmin:nw-enable-disable-menu-items', enableDisableMenuItem);279 pgadminWindow.window.pgAdmin.Browser.Events.on('pgadmin:nw-refresh-menu-item', refreshMenuItems);280 pgadminWindow.window.pgAdmin.Browser.Events.on('pgadmin:nw-update-checked-menu-item', updateCheckedMenuItem);281 pgadminWindow.window.pgAdmin.Browser.Events.on('pgadmin:nw-set-new-window-open-size', setNewWindowSize)282 // Add Main Menus to native menu.283 pgadminWindow.window.pgAdmin.Browser.MainMenus.forEach((menu)=> {284 addMenu(menu)285 })286 clearInterval(addMenuInterval);287 }288 }, 250)289 } catch (e) {290 console.error('Error in add native menus');291 }292 293 // Hide the splash screen294 splashWindow.hide();295 });296 297 pgadminWindow.on('blur', function () {298 misc.unregisterZoomEvents();299 });300 301 pgadminWindow.on('focus', function () {302 misc.registerZoomEvents();303 });304 });305}306 307// Get the gui object of NW.js308let gui = require('nw.gui');309let splashWindow = gui.Window.get();310 311// Enable dragging on the splash screen.312let isDragging = false;313let dragOrigin = { x: 0, y: 0 };314document.mouseleave = () => isDragging = false;315document.onmouseup = () => isDragging = false;316 317document.onmousedown = (e) => {318 isDragging = true;319 dragOrigin.x = e.x;320 dragOrigin.y = e.y;321};322 323document.onmousemove = (e) => {324 if (isDragging) {325 splashWindow.moveTo(e.screenX - dragOrigin.x, e.screenY - dragOrigin.y);326 }327};328 329// Always clear the cache before starting the application.330nw.App.clearCache();331 332let nativeMenu;333 334splashWindow.on('loaded', function () {335 // Initialize the ConfigureStore336 misc.ConfigureStore.init();337 338 let fixedPortCheck = misc.ConfigureStore.get('fixedPort', false);339 if (fixedPortCheck) {340 serverPort = misc.ConfigureStore.get('portNo');341 //Start the pgAdmin in Desktop mode.342 startDesktopMode();343 } else {344 // get the available TCP port by sending port no to 0.345 misc.getAvailablePort(0)346 .then((pythonApplicationPort) => {347 serverPort = pythonApplicationPort;348 //Start the pgAdmin in Desktop mode.349 startDesktopMode();350 })351 .catch((errCode) => {352 if (errCode === 'EADDRINUSE') {353 alert('The port specified is already in use. Please enter a free port number.');354 } else {355 alert(errCode);356 }357 });358 }359});360 361splashWindow.on('close', function () {362 misc.cleanupAndQuitApp();363});364 365function setNewWindowSize(){366 misc.setZoomLevelForAllWindows();367}368 369 370function addCommonMenus(menu) {371 let _menu = new gui.Menu();372 373 menu.menuItems.forEach((menuItem) => {374 let submenu = getSubMenu(menuItem);375 376 let _menuItem = new gui.MenuItem({377 label: menuItem.label,378 enabled: !menuItem.isDisabled,379 type: menuItem.type || 'normal',380 priority: menuItem.priority,381 ...(submenu.items.length > 0) && {382 submenu: submenu,383 },384 click: function () {385 menuItem.callback();386 },387 });388 _menu.append(_menuItem);389 });390 391 if (menu.name == 'file') {392 let runtimeMenu = getRuntimeMenu();393 _menu.append(runtimeMenu);394 }395 396 if (menu.menuItems.length == 0) {397 let _menuItem = new gui.MenuItem({398 label: 'No object selected',399 enabled: false,400 priority: 0,401 });402 _menu.append(_menuItem);403 }404 405 if (platform() == 'darwin') {406 pgAdminMainScreen.menu.insert(new gui.MenuItem({407 label: menu.label,408 name: menu.name,409 submenu: _menu,410 }), menu.index);411 } else {412 nativeMenu.append(new gui.MenuItem({413 label: menu.label,414 name: menu.name,415 submenu: _menu,416 }));417 pgAdminMainScreen.menu = nativeMenu;418 }419 420}421 422function getRuntimeMenu() {423 let subMenus = new gui.Menu();424 let rtmenudt = pgAdminMainScreen.window.pgAdmin.Browser.RUNTIME_MENUS_OPTIONS['runtime']425 let runtimeSubMenus = pgAdminMainScreen.window.pgAdmin.Browser.RUNTIME_MENUS_OPTIONS['runtime']['submenus']426 subMenus.append(new gui.MenuItem({427 label: runtimeSubMenus['configure'].label,428 enabled: runtimeSubMenus['configure'].enable,429 priority: runtimeSubMenus['configure'].priority,430 type: 'normal',431 checked: false,432 click: function () {433 // Create and launch new window and open pgAdmin url434 nw.Window.open('src/html/configure.html', {435 'frame': true,436 'width': 600,437 'height': 585,438 'position': 'center',439 'resizable': false,440 'focus': true,441 'show': true,442 });443 },444 }));445 subMenus.append(new gui.MenuItem({446 label: runtimeSubMenus['view_log'].label,447 enabled: runtimeSubMenus['view_log'].enable,448 priority: runtimeSubMenus['view_log'].priority,449 type: 'normal',450 checked: false,451 click: function () {452 // Create and launch new window and open pgAdmin url453 nw.Window.open('src/html/view_log.html', {454 'frame': true,455 'width': 790,456 'height': 425,457 'position': 'center',458 'resizable': false,459 'focus': true,460 'show': true,461 });462 },463 }));464 subMenus.append(new nw.MenuItem({ type: 'separator' }));465 subMenus.append(new gui.MenuItem({466 label: pgAdminMainScreen?.isFullscreen ? runtimeSubMenus['exit_full_screen'].label : runtimeSubMenus['enter_full_screen'].label,467 enabled: runtimeSubMenus['enter_full_screen'].enable,468 priority: runtimeSubMenus['enter_full_screen'].priority,469 type: 'normal',470 checked: false,471 key: runtimeSubMenus['enter_full_screen'].key,472 modifiers: runtimeSubMenus['enter_full_screen'].modifiers,473 click: function () {474 this.label = !pgAdminMainScreen?.isFullscreen ? runtimeSubMenus['exit_full_screen'].label : runtimeSubMenus['enter_full_screen'].label;475 misc.toggleFullScreen();476 },477 }));478 subMenus.append(new gui.MenuItem({479 label: runtimeSubMenus['actual_size'].label,480 enabled: runtimeSubMenus['actual_size'].enable,481 priority: runtimeSubMenus['actual_size'].priority,482 type: 'normal',483 checked: false,484 key: runtimeSubMenus['actual_size'].key,485 modifiers: runtimeSubMenus['actual_size'].modifiers,486 click: function () {487 misc.actualSize();488 },489 }));490 subMenus.append(new gui.MenuItem({491 label: runtimeSubMenus['zoom_in'].label,492 enabled: runtimeSubMenus['zoom_in'].enable,493 priority: runtimeSubMenus['zoom_in'].priority,494 type: 'normal',495 checked: false,496 key: runtimeSubMenus['zoom_in'].key,497 modifiers: runtimeSubMenus['zoom_in'].modifiers,498 click: function () {499 misc.zoomIn();500 },501 }));502 subMenus.append(new gui.MenuItem({503 label: runtimeSubMenus['zoom_out'].label,504 enabled: runtimeSubMenus['zoom_out'].enable,505 priority: runtimeSubMenus['zoom_out'].priority,506 type: 'normal',507 checked: false,508 key: runtimeSubMenus['zoom_out'].key,509 modifiers: runtimeSubMenus['zoom_out'].modifiers,510 click: function () {511 misc.zoomOut();512 },513 }));514 515 let runtimeMenu = new gui.MenuItem({516 label: rtmenudt.label,517 enabled: true,518 priority: rtmenudt.priority,519 type: 'normal',520 checked: false,521 submenu: subMenus,522})523 524 return runtimeMenu;525 526}527 528function getSubMenu(menuItem) {529 let submenu = new gui.Menu();530 if (menuItem.menu_items) {531 menuItem.menu_items.forEach((item) => {532 let menuType = typeof item.checked == 'boolean' ? 'checkbox' : item.type;533 submenu.append(new gui.MenuItem({534 label: item.label,535 enabled: !item.isDisabled,536 priority: item.priority,537 type: menuType,538 checked: item.checked,539 click: function () {540 if (menuType == 'checkbox') {541 pgAdminMainScreen.menu.items.forEach(el => {542 el.submenu.items.forEach((sub) => {543 if (sub.submenu?.items?.length) {544 sub.submenu.items.forEach((m) => {545 if (m.type == 'checkbox') {546 m.label == item.label ? m.checked = true : m.checked = false;547 }548 });549 }550 });551 });552 }553 item.callback();554 },555 }));556 });557 }558 return submenu;559}560 561function addMacMenu(menu) {562 if (menu.name == 'file' && platform() === 'darwin') {563 let rootMenu = nativeMenu.items[0].submenu;564 let indx = 0;565 menu.menuItems.forEach((menuItem) => {566 let submenu = getSubMenu(menuItem);567 568 rootMenu.insert(569 new gui.MenuItem({570 label: menuItem.label,571 type: menuItem.type || 'normal',572 enabled: !menuItem.isDisabled,573 priority: menuItem.priority,574 ...(submenu.items.length > 0) && {575 submenu: submenu,576 },577 click: function () {578 // Callback functions for actions579 menuItem.callback();580 },581 }), indx);582 indx++;583 });584 let runtimeMenu = getRuntimeMenu();585 rootMenu.insert(runtimeMenu, indx++);586 let separator_menu = new nw.MenuItem({ type: 'separator' });587 rootMenu.insert(separator_menu, indx);588 indx++;589 590 pgAdminMainScreen.menu = nativeMenu;591 } else {592 addCommonMenus(menu)593 }594}595 596function addOtherOsMenu(menu) {597 addCommonMenus(menu)598}599 600 601function addMenu(menu) {602 pgAdminMainScreen.isCustomMenusAdded = true;603 if (platform() === 'darwin') {604 addMacMenu(menu);605 } else {606 addOtherOsMenu(menu);607 }608 addMenuCompleted = true;609}610 611function enableDisableMenuItem(menu, menuItem) {612 if (addMenuCompleted) {613 // Enable or Disabled specific menu item614 pgAdminMainScreen.menu.items.forEach(el => {615 if (el?.label == menu?.label) {616 el.submenu.items.forEach((sub) => {617 if (sub.label == menuItem.label) {618 sub.enabled = !menuItem.isDisabled;619 }620 });621 }622 });623 }624}625 626function updateCheckedMenuItem(menuItem) {627 // check/ uncheck specific menu item628 pgAdminMainScreen.menu.items.forEach(el => {629 el.submenu.items.forEach((sub) => {630 if(sub.label == menuItem.parentMenu.label) {631 sub.submenu.items.forEach((sm)=> {632 if (sm.label == menuItem.label && sm.type == 'checkbox') {633 sm.checked = menuItem.checked634 }635 })636 } else if (sub.label == menuItem.label && type == 'checkbox') {637 sub.checked = menuItem.checked638 }639 });640 });641}642 643function refreshMenuItems(menu) {644 // Add menu item/option in specific menu.645 pgAdminMainScreen.menu.items.forEach(el => {646 if (el.label == menu.label) {647 let totalSubItems = el.submenu.items.length;648 649 // Remove exisitng menu options to add new options.650 for (let i = 0; i < totalSubItems; i++) {651 el.submenu.removeAt(0);652 }653 menu.menuItems.forEach((item) => {654 655 let submenu = new gui.Menu();656 if (item.menu_items) {657 item.menu_items.forEach((subItem) => {658 submenu.append(new gui.MenuItem({659 label: subItem.label,660 enabled: !subItem.isDisabled,661 priority: subItem.priority,662 type: [true, false].includes(subItem.checked) ? 'checkbox' : 'normal',663 checked: subItem.checked,664 click: function () {665 subItem.callback();666 },667 }));668 });669 }670 let _menuItem = new gui.MenuItem({671 label: item.label,672 enabled: !item.isDisabled,673 priority: item.priority,674 type: item.type,675 ...(submenu.items.length > 0) && {676 submenu: submenu,677 },678 click: function () {679 item.callback();680 },681 });682 683 el.submenu.append(_menuItem);684 });685 }686 });687 688}689 