diff --git a/packages/settings-view/lib/system-windows-panel.js b/packages/settings-view/lib/system-windows-panel.js index 604dc217b1..439e7b5894 100644 --- a/packages/settings-view/lib/system-windows-panel.js +++ b/packages/settings-view/lib/system-windows-panel.js @@ -20,6 +20,14 @@ export default class SystemPanel { WinShell.fileHandler.isRegistered((i) => { this.refs.fileHandlerCheckbox.checked = i }) WinShell.fileContextMenu.isRegistered((i) => { this.refs.fileContextMenuCheckbox.checked = i }) WinShell.folderContextMenu.isRegistered((i) => { this.refs.folderContextMenuCheckbox.checked = i }) + + if (this.isLikelyUserInstall()) { + WinShell.pathUser.isRegistered((i) => { this.refs.addToPathCheckbox.checked = i }) + } else { + WinShell.pathMachine.isRegistered((i) => { this.refs.addToPathMachineCheckbox.checked = i }) + // Check if Pulsar is running as Admin. To know if the user can modify the machine path + WinShell.runningAsAdmin((i) => { this.refs.addToPathMachineCheckbox.disabled = !i }) + } } destroy () { @@ -36,7 +44,7 @@ export default class SystemPanel {
System Settings
-
These settings determine how Atom integrates with your operating system.
+
These settings determine how Pulsar integrates with your operating system.
@@ -95,6 +103,7 @@ export default class SystemPanel {
+ { this.getPathUI() }
@@ -111,6 +120,65 @@ export default class SystemPanel { } } + isLikelyUserInstall() { + let resourcePath = atom.applicationDelegate.getWindowLoadSettings().resourcePath; + if (resourcePath.includes("AppData\\Local\\Programs\\pulsar")) { + return true; + } else { + return false; + } + } + + getPathUI() { + if (this.isLikelyUserInstall()) { + return ( +
+
+
+ +
+
+
+ ); + } else { + return ( +
+
+
+ +
+
+
+ ); + } + } + focus () { this.element.focus() } diff --git a/resources/win/installer.nsh b/resources/win/installer.nsh new file mode 100644 index 0000000000..8e9b601a80 --- /dev/null +++ b/resources/win/installer.nsh @@ -0,0 +1,15 @@ +!macro customInstall + # Set the 'InstallLocation' Registry Key for GitHub Desktop + WriteRegStr SHELL_CONTEXT "${UNINSTALL_REGISTRY_KEY}" "InstallLocation" "$INSTDIR" +!macroend + +!macro customUnInstall + # This uninstall script is ready to go. Just a question if we want to modify PATH on uninstall + ${if} $installMode == "all" + # Machine Install + #ExecWait 'powershell -ExecutionPolicy Bypass -WindowStyle Hidden -File "$INSTDIR\resources\modifyWindowsPath.ps1" -installMode Machine -installdir "$INSTDIR" -remove 1' + ${else} + # User Install + #ExecWait 'powershell -ExecutionPolicy Bypass -WindowStyle Hidden -File "$INSTDIR\resources\modifyWindowsPath.ps1" -installMode User -installdir "$INSTDIR" -remove 1' + ${endif} +!macroend diff --git a/resources/win/modifyWindowsPath.ps1 b/resources/win/modifyWindowsPath.ps1 new file mode 100644 index 0000000000..f02be5f1e9 --- /dev/null +++ b/resources/win/modifyWindowsPath.ps1 @@ -0,0 +1,99 @@ +# Modify Windows PATH for Pulsar +# Sets Pulsar & PPM into PATH, adds 'ATOM_HOME' env var + +# Example Usage: +# Pulsar User Installation: +# .\_.ps1 -installMode User -installdir "$INSTDIR" -remove 0 +# Pulsar Machine Installation: +# .\_.ps1 -installMode Machine -installdir "$INSTDIR" -remove 0 +# Pulsar User Uninstallation: +# .\_.ps1 -installMode User -installdir "$INSTDIR" -remove 1 +# Pulsar Machine Uninstallation: +# .\_.ps1 -installMode Machine -installdir "$INSTDIR" -remove 1 + +param ($installMode,$installdir,$remove) + +# When self-elevating, we can't pass a raw boolean. Meaning we accept anything then convert +$remove = [System.Convert]::ToBoolean($remove) + +# Only when modifying the Machine PATH, it takes much longer than expected. So here's a loading bar +$prog = 1 +Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog + +if ($installMode -eq "Machine") { + # PowerShell needs to be running as Admin to modify the Machine Variables + # So lets attempt to self-elevate + if (-Not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) { + if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) { + + $processOptions = @{ + FilePath = "PowerShell.exe" + Wait = $true + PassThru = $true + Verb = "RunAs" + ArgumentList = "-File `"" + $MyInvocation.MyCommand.Path + "`" -installMode $installMode -installdir `"" + $installdir + "`" -remove $remove" + } + + Start-Process @processOptions + + Exit + } + } +} + +if (-not $remove) { + if ($installMode -eq "User" -or $installMode -eq "Machine") { + + $prog = 25 + Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog + + [Environment]::SetEnvironmentVariable("Path", $env:Path + ";$installdir\resources;$installdir\resources\app\ppm\bin", $installMode) + + $prog = 50 + Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog + + # While this originally attempting to use the string '%USERPROFILE%' to avoid taking + # space on the PATH, whatever reads this path at startup in Pulsar, can't handle + # the variable, and instead creates the directory of the same name + # within the current folder. But only when opened via the context menu, terminal + # is fine. + $exitCode = [Environment]::SetEnvironmentVariable("ATOM_HOME", "$env:UserProfile\.pulsar", $installMode) + + $prog = 100 + Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog + + Exit $exitCode + } +} else { + if ($installMode -eq "User" -or $installMode -eq "Machine") { + + $prog = 25 + Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog + + $path = [Environment]::GetEnvironmentVariable("Path", $installMode) + + $prog = 50 + Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog + + # Remove unwanted element from path + $path = ($path.Split(";") | Where-Object { $_ -ne "$installdir\resources" }) -join ";" + $path = ($path.Split(";") | Where-Object { $_ -ne "$installdir\resources\app\ppm\bin" }) -join ";" + + $prog = 75 + Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog + + # Set our new path + [Environment]::SetEnvironmentVariable("Path", $path, $installMode) + + $prog = 90 + Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog + + # Set ATOM_HOME path + $exitCode = [Environment]::SetEnvironmentVariable("ATOM_HOME", $null, $installMode) + + $prog = 100 + Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog + + Exit $exitCode + } # Else we have been given bad params, and will silently exit +} diff --git a/script/electron-builder.js b/script/electron-builder.js index d84cefc774..a88a3050df 100644 --- a/script/electron-builder.js +++ b/script/electron-builder.js @@ -213,6 +213,10 @@ let options = { "from": "resources/win/pulsar.js", "to": "pulsar.js" }, + { + "from": "resources/win/modifyWindowsPath.ps1", + "to": "modifyWindowsPath.ps1" + } ], "target": [ { "target": "nsis" }, @@ -227,11 +231,12 @@ let options = { "runAfterFinish": true, "createDesktopShortcut": true, "createStartMenuShortcut": true, - "guid": "0949b555-c22c-56b7-873a-a960bdefa81f" + "guid": "0949b555-c22c-56b7-873a-a960bdefa81f", // The GUID is generated from Electron-Builder based on our AppID // Hardcoding it here means it will always be used as generated from // the AppID 'dev.pulsar-edit.pulsar'. If this value ever changes, // A PR to GitHub Desktop must be made with the updated value + "include": "resources/win/installer.nsh" }, "extraMetadata": { }, diff --git a/src/main-process/win-shell.js b/src/main-process/win-shell.js index 366eb513cd..27bbaf4f9c 100644 --- a/src/main-process/win-shell.js +++ b/src/main-process/win-shell.js @@ -1,5 +1,6 @@ const Registry = require('winreg'); const Path = require('path'); +const ChildProcess = require('child_process'); const getAppName = require('../get-app-name'); const appName = getAppName(); @@ -73,6 +74,144 @@ class ShellOption { } } +class PathOption { + constructor(installType) { + // installType MUST be 'User' or 'Machine' + this.HKPATH; + this.hive; + this.installReg = "\\SOFTWARE\\0949b555-c22c-56b7-873a-a960bdefa81f"; + this.installMode = installType; + + if (installType === "User") { + this.HKPATH = "\\Environment"; + this.hive = "HKCU"; + } else if (installType === "Machine") { + this.HKPATH = "\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; + this.hive = "HKLM"; + } + + // Unfortunately, we can only manage the PATH for a per user installation. + // While the PowerShell script does support setting the PATH for a Machine + // install, we can't yet check that. + // https://github.com/fresc81/node-winreg/tree/1.2.1#troubleshooting + // This can only be done if Pulsar is run as Admin, with a user with Admin privs + // So we will pretend a user install is all that matters here + this.isRegistered = this.isRegistered.bind(this); + this.register = this.register.bind(this); + this.deregister = this.deregister.bind(this); + this.getPulsarPath = this.getPulsarPath.bind(this); + } + + isRegistered(callback) { + let installRegKey = new Registry({ + hive: this.hive, + key: this.HKPATH + }); + + let isInstalled = false; + + installRegKey.values((err, items) => { + if (err) { + callback(err); + } else { + for (let i = 0; i < items.length; i++) { + if (items[i].name === "Path") { + let winPath = items[i].value; + if (winPath.includes("Pulsar\\resources") || winPath.includes("Pulsar\\resources\\app\\ppm\\bin")) { + isInstalled = true; + } + } + } + callback(isInstalled); + } + }); + } + + register(callback) { + this.getPulsarPath().then((pulsarPath) => { + const child = ChildProcess.execFile( + `${pulsarPath}\\resources\\modifyWindowsPath.ps1`, + ['-installMode', this.installMode, '-installdir', `"${pulsarPath}"`, '-remove', '0'], + { shell: "powershell.exe" }, + (error, stdout, stderr) => + { + if (error) { + atom.notifications.addError(`Error Running Script: ${error.toString()}`); + callback(error); + } else { + return callback(); + } + }); + }).catch((err) => { + return callback(err); + }); + } + + deregister(callback) { + this.isRegistered(isRegistered => { + if (isRegistered) { + this.getPulsarPath().then((pulsarPath) => { + const child = ChildProcess.execFile( + `${pulsarPath}\\resources\\modifyWindowsPath.ps1`, + ['-installMode', this.installMode, '-installdir', `"${pulsarPath}"`, '-remove', '1'], + { shell: "powershell.exe" }, + (error, stdout, stderr) => + { + if (error) { + atom.notifications.addError(`Error Running Script: ${error.toString()}`); + callback(error); + } else { + return callback(); + } + }); + }).catch((err) => { + return callback(err); + }); + } else { + callback(null, false); + } + }); + } + + getPulsarPath() { + return new Promise((resolve, reject) => { + let pulsarPath = ""; + let pulsarPathReg = new Registry({ + hive: this.hive, + key: this.installReg + }).get("InstallLocation", (err, val) => { + if (err) { + reject(err); + } else { + pulsarPath = val.value; + + if (pulsarPath.length === 0) { + reject("Unable to find Pulsar Install Path"); + } + + // When we are modifying Machine values, we can't accept spaces in the + // path. There's likely some combination of escapes to fix this, but + // I was unable to find them. For now we will check for the default + // Machine install location, and remove the space. + let safePulsarPath = pulsarPath.replace("Program Files", "PROGRA~1"); + resolve(safePulsarPath); + } + }); + }); + } +} + +// Function that can inform is Pulsar is running as the administrator on Windows +exports.runningAsAdmin = (callback) => { + const child = ChildProcess.exec("NET SESSION", (error, stdout, stderr) => { + if (stderr.length === 0) { + callback(true); + } else { + callback(false); + } + }); +}; + exports.appName = appName; exports.fileHandler = new ShellOption( @@ -102,3 +241,5 @@ exports.folderBackgroundContextMenu = new ShellOption( `\\Software\\Classes\\Directory\\background\\shell\\${appName}`, JSON.parse(JSON.stringify(contextParts).replace('%1', '%V')) ); +exports.pathUser = new PathOption("User"); +exports.pathMachine = new PathOption("Machine");