Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved Windows Install (PATH, ATOM_HOME, InstallLocation) #604

Merged
merged 8 commits into from
Jul 15, 2023
70 changes: 69 additions & 1 deletion packages/settings-view/lib/system-windows-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -36,7 +44,7 @@ export default class SystemPanel {
<div className='settings-panel'>
<div className='section-container'>
<div className='block section-heading icon icon-device-desktop'>System Settings</div>
<div className='text icon icon-question'>These settings determine how Atom integrates with your operating system.</div>
<div className='text icon icon-question'>These settings determine how Pulsar integrates with your operating system.</div>
<div className='section-body'>
<div className='control-group'>
<div className='controls'>
Expand Down Expand Up @@ -95,6 +103,7 @@ export default class SystemPanel {
</div>
</div>
</div>
{ this.getPathUI() }
</div>
</div>
</div>
Expand All @@ -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 (
<div className='control-group'>
<div className='controls'>
<div className='checkbox'>
<label for='system.windows.add-to-path'>
<input
ref='addToPathCheckbox'
id='system.windows.add-to-path'
className='input-checkbox'
type='checkbox'
onclick={(e) => {
this.setRegistration(WinShell.pathUser, e.target.checked)
}} />
<div className='setting-title'>Add Pulsar to PATH (User Install)</div>
<div className='setting-description'>
Add Pulsar to Windows PATH to enable CLI usage.
</div>
</label>
</div>
</div>
</div>
);
} else {
return (
<div className='control-group'>
<div className='controls'>
<div className='checkbox'>
<label for='system.windows.add-to-path-machine'>
<input
ref='addToPathMachineCheckbox'
id='system.windows.add-to-path-machine'
className='input-checkbox'
type='checkbox'
onclick={(e) => {
this.setRegistration(WinShell.pathMachine, e.target.checked)
}} />
<div className='setting-title'>Add Pulsar to PATH (Machine Install)</div>
<div className='setting-description'>
Add Pulsar to Windows PATH for machine installs. Requires administrative privileges.
</div>
</label>
</div>
</div>
</div>
);
}
}

focus () {
this.element.focus()
}
Expand Down
15 changes: 15 additions & 0 deletions resources/win/installer.nsh
Original file line number Diff line number Diff line change
@@ -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
99 changes: 99 additions & 0 deletions resources/win/modifyWindowsPath.ps1
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 6 additions & 1 deletion script/electron-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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": {
},
Expand Down
141 changes: 141 additions & 0 deletions src/main-process/win-shell.js
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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");