// Modules to control application life and create native browser window const {app, BrowserWindow, Tray, nativeImage, Menu, dialog} = require('electron'); const {ipcMain} = require('electron'); const Constants = require('../src/constants'); const path = require('path'); const url = require('url'); require('electron-debug/index')(); const os = require('os'); const helpers = require('../src/helpers'); const fs = require('fs'); const unzip = require('unzipper'); const AutoLaunch = require('auto-launch'); require.extensions['.sh'] = function (module, filename) { module.exports = fs.readFileSync(filename, 'utf8'); }; const detectScript = require('./detect_linux.sh'); // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. let trayContextMenu; let mainWindow; let mainWindowTray; let mountedData = {}; let mountedLocations = []; let expectedUnmount = {}; let launchHidden = false; let firstMountCheck = true; let manualMountDetection = {}; let platformOverride; let isShutdown = false; let isQuiting = false; let isInstalling = false; app.on('before-quit', function () { isQuiting = true; }); function closeApplication() { if (!isShutdown) { isShutdown = true; if (mainWindowTray) { mainWindowTray.destroy(); } app.quit(); } } function setWindowVisibility(show) { if (show) { mainWindow.show(); if (os.platform() === 'darwin') { app.dock.show(); } if (mainWindow.isMinimized()) { mainWindow.restore(); } mainWindow.focus(); } else { mainWindow.hide(); if (os.platform() === 'darwin') { app.dock.hide(); } } if (trayContextMenu && mainWindowTray) { trayContextMenu.items[0].checked = show; mainWindowTray.setContextMenu(trayContextMenu) } } const unmountAllDrives = () => { // Reset mount states for (const provider of Constants.PROVIDER_LIST) { clearManualMountDetection(provider); expectedUnmount[provider] = true; } // Unmount all items for (const i in mountedLocations) { const data = mountedData[mountedLocations[i]]; helpers.stopMountProcessSync(data.Version, data.Provider); } mountedLocations = []; mountedData = {}; }; function createWindow() { loadUiSettings(); let extra = {}; if (os.platform() === 'linux') { extra = { icon: path.join(__dirname, '../', 'icon.png'), } } // Create the browser window. const height = (process.env.ELECTRON_START_URL || (os.platform() === 'darwin') ? 294 : 274) - ((os.platform() === 'win32') ? 0 : 20); mainWindow = new BrowserWindow({ width: 428 + ((os.platform() === 'win32') ? 0 : (os.platform() === 'darwin') ? 150 : 160), height: height, fullscreen: false, resizable: false, show: !launchHidden, title: 'Repertory UI', ...extra, webPreferences: { webSecurity: !process.env.ELECTRON_START_URL } }); if ((os.platform() === 'darwin') && launchHidden) { app.dock.hide(); } // and load the index.html of the app. const startUrl = process.env.ELECTRON_START_URL || url.format({ pathname: path.join(__dirname, '../build/index.html'), protocol: 'file:', slashes: true }); mainWindow.on('close', function (event) { if (!isQuiting) { event.preventDefault(); if (mainWindow.isVisible()) { setWindowVisibility(false); } event.returnValue = false; } }); mainWindow.on('closed', function () { // Dereference the window object, usually you would store windows // in an array if your app supports multi windows, this is the time // when you should delete the corresponding element. mainWindow = null; unmountAllDrives(); }); const appPath = (os.platform() === 'win32') ? path.resolve(path.join(app.getAppPath(), '..\\..\\repertory-ui.exe')) : (os.platform() === 'darwin') ? path.resolve(path.join(path.dirname(app.getAppPath()), '../MacOS/repertory-ui')) : process.env.APPIMAGE; const autoLauncher = new AutoLaunch({ name: 'Repertory UI', path: appPath, }); trayContextMenu = Menu.buildFromTemplate([ { label: 'Visible', type: 'checkbox', click(item) { setWindowVisibility(item.checked); }, checked: !launchHidden, }, { label: 'Auto-start', type: 'checkbox', click(item) { if (item.checked) { autoLauncher.enable(); } else { autoLauncher.disable(); } } }, { type: 'separator' }, { label: 'Launch Hidden', type: 'checkbox', click(item) { launchHidden = !!item.checked; saveUiSettings(); }, checked: launchHidden, }, { type: 'separator' }, { label: 'Exit and Unmount', click() { closeApplication(); } } ]); const image = nativeImage.createFromPath(path.join(__dirname, (os.platform() === 'darwin') ? '../build/logo_mac.png' : '../build/logo.png')); mainWindowTray = new Tray(image); autoLauncher .isEnabled() .then((enabled) => { trayContextMenu.items[1].checked = enabled; mainWindowTray.setToolTip('Repertory UI'); mainWindowTray.setContextMenu(trayContextMenu) }) .catch(() => { closeApplication(); }); mainWindow.loadURL(startUrl); } let instanceLock = app.requestSingleInstanceLock(); const configurePrimaryApp = () => { app.on('second-instance', () => { if (!isInstalling && mainWindow) { setWindowVisibility(true); } }); app.on('ready', createWindow); app.on('window-all-closed', () => { closeApplication(); }); }; if (!instanceLock) { setTimeout(() => { if ((instanceLock = app.requestSingleInstanceLock())) { configurePrimaryApp(); } else { closeApplication(); } }, 3000); } else { configurePrimaryApp(); } const clearManualMountDetection = (provider) => { if (manualMountDetection[provider]) { clearInterval(manualMountDetection[provider]); delete manualMountDetection[provider]; } }; const loadUiSettings = () => { const settingFile = path.join(helpers.resolvePath(Constants.DATA_LOCATIONS[os.platform()]), 'ui.json'); try { if (fs.statSync(settingFile).isFile()) { const settings = JSON.parse(fs.readFileSync(settingFile, 'utf8')); launchHidden = settings.launch_hidden; platformOverride = settings.platform_override; } } catch (e) { } }; const monitorMount = (sender, provider, version, pid, location) => { manualMountDetection[provider] = setInterval(() => { helpers .detectRepertoryMounts(version) .then(result => { if (result[provider].PID !== pid) { if (result[provider].PID === -1) { clearManualMountDetection(provider); sender.send(Constants.IPC_Unmount_Drive_Reply, { data: { Expected: expectedUnmount[provider], Location: location, Provider: provider, Error: Error(provider + ' Unmounted').toString(), Success: false, } }); } else { pid = result[provider].PID; } } }) .catch(e => { console.log(e); }); },6000); }; const saveUiSettings = () => { const settingFile = path.join(helpers.getDataDirectory(), 'ui.json'); try { fs.writeFileSync(settingFile, JSON.stringify({ launch_hidden: launchHidden, platform_override: platformOverride, }), 'utf-8'); } catch (e) { } }; const standardIPCReply = (event, channel, data, error) => { if (mainWindow) { event.sender.send(channel, { data: { ...data, Error: error instanceof Error ? error.toString() : error, Success: !error, } }); } }; ipcMain.on(Constants.IPC_Browse_Directory + '_sync', (event, data) => { dialog.showOpenDialog(mainWindow, { defaultPath: data.Location, properties: ['openDirectory'], title: data.Title, }, (filePaths) => { if (filePaths && (filePaths.length > 0)) { event.returnValue = filePaths[0]; } else { event.returnValue = ''; } }); }); ipcMain.on(Constants.IPC_Check_Daemon_Version, (event, data) => { helpers .checkDaemonVersion(data.Version, data.Provider) .then(code => { standardIPCReply(event, Constants.IPC_Check_Daemon_Version_Reply, { Valid: (code === 0), Code: code, }); }) .catch(e => { standardIPCReply(event, Constants.IPC_Check_Daemon_Version_Reply, { Valid: false, }, e); }); }); ipcMain.on(Constants.IPC_Check_Daemon_Version + '_sync', (event, data) => { helpers .checkDaemonVersion(data.Version, data.Provider) .then(code => { event.returnValue = { data: { Success: true, Valid: (code === 0), Code: code, }, }; }) .catch(e => { event.returnValue = { data: { Error: e.toString(), Success: false, Valid: false }, }; }); }); ipcMain.on(Constants.IPC_Check_Dependency_Installed, (event, data) => { try { const exists = fs.lstatSync(data.File).isFile(); standardIPCReply(event, Constants.IPC_Check_Dependency_Installed_Reply, { data: { Exists: exists, }, }); } catch (e) { standardIPCReply(event, Constants.IPC_Check_Dependency_Installed_Reply, { data : { Exists: false, }, }); } }); ipcMain.on(Constants.IPC_Check_Dependency_Installed + '_sync', (event, data) => { try { const ls = fs.lstatSync(data.File); event.returnValue = { data: { Exists: ls.isFile() || ls.isSymbolicLink(), }, }; } catch (e) { event.returnValue = { data: { Exists: false }, }; } }); ipcMain.on(Constants.IPC_Check_Installed, (event, data) => { const destination = path.join(helpers.getDataDirectory(), data.Version); helpers .getMissingDependencies(data.Dependencies) .then((dependencies) => { let exists = false; try { exists = fs.existsSync(destination) && fs.lstatSync(destination).isDirectory(); } catch (e) { } standardIPCReply(event, Constants.IPC_Check_Installed_Reply, { Dependencies: dependencies, Exists: exists, Version: data.Version, }); }).catch(error => { standardIPCReply(event, Constants.IPC_Check_Installed_Reply, { Dependencies: [], Version: data.Version, }, error); }); }); ipcMain.on(Constants.IPC_Check_Mount_Location + '_sync', (event, data) => { let response = { Success: true, Error: '' }; try { if (fs.existsSync(data.Location) && fs.statSync(data.Location).isDirectory()) { if (fs.readdirSync(data.Location).length !== 0) { response.Success = false; response.Error = 'Directory not empty: ' + data.Location; } } else { response.Success = false; response.Error = 'Directory not found: ' + data.Location; } } catch (e) { response.Success = false; response.Error = e.toString(); } event.returnValue = response; }); ipcMain.on(Constants.IPC_Delete_File, (event, data) => { try { if (fs.existsSync(data.FilePath)) { fs.unlinkSync(data.FilePath); } } catch (e) { } }); ipcMain.on(Constants.IPC_Delete_File + '_sync', (event, data) => { try { if (fs.existsSync(data.FilePath)) { fs.unlinkSync(data.FilePath); } event.returnValue = { data: true, }; } catch (e) { event.returnValue = { data: false, }; } }); ipcMain.on(Constants.IPC_Detect_Mounts, (event, data) => { let driveLetters = {}; for (const provider of Constants.PROVIDER_LIST) { driveLetters[provider] = []; } const grabDriveLetters = (locations) => { for (let i = 'c'.charCodeAt(0); i <= 'z'.charCodeAt(0); i++) { const drive = (String.fromCharCode(i) + ':').toUpperCase(); let driveInUse; if (Object.keys(locations).length > 0) { for (const provider of Constants.PROVIDER_LIST) { driveInUse = locations[provider].startsWith(drive); if (driveInUse) break; } } if (!driveInUse) { try { if (!fs.existsSync(drive)) { for (const provider of Constants.PROVIDER_LIST) { driveLetters[provider].push(drive); } } } catch (e) { } } } if (Object.keys(locations).length > 0) { for (const provider of Constants.PROVIDER_LIST) { if (locations[provider].length > 0) { if (!driveLetters[provider].find((driveLetter) => { return driveLetter === locations[provider]; })) { driveLetters[provider].push(locations[provider]); } } } } }; const setImage = (locations) => { let driveInUse; if (Object.keys(locations).length > 0) { for (const provider of Constants.PROVIDER_LIST) { driveInUse = locations[provider].length > 0; if (driveInUse) break; } } let image; if (driveInUse) { image = nativeImage.createFromPath(path.join(__dirname, os.platform() === 'darwin' ? '../build/logo_both_mac.png' : '../build/logo_both.png')); } else { image = nativeImage.createFromPath(path.join(__dirname, os.platform() === 'darwin' ? '../build/logo_mac.png' : '../build/logo.png')); } mainWindowTray.setImage(image); }; helpers .detectRepertoryMounts(data.Version) .then((results) => { let storageData = {}; let locations = {}; for (const provider of Constants.PROVIDER_LIST) { storageData[provider] = results[provider] ? results[provider] : { Active: false, Location: '', PID: -1, }; locations[provider] = storageData[provider].Location; if (storageData[provider].PID !== -1) { expectedUnmount[provider] = false; if (firstMountCheck) { monitorMount(event.sender, provider, data.Version, storageData[provider].PID, storageData[provider].Location); } } } if (os.platform() === 'win32') { grabDriveLetters(locations); } setImage(locations); if (firstMountCheck) { firstMountCheck = false; } standardIPCReply(event, Constants.IPC_Detect_Mounts_Reply, { DriveLetters: driveLetters, Locations: locations, }); }) .catch(error => { if (os.platform() === 'win32') { grabDriveLetters({}); } setImage({}); standardIPCReply(event, Constants.IPC_Detect_Mounts_Reply, { DriveLetters: driveLetters, }, error); }); }); ipcMain.on(Constants.IPC_Download_File, (event, data) => { const destination = path.join(helpers.getDataDirectory(), data.Filename); helpers.downloadFile(data.URL, destination, (progress) => { standardIPCReply(event, Constants.IPC_Download_File_Progress, { Destination: destination, Progress: progress, URL: data.URL, }); }, error => { standardIPCReply(event, Constants.IPC_Download_File_Complete, { Destination: destination, URL: data.URL, }, error); }); }); ipcMain.on(Constants.IPC_Extract_Release, (event, data) => { const destination = path.join(helpers.getDataDirectory(), data.Version); helpers.removeDirectoryRecursively(destination); helpers.mkDirByPathSync(destination); const stream = fs.createReadStream(data.Source); stream .pipe(unzip.Extract({ path: destination })) .on('error', error => { try { helpers.removeDirectoryRecursively(destination); } catch (e) { } stream.close(); standardIPCReply(event, Constants.IPC_Extract_Release_Complete, { Source: data.Source, }, error); }) .on('finish', () => { stream.close(); if (os.platform() !== 'win32') { helpers .executeAndWait("chmod +x \"" + path.join(destination, 'repertory') + "\"") .then(() => { standardIPCReply(event, Constants.IPC_Extract_Release_Complete, { Source: data.Source, }); }) .catch(error => { standardIPCReply(event, Constants.IPC_Extract_Release_Complete, { Source: data.Source, }, error); }) } else { standardIPCReply(event, Constants.IPC_Extract_Release_Complete, { Source: data.Source, }); } }); }); ipcMain.on(Constants.IPC_Get_Config, (event, data) => { helpers .getConfig(data.Version, data.Provider) .then((data) => { if (data.Code === 0) { standardIPCReply(event, Constants.IPC_Get_Config_Reply, { Config: data.Data, }); } else { standardIPCReply(event, Constants.IPC_Get_Config_Reply, {}, data.Code); } }) .catch(error => { standardIPCReply(event, Constants.IPC_Get_Config_Reply, {}, error); }); }); ipcMain.on(Constants.IPC_Get_Config_Template, (event, data) => { helpers .getConfigTemplate(data.Version, data.Provider) .then((data) => { standardIPCReply(event, Constants.IPC_Get_Config_Template_Reply, { Template: data, }); }) .catch(error => { standardIPCReply(event, Constants.IPC_Get_Config_Template_Reply, {}, error); }); }); ipcMain.on(Constants.IPC_Get_Platform, (event) => { const sendResponse = (appPlatform, platform) => { event.sender.send(Constants.IPC_Get_Platform_Reply, { AppPlatform: appPlatform, Platform: platform, }); }; const platform = os.platform(); if (platform === 'linux') { if (platformOverride && (platformOverride.trim().length > 0)) { sendResponse(platformOverride.trim(), 'linux'); } else { const scriptFile = path.join(os.tmpdir(), 'repertory_detect_linux.sh'); fs.writeFileSync(scriptFile, detectScript); helpers .executeScript(scriptFile) .then(data => { let appPlatform = data.replace(/(\r\n|\n|\r)/gm, ""); if (appPlatform === 'unknown') { helpers .downloadFile(Constants.LINUX_DETECT_SCRIPT_URL, scriptFile, null, err => { if (err) { sendResponse(appPlatform, platform); } else { helpers .executeScript(scriptFile) .then(data => { appPlatform = data.replace(/(\r\n|\n|\r)/gm, ""); sendResponse(appPlatform, platform); }) .catch(() => { sendResponse(appPlatform, platform); }); } }); } else { sendResponse(appPlatform, platform); } }) .catch(() => { sendResponse(platform, platform); }); } } else { sendResponse(platform, platform); } }); ipcMain.on(Constants.IPC_Get_State, event => { helpers.mkDirByPathSync(helpers.getDataDirectory()); const configFile = path.join(helpers.getDataDirectory(), 'settings.json'); if (fs.existsSync(configFile)) { event.sender.send(Constants.IPC_Get_State_Reply, { data: JSON.parse(fs.readFileSync(configFile, 'utf8')), }); } else { event.sender.send(Constants.IPC_Get_State_Reply, { data: null, }); } }); ipcMain.on(Constants.IPC_Install_Dependency, (event, data) => { if (data.Source.toLowerCase().endsWith('.dmg')) { helpers .executeAsync('open', ['-a', 'Finder', '-W', data.Source]) .then(() => { standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, { Source: data.Source, URL: data.URL, }); }) .catch(error=> { standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, { Source: data.Source, URL: data.URL, }, error); }); } else { const execInstall = () => { helpers .executeAndWait(data.Source) .then(() => { standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, { Source: data.Source, URL: data.URL, }); }) .catch(error => { standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, { Source: data.Source, URL: data.URL, }, error); }); }; if (data.IsWinFSP) { helpers .performWindowsUninstall(["WinFsp 2019.1", "WinFsp 2019.2", "WinFsp 2019.3 B1", "WinFsp 2019.3 B2"]) .then(uninstalled => { if (uninstalled) { standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, { RebootRequired: true, Source: data.Source, URL: data.URL, }); } else { execInstall(); } }) .catch(error => { standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, { Source: data.Source, URL: data.URL, }, error); }); } else { execInstall(); } } }); ipcMain.on(Constants.IPC_Install_Upgrade, (event, data) => { let allowSkipVerification = true; unmountAllDrives(); let tempSig; let tempPub; const cleanupFiles = () => { try { if (tempSig) { fs.unlinkSync(tempSig); } if (tempPub) { fs.unlinkSync(tempPub); } } catch (e) { } }; const errorHandler = err => { cleanupFiles(); standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { AllowSkipVerification: allowSkipVerification, Source: data.Source, }, err); }; // TODO Enable verification in 1.0.4 const hasSignature = false;//!data.SkipVerification && data.Signature && (data.Signature.length > 0); const hasHash = false;//!data.SkipVerification && data.Sha256 && (data.Sha256.length > 0); if (hasSignature) { try { const files = helpers.createSignatureFiles(data.Signature, Constants.DEV_PUBLIC_KEY); tempPub = files.PublicKeyFile; tempSig = files.SignatureFile; } catch (e) { errorHandler(e); return; } } let command; let args; const platform = os.platform(); if (platform === 'win32') { command = data.Source; } else if (platform === 'darwin') { command = 'open'; args = ['-a', 'Finder', data.Source]; } else if (platform === 'linux') { try { command = data.Source; fs.chmodSync(command, '750'); } catch (e) { errorHandler(e); } } else { errorHandler(Error('Platform not supported: ' + os.platform())); } if (command) { const executeInstall = () => { isInstalling = true; helpers .executeAsync(command, args) .then(() => { cleanupFiles(); standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply) }) .catch(error => { isInstalling = false; errorHandler(error); }); }; if (hasSignature) { helpers .verifySignature(data.Source, tempSig, tempPub) .then(() => { executeInstall(); }) .catch(() => { errorHandler(Error('Failed to verify installation package signature')); }); } else if (hasHash) { helpers .verifyHash(data.Source, data.Sha256) .then(()=> { executeInstall(); }) .catch(() => { errorHandler(Error('Failed to verify installation package hash')); }); } else { if (platform === 'darwin') { setTimeout(executeInstall, 3000); } else { executeInstall(); } } } else { errorHandler(Error('Unsupported upgrade: ' + data.Source)); } }); ipcMain.on(Constants.IPC_Mount_Drive, (event, data) => { expectedUnmount[data.Provider] = false; if (mountedLocations.indexOf(data.Location) !== -1) { console.log(data.Provider + ' already mounted: ' + data.Location); } else { mountedLocations.push(data.Location); mountedData[data.Location] = { Version: data.Version, Provider: data.Provider, }; const errorHandler = (pid, error) => { if (mountedLocations.indexOf(data.Location) !== -1) { mountedLocations.splice(mountedLocations.indexOf(data.Location), 1); delete mountedData[data.Location]; } standardIPCReply(event, Constants.IPC_Unmount_Drive_Reply, { Expected: expectedUnmount[data.Provider], Location: data.Location, Provider: data.Provider, }, error || Error(data.Provider + ' Unmounted')); }; helpers .executeMount(data.Version, data.Provider, data.Location, data.NoConsoleSupported, (error, pid) => { errorHandler(pid, error); }) .then(() => { standardIPCReply(event, Constants.IPC_Mount_Drive_Reply, { Provider: data.Provider, }); }) .catch(error => { errorHandler(-1, error); }); } }); ipcMain.on(Constants.IPC_Reboot_System, () => { if (os.platform() === 'win32') { helpers.executeAsync('shutdown.exe', ['/r', '/t', '30']); } closeApplication(); }); ipcMain.on(Constants.IPC_Save_State, (event, data) => { helpers.mkDirByPathSync(helpers.getDataDirectory()); const configFile = path.join(helpers.getDataDirectory(), 'settings.json'); fs.writeFileSync(configFile, JSON.stringify(data.State), 'utf8'); }); ipcMain.on(Constants.IPC_Set_Config_Values, (event, data) => { const setConfigValue = (i) => { if (i < data.Items.length) { helpers .setConfigValue(data.Items[i].Name, data.Items[i].Value, data.Provider, data.Version) .then(() => { setConfigValue(++i); }) .catch(() => { setConfigValue(++i); }); } else { standardIPCReply(event, Constants.IPC_Set_Config_Values_Reply, {}); } }; setConfigValue(0); }); ipcMain.on(Constants.IPC_Set_Linux_AppPlatform, (event, data) => { platformOverride = data.AppPlatform; saveUiSettings(); event.returnValue = true; }); ipcMain.on(Constants.IPC_Shutdown, () => { closeApplication(); }); ipcMain.on(Constants.IPC_Show_Window, () => { setWindowVisibility(true); }); ipcMain.on(Constants.IPC_Show_Window + '_sync', event => { setWindowVisibility(true); event.returnValue = true; }); ipcMain.on(Constants.IPC_Test_Release, (event, data) => { helpers .testRepertoryBinary(data.Version) .then(() => { standardIPCReply(event, Constants.IPC_Test_Release_Reply, {}); }) .catch(error => { standardIPCReply(event, Constants.IPC_Test_Release_Reply, {}, error); }); }); ipcMain.on(Constants.IPC_Unmount_All_Drives, (event, data) => { unmountAllDrives(); standardIPCReply(event, Constants.IPC_Unmount_All_Drives_Reply); }); ipcMain.on(Constants.IPC_Unmount_Drive, (event, data) => { clearManualMountDetection(data.Provider); expectedUnmount[data.Provider] = true; helpers .stopMountProcess(data.Version, data.Provider) .then(result => { console.log(result); }) .catch(e => { console.log(e); }); });