const { app, Tray, Menu, dialog, Notification, powerMonitor, BrowserWindow } = require('electron') const version = require('./package.json').version; const child_process = require('child_process'); const fs = require('fs'); setInterval(() => { if (global.openWindow !== null) { global.openWindow.webContents.send("config", JSON.stringify(config)) global.openWindow.webContents.send("watchers", JSON.stringify({ fileWatchersPurgeable, fileWatchers, fileWatchersFailed: global.trackingFailures })) } }, 1000); global.openWindow = null; function createWindow(page, width, height) { global.openWindow = new BrowserWindow({ id: "window", alwaysOnTop: true, backgroundColor: "#333333", skipTaskbar: true, autoHideMenuBar: true, enableLargerThanScreen: true, width: width ?? 800, height: height ?? 600, resizable: false, maximizable: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); openWindow.loadFile(page); openWindow.setMenuBarVisibility(false); if (global.linux) openWindow.setMenu(null); openWindow.on('close', () => { global.openWindow = null; }); } global.forceDebug = process.argv.includes("--verbose"); global.linux = require('os').platform() === "linux"; let configPath = app.getPath("userData") + "/config.json"; if (!fs.existsSync(configPath)) { fs.writeFileSync(configPath, JSON.stringify({ disk: linux ? "/dev/vda3" : "/dev/disk3s8", directory: linux ? require('os').homedir() + "/.tempdisk" : "/Volumes/Earth pony", timeout: 600, lastAccessTime: null, lastLockTime: null })); } global.config = config = JSON.parse(fs.readFileSync(configPath).toString()); global.lastKnownConfig = fs.readFileSync(configPath).toString(); try { if (!child_process.execSync("df").toString().trim().includes(global.config.directory)) { fs.rmdirSync(global.config.directory); } } catch (e) {} setInterval(() => { config.lastLockTime = global.lockDate; config.lastAccessTime = global.lastAccess; if (JSON.stringify(config) !== lastKnownConfig) { fs.writeFileSync(configPath, JSON.stringify(config)); global.lastKnownConfig = JSON.stringify(config); } }, 5000); global.trayIcon = null; function updateTrayIcon(icon) { if (icon !== trayIcon) { try { tray.setImage(icon); trayIcon = icon; } catch (e) { console.error(e); } } } process.chdir(__dirname); powerMonitor.on('lock-screen', () => { if (diskStatus === 3) { global.commands.lock(); global.notifications.lock("lock"); } }) global.debugMenu = global.forceDebug; global.directory = global.config.directory; global.disk = global.config.disk global.time = global.config.timeout; global.oldDiskStatus = null; global.processes = []; global.commands = { lock: () => { if (linux) { try { child_process.execSync("sudo umount " + require('os').userInfo().homedir + "/.tempdisk"); } catch (e) {} try { child_process.execSync("rmdir " + require('os').userInfo().homedir + "/.tempdisk"); } catch (e) {} child_process.execSync("sudo cryptsetup luksClose /dev/mapper/tempdisk"); } else { child_process.execSync("diskutil unmount force " + disk); } }, unlock: () => { if (linux) { let password = child_process.execSync("zenity --password --width 600 --title \"Enter a password to unlock the disk\" --ok-label \"Unlock\" --cancel-label \"Cancel\"").toString().trim(); child_process.execSync("echo \"" + Buffer.from(password).toString("base64") + "\" | base64 -d | sudo cryptsetup luksOpen " + disk + " tempdisk"); child_process.execSync("mkdir " + require('os').userInfo().homedir + "/.tempdisk"); child_process.execSync("sudo mount /dev/mapper/tempdisk " + require('os').userInfo().homedir + "/.tempdisk"); } else { child_process.execSync("automator ./decryptDisk.workflow"); } } } setInterval(async () => { if (diskStatus === 3) { try { let execFile = require('util').promisify(child_process.execFile); let parts = (await execFile("lsof", [ directory ])).stdout.trim().split("\n").map(i => i.replace(/ +/g, " ").split(" ")[1]); parts.shift(); let fullParts = []; for (let part of parts) { fullParts.push({ pid: part, name: (await execFile("ps", [ "-p", part, "-o", "comm=" ])).stdout.trim().replace(/(.*)\/(.*).app\/(.*)/i, "$2") }); } global.processes = fullParts; } catch (e) { global.processes = []; } } }, 1000); global.TempDisk = { UNLOADED: 0, LOADING: 1, LOCKED: 2, UNLOCKED: 3 } global.notifications = { unlock: () => { let notification = new Notification({ title: "Disk is unlocked", silent: true, body: "The disk has been unlocked and is now usable.", actions: [ { type: "button", text: "Open in Finder" } ] }); notification.show(); notification.on('action', () => { global.actions.open(); }) }, lock: (reason) => { let notification = new Notification({ title: "Disk is locked", silent: true, body: "The disk has been locked" + (reason === "inactivity" ? " because of inactivity" : (reason === "lock" ? " because the session was locked" : "")) + ".", actions: [ { type: "button", text: "Unlock" } ] }); notification.show(); notification.on('action', () => { global.actions.unlockDisk(); }) } } global.diskName = linux ? disk : child_process.execFileSync("diskutil", [ "info", disk ]).toString().trim().split("\n").map(i => i.trim()).filter(i => i.startsWith("Volume Name: "))[0].replace(/ +/g, " ").split(": ")[1] function secondsToHMS(input) { if (input > 3600) { let hours = Math.floor(input / 3600) let minutes = Math.floor((input - hours * 60) / 60); let seconds = input - minutes * 60; if (minutes < 10) { minutes = "0" + minutes; } if (seconds < 10) { seconds = "0" + seconds; } if (isNaN(seconds) || isNaN(minutes) || isNaN(hours)) return secondsToHMS(global.time); return hours + ":" + minutes + ":" + seconds; } else { let minutes = Math.floor(input / 60); let seconds = input - minutes * 60; if (seconds < 10) { seconds = "0" + seconds; } if (isNaN(seconds) || isNaN(minutes)) return secondsToHMS(global.time); return minutes + ":" + seconds; } } function timeAgo(time) { if (!isNaN(parseInt(time))) { time = new Date(time).getTime(); } let periods = ["second", "minute", "hour", "day", "week", "month", "year", "age"]; let lengths = ["60", "60", "24", "7", "4.35", "12", "100"]; let now = new Date().getTime(); let difference = Math.round((now - time) / 1000); let tense; let period; if (difference <= 10 && difference >= 0) { tray.setTitle("now"); return "now"; } else if (difference > 0) { tense = "ago"; } else { tense = "later"; } let j; for (j = 0; difference >= lengths[j] && j < lengths.length - 1; j++) { difference /= lengths[j]; } difference = Math.round(difference); period = periods[j]; if (period === "second") { tray.setTitle(`${difference} ${period}${difference > 1 ? "s" : ""} ${tense}`); } return `${difference} ${period}${difference > 1 ? "s" : ""} ${tense}`; } function secondsToMinutes(seconds) { if (seconds < 60) { return "less than a minute"; } else if (seconds < 120) { return "a minute"; } else { return Math.round(seconds / 60) + " minutes"; } } global.actions = { tracking: () => { createWindow("tracking.html", 400); }, devtools: () => { if (global.openWindow) { openWindow.toggleDevTools(); } }, lockDisk: () => { oldDiskStatus = diskStatus; diskStatus = TempDisk.LOADING; updateTrayIcon('./icon/loading/16x16Template@2x.png'); let add = ""; if (global.processes.length > 0) { if (global.processes.length > 1) { add = "\n\nThe following apps are currently using the disk:\n- " + global.processes.map(i => i.name).join("\n- ") + "\nLocking the disk may cause errors in some or all of these apps." } else { add = "\n\n" + global.processes[0].name + " is currently using the disk, locking the disk may cause errors in this app."; } } dialog.showMessageBox({ message: "Are you sure you want to lock the disk?", detail: "Locking the disk manually will make it unreadable for other apps until you unlock it again." + add, buttons: [ "Lock", "Cancel" ], defaultId: 0, type: "warning" }).then((result) => { if (result.response === 0) { global.commands.lock(); global.notifications.lock(); } else { diskStatus = oldDiskStatus; } }) }, reveal: () => { child_process.execFile("open", [ "-R", directory ]); }, open: () => { if (linux) { child_process.execFile("xdg-open", [ directory ]); } else { child_process.execFile("open", [ directory ]); } }, config: () => { if (linux) { child_process.execFile("xdg-open", [ configPath ]); } else { child_process.execFile("open", [ configPath ]); } }, unlockDisk: () => { oldDiskStatus = diskStatus; diskStatus = TempDisk.LOADING; updateTrayIcon('./icon/loading/16x16Template@2x.png'); fs.writeFileSync("./decryptDisk.workflow/Contents/document.wflow", fs.readFileSync("./decryptDisk.workflow/Contents/document.pre.wflow").toString().replace("%VOLUME%", disk).replace("%NAME%", diskName)); try { global.commands.unlock(); diskStatus = oldDiskStatus; global.lastAccess = new Date(); } catch (e) { dialog.showMessageBox({ message: "Failed to unlock disk", detail: "An error occurred while unlocking this disk, maybe your password is incorrect? Please try again later.", buttons: [ "Close" ], type: "error" }) console.error(e); fs.writeFileSync(require('os').homedir() + "/.DiskUnlockFailure.txt", e.stack); diskStatus = oldDiskStatus; } }, quit: () => { oldDiskStatus = diskStatus; diskStatus = TempDisk.LOADING; updateTrayIcon('./icon/loading/16x16Template@2x.png'); dialog.showMessageBox({ message: "Are you sure you want to quit TempDisk?", detail: "If you quit TempDisk, your disk will be unprotected as it won't be unmounted automatically when unused anymore. This may not be what you want.", buttons: [ "Quit", "Cancel" ], defaultId: 0, type: "warning", checkboxLabel: "Lock the disk before quitting", checkboxChecked: oldDiskStatus === 3 }).then((result) => { if (result.response === 0) { if (result.checkboxChecked && oldDiskStatus === 3) { global.commands.lock(); } app.exit(); } else { diskStatus = oldDiskStatus; } }) } } global.diskStatus = TempDisk.UNLOADED; global.lastKnownMenu = null; global.updateMenu = () => { switch (diskStatus) { case 0: updateTrayIcon('./icon/unknown/16x16Template@2x.png'); tray.setToolTip("TempDisk"); tray.setTitle(""); if (global.openWindow !== null) { global.openWindow.destroy(); global.openWindow = null; } break; case 1: updateTrayIcon('./icon/loading/16x16Template@2x.png'); tray.setToolTip("TempDisk\nLoading"); tray.setTitle(""); break; case 2: updateTrayIcon('./icon/locked/16x16Template@2x.png'); tray.setToolTip("TempDisk\nLocked"); tray.setTitle(""); if (global.openWindow !== null) { global.openWindow.destroy(); global.openWindow = null; } break; case 3: updateTrayIcon('./icon/unlocked/16x16Template@2x.png'); tray.setToolTip("TempDisk\nUnlocked"); if (processes.length > 0) { tray.setTitle(processes.length + " app" + (processes.length > 1 ? "s" : ""), { fontType: "monospacedDigit" }); } else { tray.setTitle(secondsToHMS(global.left), { fontType: "monospacedDigit" }); } break; } let template = [ { label: 'TempDisk ' + version + (debugMenu ? " (" + require('os').platform() +")" : ""), icon: "./icon/app/16x16@2x.png", type: 'normal', enabled: false }, { type: 'separator' }, { label: diskStatus === 0 ? 'No information available' : diskStatus === 1 ? 'Loading...' : diskStatus === 2 ? 'Disk is locked' : 'Disk is unlocked', type: 'normal', enabled: false }, ]; if (diskStatus === 3) { if (global.left) { if (global.processes.length > 0) { template.push({ label: 'Not locking automatically', type: 'normal', enabled: false }) } else { template.push({ label: 'Locking disk in ' + secondsToMinutes(global.left), type: 'normal', enabled: false }) } } else { template.push({ label: '-', type: 'normal', enabled: false }) } if (global.processes.length > 0) { template.push({ type: 'separator' }); template.push({ label: 'App' + (global.processes.length > 1 ? 's' : '') + ' using the disk:', enabled: false }); for (let proc of processes) { template.push({ label: ' ' + proc.name + (debugMenu ? ' [' + proc.pid + ']' : ''), enabled: false }); } } } if (diskStatus === 2) { if (global.lockDate) { template.push({ label: 'Locked ' + timeAgo(global.lockDate.getTime()), type: 'normal', enabled: false }) } else { template.push({ label: '-', type: 'normal', enabled: false }) } } template.push(...[ { type: 'separator' }, { id: 'open', label: linux ? 'Open in file manager' : 'Open disk in Finder', type: 'normal', enabled: diskStatus === 3, accelerator: linux ? "" : "CommandOrControl+O", click: global.actions.open } ]); if (!linux) { template.push({ id: 'reveal', label: 'Reveal disk in Finder', type: 'normal', enabled: diskStatus === 3, accelerator: linux ? "" : "CommandOrControl+Shift+O", click: global.actions.reveal }); } template.push(...[ { id: 'lock', label: 'Lock disk immediately', type: 'normal', enabled: diskStatus === 3, accelerator: linux ? "" : "CommandOrControl+Shift+L", click: global.actions.lockDisk }, { id: 'unlock', label: 'Unlock disk', type: 'normal', enabled: diskStatus === 2, accelerator: linux ? "" : "CommandOrControl+L", click: global.actions.unlockDisk }, { type: 'separator' }, { id: 'tracking', label: 'File usage tracking', type: 'normal', enabled: diskStatus === 3 && global.openWindow === null, accelerator: linux ? "" : "CommandOrControl+T", click: global.actions.tracking }, { type: 'separator' }, { id: 'quit', label: 'Quit', type: 'normal', enabled: true, accelerator: linux ? "" : "CommandOrControl+Q", click: global.actions.quit } ]); if (global.debugMenu) { template.push({ type: 'separator' }) template.push({ label: 'Debugging', type: 'normal', enabled: false }) template.push({ label: ' Disk: ' + disk, type: 'normal', enabled: false }) template.push({ label: ' Mount point: ' + directory, type: 'normal', enabled: false }) template.push({ label: ' Inactivity time: ' + time, type: 'normal', enabled: false }) template.push({ label: ' State: ' + diskStatus, type: 'normal', enabled: false }) template.push({ label: ' Electron: ' + process.versions.electron, type: 'normal', enabled: false }) template.push({ label: ' Node: ' + process.versions.node, type: 'normal', enabled: false }) template.push({ label: ' Chrome: ' + process.versions.chrome, type: 'normal', enabled: false }) template.push({ label: ' File watchers: ' + Object.keys(global.fileWatchers ?? {}).length + " (" + (global.fileWatchersPurgeable ?? []).length + " purgeable, " + (global.trackingFailures ?? []).length + " failed)", type: 'normal', enabled: false }) template.push({ label: ' Edit config file', type: 'normal', enabled: true, click: global.actions.config }) template.push({ label: ' Open Chrome dev tools', type: 'normal', enabled: diskStatus === 3 && global.openWindow !== null, click: global.actions.devtools }) } if (JSON.stringify(global.lastKnownMenu) !== JSON.stringify(template)) { const contextMenu = Menu.buildFromTemplate(template); tray.setContextMenu(contextMenu); if (global.linux) Menu.setApplicationMenu(contextMenu); global.lastKnownMenu = template; } } app.whenReady().then(() => { console.log(__dirname); if (!linux) { app.dock.hide(); } global.tray = new Tray('./icon/unknown/16x16Template@2x.png') updateMenu(); tray.setToolTip("TempDisk"); tray.on('click', (event) => { global.debugMenu = event.altKey; if (global.forceDebug) global.debugMenu = true; updateMenu(); }) require('./index'); }) app.on('window-all-closed', () => { return false; });