Skip to content

Commit 0efab47

Browse files
committed
Implement PATH persistence on every system (including nanoserver)
1 parent 240c71f commit 0efab47

File tree

8 files changed

+225
-165
lines changed

8 files changed

+225
-165
lines changed

PhpManager/private/Add-PhpFolderToPath.ps1

Lines changed: 0 additions & 89 deletions
This file was deleted.
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
Function Edit-PhpFolderInPath
2+
{
3+
<#
4+
.Synopsis
5+
Adds or removes a folder to/from the PATH environment variable.
6+
7+
.Parameter Operation
8+
The operation to be performed.
9+
10+
.Parameter Path
11+
The path of the directory to be added to the PATH environment variable.
12+
13+
.Parameter Persist
14+
When Operation is Add: permamently set the PATH for either the current user ('User') or for the whole system ('System').
15+
16+
.Parameter CurrentProcess
17+
When Operation is Add: specify this switch to add Path to the current PATH environment variable.
18+
#>
19+
Param (
20+
[Parameter(Mandatory = $True, Position = 0)]
21+
[ValidateNotNull()]
22+
[ValidateSet('Add', 'Remove')]
23+
[string]$Operation,
24+
[Parameter(Mandatory = $True, Position = 1)]
25+
[ValidateNotNull()]
26+
[ValidateLength(1, [int]::MaxValue)]
27+
[string]$Path,
28+
[Parameter(Mandatory = $False, Position = 2)]
29+
[ValidateSet('User', 'System')]
30+
[string]$Persist,
31+
[switch]$CurrentProcess
32+
)
33+
Begin {
34+
}
35+
Process {
36+
$pathSeparator = [System.IO.Path]::PathSeparator
37+
$directorySeparator = [System.IO.Path]::DirectorySeparatorChar
38+
$Path = [System.IO.Path]::GetFullPath($Path).TrimEnd($directorySeparator)
39+
$alternativePath = $Path + $directorySeparator
40+
$needDirectRegistryAccess = [System.Environment]::GetEnvironmentVariable[0].OverloadDefinitions.Count -lt 2
41+
If ($Operation -eq 'Add') {
42+
$targets = @{}
43+
If ($CurrentProcess) {
44+
$targets[$Script:ENVTARGET_PROCESS] = $null
45+
}
46+
If ($Persist -eq 'User') {
47+
$targets[$Script:ENVTARGET_USER] = 'HKCU:\Environment'
48+
} ElseIf ($Persist -eq 'System') {
49+
$targets[$Script:ENVTARGET_MACHINE] = 'HKLM:\System\CurrentControlSet\Control\Session Manager\Environment'
50+
}
51+
} Else {
52+
$targets = @{
53+
$Script:ENVTARGET_PROCESS = $null
54+
$Script:ENVTARGET_USER = 'HKCU:\Environment'
55+
$Script:ENVTARGET_MACHINE = 'HKLM:\System\CurrentControlSet\Control\Session Manager\Environment'
56+
}
57+
}
58+
$haveToBroadcast = $false
59+
ForEach ($target in $targets.Keys) {
60+
If ($target -eq $Script:ENVTARGET_PROCESS) {
61+
$originalPath = $Env:Path
62+
} ElseIf ($needDirectRegistryAccess) {
63+
$originalPath = ''
64+
If (Test-Path -LiteralPath $targets[$target]) {
65+
$pathProperties = Get-ItemProperty -LiteralPath $targets[$target] -Name 'Path'
66+
If ($pathProperties | Get-Member -Name 'Path') {
67+
$originalPath = $pathProperties.Path
68+
}
69+
}
70+
} Else {
71+
$originalPath = [System.Environment]::GetEnvironmentVariable('Path', $target)
72+
}
73+
If ($null -eq $originalPath -or $originalPath -eq '') {
74+
$parts = @()
75+
} Else {
76+
$parts = $originalPath.Split($pathSeparator)
77+
}
78+
$newPath = $null
79+
If ($Operation -eq 'Add') {
80+
If (-Not($parts -icontains $Path -or $parts -icontains $alternativePath)) {
81+
$parts += $Path
82+
$newPath = $parts -join $pathSeparator
83+
}
84+
} ElseIf ($Operation -eq 'Remove') {
85+
$parts = $parts | Where-Object {$_ -ne $Path -and $_ -ne $alternativePath}
86+
$newPath = $parts -join $pathSeparator
87+
}
88+
If ($null -ne $newPath -and $newPath -ne $originalPath) {
89+
If ($target -eq $Script:ENVTARGET_PROCESS) {
90+
$Env:Path = $newPath
91+
} Else {
92+
$requireRunAs = $false
93+
If ($target -eq $Script:ENVTARGET_MACHINE) {
94+
$currentUser = [System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()
95+
If (-Not($currentUser.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator))) {
96+
$requireRunAs = $true
97+
}
98+
}
99+
If ($requireRunAs) {
100+
$newPathEscaped = $newPath -replace "'", "''"
101+
If ($needDirectRegistryAccess) {
102+
$exeCommand = "New-ItemProperty -Path '$($targets[$target])' -Name 'Path' -Value '$newPathEscaped' -PropertyType ExpandString -Force | Out-Null"
103+
} Else {
104+
$exeCommand = "[System.Environment]::SetEnvironmentVariable('Path', '$newPathEscaped', '$Script:ENVTARGET_MACHINE') | Out-Null"
105+
}
106+
Start-Process -FilePath 'powershell.exe' -ArgumentList "-Command ""$exeCommand""" -Verb RunAs
107+
} else {
108+
If ($needDirectRegistryAccess) {
109+
New-ItemProperty -Path $targets[$target] -Name 'Path' -Value $newPath -PropertyType ExpandString -Force | Out-Null
110+
} Else {
111+
[System.Environment]::SetEnvironmentVariable('Path', $newPath, $target) | Out-Null
112+
}
113+
}
114+
$haveToBroadcast = $True
115+
}
116+
}
117+
}
118+
If ($haveToBroadcast) {
119+
Try {
120+
$className = 'PhpManager_Env_Broadcaster_v1'
121+
$classType = ([System.Management.Automation.PSTypeName]$className).Type
122+
If ($null -eq $classType) {
123+
Add-Type -Language CSharp -TypeDefinition @"
124+
using System;
125+
using System.ComponentModel;
126+
using System.Runtime.InteropServices;
127+
128+
public sealed class $className
129+
{
130+
/// <summary>
131+
/// Send the message to all top-level windows in the system, including disabled or invisible unowned windows.
132+
/// </summary>
133+
private const int HWND_BROADCAST = 0xffff;
134+
135+
/// <summary>
136+
/// Possible messages to be sent.
137+
/// </summary>
138+
private enum SmtoMsg : uint
139+
{
140+
/// <summary>
141+
/// A message that is sent to all top-level windows when the SystemParametersInfo function changes a system-wide setting or when policy settings have changed.
142+
/// </summary>
143+
WM_SETTINGCHANGE = 0x001A,
144+
}
145+
146+
/// <summary>
147+
/// Possible flags of the SendMessageTimeout function
148+
/// </summary>
149+
[Flags]
150+
private enum SmtoFlags : uint
151+
{
152+
/// <summary>
153+
/// The function returns without waiting for the time-out period to elapse if the receiving thread appears to not respond or "hangs."
154+
/// </summary>
155+
SMTO_ABORTIFHUNG = 0x0002,
156+
/// <summary>
157+
/// Prevents the calling thread from processing any other requests until the function returns.
158+
/// </summary>
159+
SMTO_BLOCK = 0x0001,
160+
/// <summary>
161+
/// The calling thread is not prevented from processing other requests while waiting for the function to return.
162+
/// </summary>
163+
SMTO_NORMAL = 0x0000,
164+
/// <summary>
165+
/// The function does not enforce the time-out period as long as the receiving thread is processing messages.
166+
/// </summary>
167+
SMTO_NOTIMEOUTIFNOTHUNG = 0x0008,
168+
/// <summary>
169+
/// The function should return 0 if the receiving window is destroyed or its owning thread dies while the message is being processed.
170+
/// </summary>
171+
SMTO_ERRORONEXIT = 0x0020,
172+
}
173+
174+
/// <summary>
175+
/// Sends the specified message to one or more windows.
176+
/// </summary>
177+
/// <param name="hWnd">A handle to the window whose window procedure will receive the message.</param>
178+
/// <param name="Msg">The message to be sent.</param>
179+
/// <param name="wParam">Any additional message-specific information.</param>
180+
/// <param name="lParam">Any additional message-specific information.</param>
181+
/// <param name="fuFlags">The behavior of this function.</param>
182+
/// <param name="uTimeout">The duration of the time-out period, in milliseconds.</param>
183+
/// <param name="lpdwResult">The result of the message processing.</param>
184+
/// <returns>If the function fails or times out, the return value is 0.</returns>
185+
[DllImport("user32.dll", BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)]
186+
private static extern IntPtr SendMessageTimeout(IntPtr hWnd, UInt32 Msg, IntPtr wParam, String lParam, UInt32 fuFlags, UInt32 uTimeout, IntPtr lpdwResult);
187+
188+
/// <summary>
189+
/// Broadcasts a "Environment settings changed" message.
190+
/// </summary>
191+
/// <exception cref="System.TimeoutException">Throws a TimeoutException when the message timed out.</exception>
192+
/// <exception cref="System.System.ComponentModel.Win32Exception">Throws a Win32Exception when the function fails.</exception>
193+
public static void BroadcastEnvironmentChange()
194+
{
195+
IntPtr lResult = SendMessageTimeout(new IntPtr(HWND_BROADCAST), (UInt32)SmtoMsg.WM_SETTINGCHANGE, IntPtr.Zero, "Environment", (UInt32)SmtoFlags.SMTO_ABORTIFHUNG, 1000, IntPtr.Zero);
196+
if (lResult == IntPtr.Zero)
197+
{
198+
int rc = Marshal.GetLastWin32Error();
199+
if (rc == 0)
200+
{
201+
throw new TimeoutException("SendMessageTimeout() timed out");
202+
}
203+
throw new Win32Exception(rc);
204+
}
205+
}
206+
}
207+
"@
208+
$classType = ([System.Management.Automation.PSTypeName]$className).Type
209+
}
210+
$classType.GetMethod('BroadcastEnvironmentChange').Invoke($null, @())
211+
}
212+
Catch {
213+
Write-Debug -Message $_
214+
}
215+
}
216+
}
217+
End {
218+
}
219+
}

PhpManager/private/Remove-PhpFolderFromPath.ps1

Lines changed: 0 additions & 70 deletions
This file was deleted.

PhpManager/public/Install-Php.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ function Install-Php() {
152152
Set-PhpIniKey -Key 'extension_dir' -Value $([System.IO.Path]::Combine($Path, 'ext')) -Path $iniPath
153153
}
154154
If ($null -ne $AddToPath -and $AddToPath -ne '') {
155-
Add-PhpFolderToPath -Path $Path -Persist $AddToPath -CurrentProcess
155+
Edit-PhpFolderInPath -Operation Add -Path $Path -Persist $AddToPath -CurrentProcess
156156
}
157157
}
158158
End {

0 commit comments

Comments
 (0)