win-nice 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +141 -0
- package/README.md +309 -41
- package/bin/abovenormal +11 -11
- package/bin/abovenormal.bat +2 -2
- package/bin/abovenormal.ps1 +38 -10
- package/bin/admin +11 -11
- package/bin/admin.bat +2 -2
- package/bin/admin.ps1 +52 -13
- package/bin/belownormal +11 -11
- package/bin/belownormal.bat +2 -2
- package/bin/belownormal.ps1 +38 -10
- package/bin/{cap → capc} +11 -11
- package/bin/{cap.bat → capc.bat} +3 -3
- package/bin/capc.ps1 +432 -0
- package/bin/{pint → capm} +11 -11
- package/bin/capm.bat +12 -0
- package/bin/capm.ps1 +506 -0
- package/bin/capn +11 -0
- package/bin/capn.bat +8 -0
- package/bin/capn.ps1 +426 -0
- package/bin/caps +11 -0
- package/bin/caps.bat +10 -0
- package/bin/caps.ps1 +589 -0
- package/bin/capt +11 -0
- package/bin/capt.bat +8 -0
- package/bin/capt.ps1 +449 -0
- package/bin/cx +11 -11
- package/bin/cx.bat +1 -1
- package/bin/cx.ps1 +38 -10
- package/bin/cy +11 -11
- package/bin/cy.bat +1 -1
- package/bin/cy.ps1 +38 -10
- package/bin/high +11 -11
- package/bin/high.bat +2 -2
- package/bin/high.ps1 +38 -10
- package/bin/idle +11 -11
- package/bin/idle.bat +2 -2
- package/bin/idle.ps1 +38 -10
- package/bin/realtime +11 -11
- package/bin/realtime.bat +2 -2
- package/bin/realtime.ps1 +38 -10
- package/bin/uiup +11 -11
- package/bin/uiup.bat +1 -1
- package/bin/uiup.ps1 +2 -1
- package/install/install.js +30 -15
- package/install/paths.js +28 -2
- package/install/skill.js +25 -1
- package/install/uninstall.js +11 -0
- package/package.json +7 -3
- package/skills/win-nice/SKILL.md +107 -12
- package/bin/cap.ps1 +0 -269
- package/bin/pint.bat +0 -8
- package/bin/pint.ps1 +0 -270
package/bin/abovenormal.ps1
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
2
2
|
# win-nice: managed-file
|
|
3
3
|
# No param(): nothing here needs a named parameter, and $args sidesteps
|
|
4
|
-
# PowerShell's parameter binder entirely - see
|
|
4
|
+
# PowerShell's parameter binder entirely - see capc.ps1 for why that matters.
|
|
5
5
|
$Command = $args
|
|
6
6
|
|
|
7
7
|
if (-not $Command -or $Command.Count -eq 0) {
|
|
@@ -11,7 +11,7 @@ if (-not $Command -or $Command.Count -eq 0) {
|
|
|
11
11
|
|
|
12
12
|
# Fallback command line for when the target isn't a directly-launchable .exe (see
|
|
13
13
|
# AboveNormalLauncher.Run below) - re-parsed by cmd.exe (via "cmd.exe /c"), so quoting must
|
|
14
|
-
# neutralize its operators (&|<>^) and not just whitespace - see
|
|
14
|
+
# neutralize its operators (&|<>^) and not just whitespace - see capc.ps1 for the
|
|
15
15
|
# same logic and its documented "%" limitation. abovenormal.bat has its own, more
|
|
16
16
|
# severe "%" caveat (see there) that applies before this script ever runs.
|
|
17
17
|
$commandLine = ($Command | ForEach-Object {
|
|
@@ -70,6 +70,9 @@ public static class AboveNormalLauncher
|
|
|
70
70
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
71
71
|
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
72
72
|
|
|
73
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
74
|
+
static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
|
|
75
|
+
|
|
73
76
|
[DllImport("kernel32.dll")]
|
|
74
77
|
static extern bool CloseHandle(IntPtr hObject);
|
|
75
78
|
|
|
@@ -124,7 +127,7 @@ public static class AboveNormalLauncher
|
|
|
124
127
|
si.cb = Marshal.SizeOf(si);
|
|
125
128
|
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
|
126
129
|
|
|
127
|
-
// See
|
|
130
|
+
// See capc.ps1 for why .bat/.cmd targets skip the direct attempt entirely:
|
|
128
131
|
// CreateProcess silently re-invokes them through cmd.exe on its own, using
|
|
129
132
|
// unescaped text, instead of failing the way a genuinely missing exe would.
|
|
130
133
|
bool isBatOrCmd = argv.Length > 0 && (
|
|
@@ -146,6 +149,7 @@ public static class AboveNormalLauncher
|
|
|
146
149
|
// across the whole command line, even across separate arguments) and
|
|
147
150
|
// change what actually runs. Fail loudly here instead of silently risking
|
|
148
151
|
// that - there's no reliable per-character escape for "%" at this level.
|
|
152
|
+
// No handle is held at this point, so this throw has nothing to clean up.
|
|
149
153
|
foreach (var a in argv)
|
|
150
154
|
{
|
|
151
155
|
if (a.IndexOf('%') >= 0)
|
|
@@ -169,15 +173,39 @@ public static class AboveNormalLauncher
|
|
|
169
173
|
throw new InvalidOperationException("CreateProcess failed: " + Marshal.GetLastWin32Error());
|
|
170
174
|
}
|
|
171
175
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
// Ownership of the child's handles starts here - CreateProcess has succeeded, so
|
|
177
|
+
// both are valid, and the finally below closes each of them exactly once on every
|
|
178
|
+
// way out: normal return, a thrown InvalidOperationException, or an unexpected
|
|
179
|
+
// managed exception.
|
|
180
|
+
IntPtr hProcess = pi.hProcess;
|
|
181
|
+
IntPtr hThread = pi.hThread;
|
|
182
|
+
try
|
|
183
|
+
{
|
|
184
|
+
if (WaitForSingleObject(hProcess, 0xFFFFFFFF) == 0xFFFFFFFF)
|
|
185
|
+
{
|
|
186
|
+
// The child's actual state is unknown here - don't just report
|
|
187
|
+
// failure and potentially leave it running unmanaged in the
|
|
188
|
+
// background. Best-effort kill before giving up.
|
|
189
|
+
int waitErr = Marshal.GetLastWin32Error();
|
|
190
|
+
string message = "WaitForSingleObject failed: " + waitErr;
|
|
191
|
+
// Report if the best-effort kill itself also failed.
|
|
192
|
+
if (!TerminateProcess(hProcess, 1))
|
|
193
|
+
message += "; TerminateProcess also failed: " + Marshal.GetLastWin32Error();
|
|
194
|
+
throw new InvalidOperationException(message);
|
|
195
|
+
}
|
|
176
196
|
|
|
177
|
-
|
|
178
|
-
|
|
197
|
+
uint exitCode;
|
|
198
|
+
if (!GetExitCodeProcess(hProcess, out exitCode))
|
|
199
|
+
throw new InvalidOperationException("GetExitCodeProcess failed: " + Marshal.GetLastWin32Error());
|
|
179
200
|
|
|
180
|
-
|
|
201
|
+
return (int)exitCode;
|
|
202
|
+
}
|
|
203
|
+
finally
|
|
204
|
+
{
|
|
205
|
+
// Same order as the code this replaces: thread handle, then process handle.
|
|
206
|
+
CloseHandle(hThread);
|
|
207
|
+
CloseHandle(hProcess);
|
|
208
|
+
}
|
|
181
209
|
}
|
|
182
210
|
}
|
|
183
211
|
"@
|
package/bin/admin
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
-
# win-nice: managed-file
|
|
4
|
-
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
-
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
-
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
-
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
-
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
-
export MSYS2_ARG_CONV_EXCL
|
|
10
|
-
script=$(cygpath -w "$(dirname "$0")/admin.ps1" 2>/dev/null) || script="$(dirname "$0")/admin.ps1"
|
|
11
|
-
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
# win-nice: managed-file
|
|
4
|
+
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
+
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
+
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
+
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
+
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
+
export MSYS2_ARG_CONV_EXCL
|
|
10
|
+
script=$(cygpath -w "$(dirname "$0")/admin.ps1" 2>/dev/null) || script="$(dirname "$0")/admin.ps1"
|
|
11
|
+
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
package/bin/admin.bat
CHANGED
|
@@ -5,8 +5,8 @@ if "%~1"=="" (
|
|
|
5
5
|
echo usage: admin ^<command^> [args...] 1>&2
|
|
6
6
|
exit /b 1
|
|
7
7
|
)
|
|
8
|
-
:: A literal "%" in any argument gets corrupted here - see
|
|
8
|
+
:: A literal "%" in any argument gets corrupted here - see capc.bat for why (a
|
|
9
9
|
:: cmd.exe batch-parameter quirk, not fixable from inside a .bat). Every other
|
|
10
10
|
:: cmd.exe metacharacter (&|<>^) survives this hop untouched. Invoking "admin"
|
|
11
11
|
:: bare from an actual PowerShell session skips this file (admin.ps1 preferred).
|
|
12
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0admin.ps1" %*
|
|
12
|
+
"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "%~dp0admin.ps1" %*
|
package/bin/admin.ps1
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
2
2
|
# win-nice: managed-file
|
|
3
3
|
$Command = $args
|
|
4
|
-
# Deliberately no [Parameter()]/[CmdletBinding()] attributes: see
|
|
4
|
+
# Deliberately no [Parameter()]/[CmdletBinding()] attributes: see capc.ps1 for why -
|
|
5
5
|
# it would expose PowerShell's common parameters and make them ambiguously
|
|
6
6
|
# prefix-match flags meant for the wrapped command.
|
|
7
7
|
|
|
@@ -13,7 +13,7 @@ if (-not $Command -or $Command.Count -eq 0) {
|
|
|
13
13
|
# Fallback command line for the UAC (-Verb RunAs) branch, and for the inline branch
|
|
14
14
|
# when the target isn't a directly-launchable .exe (see AdminLauncher.Run below) -
|
|
15
15
|
# re-parsed by cmd.exe, so quoting must neutralize its operators (&|<>^) and not
|
|
16
|
-
# just whitespace - see
|
|
16
|
+
# just whitespace - see capc.ps1 for the same logic and its documented "%" limitation.
|
|
17
17
|
$commandLine = ($Command | ForEach-Object {
|
|
18
18
|
$escaped = $_ -replace '"', '\"'
|
|
19
19
|
if ($escaped -eq '' -or $escaped -match '[\s"&|<>^]') { '"' + $escaped + '"' } else { $escaped }
|
|
@@ -22,7 +22,7 @@ $commandLine = ($Command | ForEach-Object {
|
|
|
22
22
|
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
23
23
|
|
|
24
24
|
# AdminLauncher (embedded C#): direct-CreateProcess-first, cmd.exe-fallback launcher,
|
|
25
|
-
# same strategy as
|
|
25
|
+
# same strategy as capc.ps1's Capper - a direct .exe target never touches cmd.exe, so
|
|
26
26
|
# it isn't exposed to "%" expansion at all. Defined unconditionally (not only inside
|
|
27
27
|
# the already-elevated branch below) because the not-yet-elevated branch also calls
|
|
28
28
|
# AdminLauncher.BuildArgvCommandLine for its own direct (non-cmd.exe) -Verb RunAs launch.
|
|
@@ -77,6 +77,9 @@ public static class AdminLauncher
|
|
|
77
77
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
78
78
|
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
79
79
|
|
|
80
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
81
|
+
static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
|
|
82
|
+
|
|
80
83
|
[DllImport("kernel32.dll")]
|
|
81
84
|
static extern bool CloseHandle(IntPtr hObject);
|
|
82
85
|
|
|
@@ -146,6 +149,7 @@ public static class AdminLauncher
|
|
|
146
149
|
// across the whole command line, even across separate arguments) and
|
|
147
150
|
// change what actually runs. Fail loudly here instead of silently risking
|
|
148
151
|
// that - there's no reliable per-character escape for "%" at this level.
|
|
152
|
+
// No handle is held at this point, so this throw has nothing to clean up.
|
|
149
153
|
foreach (var a in argv)
|
|
150
154
|
{
|
|
151
155
|
if (a.IndexOf('%') >= 0)
|
|
@@ -174,15 +178,39 @@ public static class AdminLauncher
|
|
|
174
178
|
throw new InvalidOperationException("CreateProcess failed: " + Marshal.GetLastWin32Error());
|
|
175
179
|
}
|
|
176
180
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
// Ownership of the child's handles starts here - CreateProcess has succeeded, so
|
|
182
|
+
// both are valid, and the finally below closes each of them exactly once on every
|
|
183
|
+
// way out: normal return, a thrown InvalidOperationException, or an unexpected
|
|
184
|
+
// managed exception.
|
|
185
|
+
IntPtr hProcess = pi.hProcess;
|
|
186
|
+
IntPtr hThread = pi.hThread;
|
|
187
|
+
try
|
|
188
|
+
{
|
|
189
|
+
if (WaitForSingleObject(hProcess, 0xFFFFFFFF) == 0xFFFFFFFF)
|
|
190
|
+
{
|
|
191
|
+
// The child's actual state is unknown here - don't just report
|
|
192
|
+
// failure and potentially leave it running unmanaged in the
|
|
193
|
+
// background. Best-effort kill before giving up.
|
|
194
|
+
int waitErr = Marshal.GetLastWin32Error();
|
|
195
|
+
string message = "WaitForSingleObject failed: " + waitErr;
|
|
196
|
+
// Report if the best-effort kill itself also failed.
|
|
197
|
+
if (!TerminateProcess(hProcess, 1))
|
|
198
|
+
message += "; TerminateProcess also failed: " + Marshal.GetLastWin32Error();
|
|
199
|
+
throw new InvalidOperationException(message);
|
|
200
|
+
}
|
|
181
201
|
|
|
182
|
-
|
|
183
|
-
|
|
202
|
+
uint exitCode;
|
|
203
|
+
if (!GetExitCodeProcess(hProcess, out exitCode))
|
|
204
|
+
throw new InvalidOperationException("GetExitCodeProcess failed: " + Marshal.GetLastWin32Error());
|
|
184
205
|
|
|
185
|
-
|
|
206
|
+
return (int)exitCode;
|
|
207
|
+
}
|
|
208
|
+
finally
|
|
209
|
+
{
|
|
210
|
+
// Same order as the code this replaces: thread handle, then process handle.
|
|
211
|
+
CloseHandle(hThread);
|
|
212
|
+
CloseHandle(hProcess);
|
|
213
|
+
}
|
|
186
214
|
}
|
|
187
215
|
}
|
|
188
216
|
"@
|
|
@@ -214,11 +242,21 @@ if ($isAdmin) {
|
|
|
214
242
|
# aliases/functions (dir, echo, start) are invisible to it, which routes them to the
|
|
215
243
|
# fallback, while real executables resolve. Standalone function so the routing
|
|
216
244
|
# decision is testable without ever reaching -Verb RunAs (a real UAC prompt).
|
|
245
|
+
# The resolver also records the path used by the direct route. Keep the routing
|
|
246
|
+
# function self-contained: the test suite AST-extracts it without the rest of this
|
|
247
|
+
# script, so it must not depend on a second helper function.
|
|
248
|
+
$script:AdminLaunchPath = $null
|
|
217
249
|
function Get-AdminLaunchRoute {
|
|
218
250
|
param([Parameter(Mandatory = $true)][string]$Target)
|
|
251
|
+
$script:AdminLaunchPath = $null
|
|
219
252
|
if ($Target -match '\.(bat|cmd)$') { return 'CmdFallback' }
|
|
220
253
|
$resolved = Get-Command -Name ([System.Management.Automation.WildcardPattern]::Escape($Target)) -CommandType Application -ErrorAction SilentlyContinue
|
|
221
|
-
if ($resolved -and $resolved.Path -and $resolved.Path -notmatch '\.(bat|cmd)$') {
|
|
254
|
+
if ($resolved -and $resolved.Path -and $resolved.Path -notmatch '\.(bat|cmd)$') {
|
|
255
|
+
# ShellExecute must receive the resolved absolute path. Passing a bare name
|
|
256
|
+
# here lets the elevated process search the current directory first.
|
|
257
|
+
$script:AdminLaunchPath = [System.IO.Path]::GetFullPath($resolved.Path)
|
|
258
|
+
return 'Direct'
|
|
259
|
+
}
|
|
222
260
|
return 'CmdFallback'
|
|
223
261
|
}
|
|
224
262
|
|
|
@@ -237,10 +275,11 @@ try {
|
|
|
237
275
|
if ($route -eq 'CmdFallback') {
|
|
238
276
|
# Same /d /s /v:off + outer-quote-wrap fix as AdminLauncher.Run's cmd.exe
|
|
239
277
|
# fallback, and for the same reason: cmd.exe's /C quote-stripping.
|
|
240
|
-
$
|
|
278
|
+
$cmdExePath = [Environment]::SystemDirectory + '\cmd.exe'
|
|
279
|
+
$p = Start-Process -FilePath $cmdExePath -ArgumentList @('/d', '/s', '/v:off', '/c', ('"' + $commandLine + '"')) -Verb RunAs -Wait -PassThru
|
|
241
280
|
} else {
|
|
242
281
|
$startArgs = @{
|
|
243
|
-
FilePath = $
|
|
282
|
+
FilePath = $script:AdminLaunchPath
|
|
244
283
|
Verb = 'RunAs'
|
|
245
284
|
Wait = $true
|
|
246
285
|
PassThru = $true
|
package/bin/belownormal
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
-
# win-nice: managed-file
|
|
4
|
-
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
-
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
-
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
-
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
-
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
-
export MSYS2_ARG_CONV_EXCL
|
|
10
|
-
script=$(cygpath -w "$(dirname "$0")/belownormal.ps1" 2>/dev/null) || script="$(dirname "$0")/belownormal.ps1"
|
|
11
|
-
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
# win-nice: managed-file
|
|
4
|
+
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
+
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
+
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
+
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
+
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
+
export MSYS2_ARG_CONV_EXCL
|
|
10
|
+
script=$(cygpath -w "$(dirname "$0")/belownormal.ps1" 2>/dev/null) || script="$(dirname "$0")/belownormal.ps1"
|
|
11
|
+
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
package/bin/belownormal.bat
CHANGED
|
@@ -5,9 +5,9 @@ if "%~1"=="" (
|
|
|
5
5
|
echo usage: belownormal ^<command^> [args...] 1>&2
|
|
6
6
|
exit /b 1
|
|
7
7
|
)
|
|
8
|
-
:: A literal "%" in any argument gets corrupted here - see
|
|
8
|
+
:: A literal "%" in any argument gets corrupted here - see capc.bat for why (a
|
|
9
9
|
:: cmd.exe batch-parameter quirk, not fixable from inside a .bat). Every other
|
|
10
10
|
:: cmd.exe metacharacter (&|<>^) survives this hop untouched. Invoking
|
|
11
11
|
:: "belownormal" bare from an actual PowerShell session skips this file
|
|
12
12
|
:: (belownormal.ps1 preferred).
|
|
13
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0belownormal.ps1" %*
|
|
13
|
+
"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "%~dp0belownormal.ps1" %*
|
package/bin/belownormal.ps1
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
2
2
|
# win-nice: managed-file
|
|
3
3
|
# No param(): nothing here needs a named parameter, and $args sidesteps
|
|
4
|
-
# PowerShell's parameter binder entirely - see
|
|
4
|
+
# PowerShell's parameter binder entirely - see capc.ps1 for why that matters.
|
|
5
5
|
$Command = $args
|
|
6
6
|
|
|
7
7
|
if (-not $Command -or $Command.Count -eq 0) {
|
|
@@ -11,7 +11,7 @@ if (-not $Command -or $Command.Count -eq 0) {
|
|
|
11
11
|
|
|
12
12
|
# Fallback command line for when the target isn't a directly-launchable .exe (see
|
|
13
13
|
# BelowNormalLauncher.Run below) - re-parsed by cmd.exe (via "cmd.exe /c"), so quoting must
|
|
14
|
-
# neutralize its operators (&|<>^) and not just whitespace - see
|
|
14
|
+
# neutralize its operators (&|<>^) and not just whitespace - see capc.ps1 for the
|
|
15
15
|
# same logic and its documented "%" limitation. belownormal.bat has its own, more
|
|
16
16
|
# severe "%" caveat (see there) that applies before this script ever runs.
|
|
17
17
|
$commandLine = ($Command | ForEach-Object {
|
|
@@ -70,6 +70,9 @@ public static class BelowNormalLauncher
|
|
|
70
70
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
71
71
|
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
72
72
|
|
|
73
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
74
|
+
static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
|
|
75
|
+
|
|
73
76
|
[DllImport("kernel32.dll")]
|
|
74
77
|
static extern bool CloseHandle(IntPtr hObject);
|
|
75
78
|
|
|
@@ -123,7 +126,7 @@ public static class BelowNormalLauncher
|
|
|
123
126
|
si.cb = Marshal.SizeOf(si);
|
|
124
127
|
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
|
125
128
|
|
|
126
|
-
// See
|
|
129
|
+
// See capc.ps1 for why .bat/.cmd targets skip the direct attempt entirely:
|
|
127
130
|
// CreateProcess silently re-invokes them through cmd.exe on its own, using
|
|
128
131
|
// unescaped text, instead of failing the way a genuinely missing exe would.
|
|
129
132
|
bool isBatOrCmd = argv.Length > 0 && (
|
|
@@ -145,6 +148,7 @@ public static class BelowNormalLauncher
|
|
|
145
148
|
// across the whole command line, even across separate arguments) and
|
|
146
149
|
// change what actually runs. Fail loudly here instead of silently risking
|
|
147
150
|
// that - there's no reliable per-character escape for "%" at this level.
|
|
151
|
+
// No handle is held at this point, so this throw has nothing to clean up.
|
|
148
152
|
foreach (var a in argv)
|
|
149
153
|
{
|
|
150
154
|
if (a.IndexOf('%') >= 0)
|
|
@@ -168,15 +172,39 @@ public static class BelowNormalLauncher
|
|
|
168
172
|
throw new InvalidOperationException("CreateProcess failed: " + Marshal.GetLastWin32Error());
|
|
169
173
|
}
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
// Ownership of the child's handles starts here - CreateProcess has succeeded, so
|
|
176
|
+
// both are valid, and the finally below closes each of them exactly once on every
|
|
177
|
+
// way out: normal return, a thrown InvalidOperationException, or an unexpected
|
|
178
|
+
// managed exception.
|
|
179
|
+
IntPtr hProcess = pi.hProcess;
|
|
180
|
+
IntPtr hThread = pi.hThread;
|
|
181
|
+
try
|
|
182
|
+
{
|
|
183
|
+
if (WaitForSingleObject(hProcess, 0xFFFFFFFF) == 0xFFFFFFFF)
|
|
184
|
+
{
|
|
185
|
+
// The child's actual state is unknown here - don't just report
|
|
186
|
+
// failure and potentially leave it running unmanaged in the
|
|
187
|
+
// background. Best-effort kill before giving up.
|
|
188
|
+
int waitErr = Marshal.GetLastWin32Error();
|
|
189
|
+
string message = "WaitForSingleObject failed: " + waitErr;
|
|
190
|
+
// Report if the best-effort kill itself also failed.
|
|
191
|
+
if (!TerminateProcess(hProcess, 1))
|
|
192
|
+
message += "; TerminateProcess also failed: " + Marshal.GetLastWin32Error();
|
|
193
|
+
throw new InvalidOperationException(message);
|
|
194
|
+
}
|
|
175
195
|
|
|
176
|
-
|
|
177
|
-
|
|
196
|
+
uint exitCode;
|
|
197
|
+
if (!GetExitCodeProcess(hProcess, out exitCode))
|
|
198
|
+
throw new InvalidOperationException("GetExitCodeProcess failed: " + Marshal.GetLastWin32Error());
|
|
178
199
|
|
|
179
|
-
|
|
200
|
+
return (int)exitCode;
|
|
201
|
+
}
|
|
202
|
+
finally
|
|
203
|
+
{
|
|
204
|
+
// Same order as the code this replaces: thread handle, then process handle.
|
|
205
|
+
CloseHandle(hThread);
|
|
206
|
+
CloseHandle(hProcess);
|
|
207
|
+
}
|
|
180
208
|
}
|
|
181
209
|
}
|
|
182
210
|
"@
|
package/bin/{cap → capc}
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
-
# win-nice: managed-file
|
|
4
|
-
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
-
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
-
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
-
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
-
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
-
export MSYS2_ARG_CONV_EXCL
|
|
10
|
-
script=$(cygpath -w "$(dirname "$0")/
|
|
11
|
-
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
# win-nice: managed-file
|
|
4
|
+
# Git Bash ignores PATHEXT for bare-name resolution; this shim covers that shell.
|
|
5
|
+
# MSYS2_ARG_CONV_EXCL='*' stops Git Bash/MSYS from rewriting user arguments
|
|
6
|
+
# (e.g. /c, /d, C:\...) into Windows paths before the exec; the shim's own
|
|
7
|
+
# .ps1 path is converted explicitly with cygpath -w so -File gets a Windows path.
|
|
8
|
+
MSYS2_ARG_CONV_EXCL='*'
|
|
9
|
+
export MSYS2_ARG_CONV_EXCL
|
|
10
|
+
script=$(cygpath -w "$(dirname "$0")/capc.ps1" 2>/dev/null) || script="$(dirname "$0")/capc.ps1"
|
|
11
|
+
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
package/bin/{cap.bat → capc.bat}
RENAMED
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
:: %...% patterns the moment a batch file reads them - confirmed with nothing more
|
|
6
6
|
:: than a bare "echo %1", no forwarding involved; there's no per-character escape
|
|
7
7
|
:: for this from inside a .bat). Every other cmd.exe metacharacter (&|<>^) survives
|
|
8
|
-
:: this hop untouched. Invoking "
|
|
9
|
-
:: this file entirely (PowerShell prefers
|
|
10
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File "%~
|
|
8
|
+
:: this hop untouched. Invoking "capc" bare from an actual PowerShell session skips
|
|
9
|
+
:: this file entirely (PowerShell prefers capc.ps1) and has no "%" problem at all.
|
|
10
|
+
"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "%~dp0capc.ps1" %*
|