win-nice 0.1.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/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +356 -0
- package/bin/abovenormal +11 -0
- package/bin/abovenormal.bat +13 -0
- package/bin/abovenormal.ps1 +193 -0
- package/bin/admin +11 -0
- package/bin/admin.bat +12 -0
- package/bin/admin.ps1 +259 -0
- package/bin/belownormal +11 -0
- package/bin/belownormal.bat +13 -0
- package/bin/belownormal.ps1 +192 -0
- package/bin/cap +11 -0
- package/bin/cap.bat +10 -0
- package/bin/cap.ps1 +269 -0
- package/bin/cx +11 -0
- package/bin/cx.bat +8 -0
- package/bin/cx.ps1 +185 -0
- package/bin/cy +11 -0
- package/bin/cy.bat +8 -0
- package/bin/cy.ps1 +185 -0
- package/bin/high +11 -0
- package/bin/high.bat +12 -0
- package/bin/high.ps1 +193 -0
- package/bin/idle +11 -0
- package/bin/idle.bat +12 -0
- package/bin/idle.ps1 +192 -0
- package/bin/pint +11 -0
- package/bin/pint.bat +8 -0
- package/bin/pint.ps1 +270 -0
- package/bin/realtime +11 -0
- package/bin/realtime.bat +13 -0
- package/bin/realtime.ps1 +197 -0
- package/bin/uiup +11 -0
- package/bin/uiup.bat +4 -0
- package/bin/uiup.ps1 +41 -0
- package/install/cli.js +62 -0
- package/install/install.js +79 -0
- package/install/manifest.js +29 -0
- package/install/paths.js +141 -0
- package/install/skill.js +68 -0
- package/install/uninstall.js +76 -0
- package/package.json +48 -0
- package/skills/win-nice/SKILL.md +132 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
2
|
+
# win-nice: managed-file
|
|
3
|
+
# No param(): nothing here needs a named parameter, and $args sidesteps
|
|
4
|
+
# PowerShell's parameter binder entirely - see cap.ps1 for why that matters.
|
|
5
|
+
$Command = $args
|
|
6
|
+
|
|
7
|
+
if (-not $Command -or $Command.Count -eq 0) {
|
|
8
|
+
Write-Error "usage: abovenormal <command> [args...]"
|
|
9
|
+
exit 1
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# Fallback command line for when the target isn't a directly-launchable .exe (see
|
|
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 cap.ps1 for the
|
|
15
|
+
# same logic and its documented "%" limitation. abovenormal.bat has its own, more
|
|
16
|
+
# severe "%" caveat (see there) that applies before this script ever runs.
|
|
17
|
+
$commandLine = ($Command | ForEach-Object {
|
|
18
|
+
$escaped = $_ -replace '"', '\"'
|
|
19
|
+
if ($escaped -eq '' -or $escaped -match '[\s"&|<>^]') { '"' + $escaped + '"' } else { $escaped }
|
|
20
|
+
}) -join ' '
|
|
21
|
+
|
|
22
|
+
$source = @"
|
|
23
|
+
using System;
|
|
24
|
+
using System.Runtime.InteropServices;
|
|
25
|
+
using System.Text;
|
|
26
|
+
|
|
27
|
+
public static class AboveNormalLauncher
|
|
28
|
+
{
|
|
29
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
30
|
+
struct STARTUPINFO
|
|
31
|
+
{
|
|
32
|
+
public int cb;
|
|
33
|
+
public string lpReserved;
|
|
34
|
+
public string lpDesktop;
|
|
35
|
+
public string lpTitle;
|
|
36
|
+
public int dwX;
|
|
37
|
+
public int dwY;
|
|
38
|
+
public int dwXSize;
|
|
39
|
+
public int dwYSize;
|
|
40
|
+
public int dwXCountChars;
|
|
41
|
+
public int dwYCountChars;
|
|
42
|
+
public int dwFillAttribute;
|
|
43
|
+
public int dwFlags;
|
|
44
|
+
public short wShowWindow;
|
|
45
|
+
public short cbReserved2;
|
|
46
|
+
public IntPtr lpReserved2;
|
|
47
|
+
public IntPtr hStdInput;
|
|
48
|
+
public IntPtr hStdOutput;
|
|
49
|
+
public IntPtr hStdError;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
53
|
+
struct PROCESS_INFORMATION
|
|
54
|
+
{
|
|
55
|
+
public IntPtr hProcess;
|
|
56
|
+
public IntPtr hThread;
|
|
57
|
+
public int dwProcessId;
|
|
58
|
+
public int dwThreadId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
62
|
+
static extern bool CreateProcess(string lpApplicationName, StringBuilder lpCommandLine,
|
|
63
|
+
IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles,
|
|
64
|
+
uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory,
|
|
65
|
+
ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
|
|
66
|
+
|
|
67
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
68
|
+
static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
|
69
|
+
|
|
70
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
71
|
+
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
72
|
+
|
|
73
|
+
[DllImport("kernel32.dll")]
|
|
74
|
+
static extern bool CloseHandle(IntPtr hObject);
|
|
75
|
+
|
|
76
|
+
// Standard MSVCRT/CommandLineToArgvW quoting: safe for a directly-launched .exe's
|
|
77
|
+
// own argv parsing. No cmd.exe involved on this path, so none of its operator or
|
|
78
|
+
// "%" expansion semantics apply - this is the safe path, used whenever possible.
|
|
79
|
+
static string ArgvQuote(string arg)
|
|
80
|
+
{
|
|
81
|
+
if (arg.Length > 0 && arg.IndexOfAny(new char[] { ' ', '\t', '\n', '\v', '"' }) < 0)
|
|
82
|
+
return arg;
|
|
83
|
+
|
|
84
|
+
var result = new StringBuilder();
|
|
85
|
+
result.Append('"');
|
|
86
|
+
int backslashes = 0;
|
|
87
|
+
foreach (char c in arg)
|
|
88
|
+
{
|
|
89
|
+
if (c == '\\')
|
|
90
|
+
{
|
|
91
|
+
backslashes++;
|
|
92
|
+
}
|
|
93
|
+
else if (c == '"')
|
|
94
|
+
{
|
|
95
|
+
result.Append('\\', backslashes * 2 + 1);
|
|
96
|
+
result.Append('"');
|
|
97
|
+
backslashes = 0;
|
|
98
|
+
}
|
|
99
|
+
else
|
|
100
|
+
{
|
|
101
|
+
if (backslashes > 0) { result.Append('\\', backslashes); backslashes = 0; }
|
|
102
|
+
result.Append(c);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (backslashes > 0) result.Append('\\', backslashes * 2);
|
|
106
|
+
result.Append('"');
|
|
107
|
+
return result.ToString();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static string BuildArgvCommandLine(string[] argv)
|
|
111
|
+
{
|
|
112
|
+
var parts = new string[argv.Length];
|
|
113
|
+
for (int i = 0; i < argv.Length; i++) parts[i] = ArgvQuote(argv[i]);
|
|
114
|
+
return string.Join(" ", parts);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Priority is passed via dwCreationFlags, applied atomically at creation - no
|
|
118
|
+
// Job Object needed. Windows' CreateProcess inherits IDLE/BELOW_NORMAL priority
|
|
119
|
+
// by default to children that don't request a priority of their own; ABOVE_NORMAL
|
|
120
|
+
// and higher are NOT inherited by default (see README).
|
|
121
|
+
public static int Run(uint priorityClass, string[] argv, string cmdExeCommandLine)
|
|
122
|
+
{
|
|
123
|
+
var si = new STARTUPINFO();
|
|
124
|
+
si.cb = Marshal.SizeOf(si);
|
|
125
|
+
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
|
126
|
+
|
|
127
|
+
// See cap.ps1 for why .bat/.cmd targets skip the direct attempt entirely:
|
|
128
|
+
// CreateProcess silently re-invokes them through cmd.exe on its own, using
|
|
129
|
+
// unescaped text, instead of failing the way a genuinely missing exe would.
|
|
130
|
+
bool isBatOrCmd = argv.Length > 0 && (
|
|
131
|
+
argv[0].EndsWith(".bat", StringComparison.OrdinalIgnoreCase) ||
|
|
132
|
+
argv[0].EndsWith(".cmd", StringComparison.OrdinalIgnoreCase));
|
|
133
|
+
|
|
134
|
+
bool created = false;
|
|
135
|
+
if (!isBatOrCmd)
|
|
136
|
+
{
|
|
137
|
+
var directCommandLine = new StringBuilder(BuildArgvCommandLine(argv));
|
|
138
|
+
created = CreateProcess(null, directCommandLine, IntPtr.Zero, IntPtr.Zero, true,
|
|
139
|
+
priorityClass, IntPtr.Zero, null, ref si, out pi);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!created)
|
|
143
|
+
{
|
|
144
|
+
// Falling back to cmd.exe /c: a literal "%" in any argument could now
|
|
145
|
+
// trigger environment-variable expansion (cmd.exe pairs up "%" characters
|
|
146
|
+
// across the whole command line, even across separate arguments) and
|
|
147
|
+
// change what actually runs. Fail loudly here instead of silently risking
|
|
148
|
+
// that - there's no reliable per-character escape for "%" at this level.
|
|
149
|
+
foreach (var a in argv)
|
|
150
|
+
{
|
|
151
|
+
if (a.IndexOf('%') >= 0)
|
|
152
|
+
throw new InvalidOperationException(
|
|
153
|
+
"Refusing to run: argument contains '%' and the target needs the cmd.exe " +
|
|
154
|
+
"fallback (not a directly-launchable .exe), where '%' can trigger unintended " +
|
|
155
|
+
"environment-variable expansion. See README's Argument handling section.");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
string cmdExe = Environment.SystemDirectory + "\\cmd.exe";
|
|
159
|
+
// /d: skip HKCU AutoRun (user-writable registry key). /v:off: disable delayed
|
|
160
|
+
// expansion so "!var!" in an argument can't be expanded. /s plus the extra outer
|
|
161
|
+
// quote pair: cmd's /S rule strips exactly that outer pair and leaves the rest of
|
|
162
|
+
// the string untouched - without /S, cmd strips the first and last quote of the
|
|
163
|
+
// whole line instead, which breaks quoting whenever the target path itself needs
|
|
164
|
+
// quotes AND another argument is also quoted.
|
|
165
|
+
var shellCommandLine = new StringBuilder("\"" + cmdExe + "\" /d /v:off /s /c \"" + cmdExeCommandLine + "\"");
|
|
166
|
+
created = CreateProcess(null, shellCommandLine, IntPtr.Zero, IntPtr.Zero, true,
|
|
167
|
+
priorityClass, IntPtr.Zero, null, ref si, out pi);
|
|
168
|
+
if (!created)
|
|
169
|
+
throw new InvalidOperationException("CreateProcess failed: " + Marshal.GetLastWin32Error());
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
WaitForSingleObject(pi.hProcess, 0xFFFFFFFF);
|
|
173
|
+
|
|
174
|
+
uint exitCode;
|
|
175
|
+
GetExitCodeProcess(pi.hProcess, out exitCode);
|
|
176
|
+
|
|
177
|
+
CloseHandle(pi.hThread);
|
|
178
|
+
CloseHandle(pi.hProcess);
|
|
179
|
+
|
|
180
|
+
return (int)exitCode;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
"@
|
|
184
|
+
|
|
185
|
+
Add-Type -TypeDefinition $source -Language CSharp
|
|
186
|
+
|
|
187
|
+
$ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000
|
|
188
|
+
try {
|
|
189
|
+
exit ([AboveNormalLauncher]::Run($ABOVE_NORMAL_PRIORITY_CLASS, [string[]]$Command, $commandLine))
|
|
190
|
+
} catch {
|
|
191
|
+
Write-Error $_.Exception.InnerException.Message
|
|
192
|
+
exit 1
|
|
193
|
+
}
|
package/bin/admin
ADDED
|
@@ -0,0 +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" "$@"
|
package/bin/admin.bat
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
:: SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
:: win-nice: managed-file
|
|
4
|
+
if "%~1"=="" (
|
|
5
|
+
echo usage: admin ^<command^> [args...] 1>&2
|
|
6
|
+
exit /b 1
|
|
7
|
+
)
|
|
8
|
+
:: A literal "%" in any argument gets corrupted here - see cap.bat for why (a
|
|
9
|
+
:: cmd.exe batch-parameter quirk, not fixable from inside a .bat). Every other
|
|
10
|
+
:: cmd.exe metacharacter (&|<>^) survives this hop untouched. Invoking "admin"
|
|
11
|
+
:: bare from an actual PowerShell session skips this file (admin.ps1 preferred).
|
|
12
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0admin.ps1" %*
|
package/bin/admin.ps1
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
2
|
+
# win-nice: managed-file
|
|
3
|
+
$Command = $args
|
|
4
|
+
# Deliberately no [Parameter()]/[CmdletBinding()] attributes: see cap.ps1 for why -
|
|
5
|
+
# it would expose PowerShell's common parameters and make them ambiguously
|
|
6
|
+
# prefix-match flags meant for the wrapped command.
|
|
7
|
+
|
|
8
|
+
if (-not $Command -or $Command.Count -eq 0) {
|
|
9
|
+
Write-Error "usage: admin <command> [args...]"
|
|
10
|
+
exit 1
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
# Fallback command line for the UAC (-Verb RunAs) branch, and for the inline branch
|
|
14
|
+
# when the target isn't a directly-launchable .exe (see AdminLauncher.Run below) -
|
|
15
|
+
# re-parsed by cmd.exe, so quoting must neutralize its operators (&|<>^) and not
|
|
16
|
+
# just whitespace - see cap.ps1 for the same logic and its documented "%" limitation.
|
|
17
|
+
$commandLine = ($Command | ForEach-Object {
|
|
18
|
+
$escaped = $_ -replace '"', '\"'
|
|
19
|
+
if ($escaped -eq '' -or $escaped -match '[\s"&|<>^]') { '"' + $escaped + '"' } else { $escaped }
|
|
20
|
+
}) -join ' '
|
|
21
|
+
|
|
22
|
+
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
23
|
+
|
|
24
|
+
# AdminLauncher (embedded C#): direct-CreateProcess-first, cmd.exe-fallback launcher,
|
|
25
|
+
# same strategy as cap.ps1's Capper - a direct .exe target never touches cmd.exe, so
|
|
26
|
+
# it isn't exposed to "%" expansion at all. Defined unconditionally (not only inside
|
|
27
|
+
# the already-elevated branch below) because the not-yet-elevated branch also calls
|
|
28
|
+
# AdminLauncher.BuildArgvCommandLine for its own direct (non-cmd.exe) -Verb RunAs launch.
|
|
29
|
+
$source = @"
|
|
30
|
+
using System;
|
|
31
|
+
using System.Runtime.InteropServices;
|
|
32
|
+
using System.Text;
|
|
33
|
+
|
|
34
|
+
public static class AdminLauncher
|
|
35
|
+
{
|
|
36
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
37
|
+
struct STARTUPINFO
|
|
38
|
+
{
|
|
39
|
+
public int cb;
|
|
40
|
+
public string lpReserved;
|
|
41
|
+
public string lpDesktop;
|
|
42
|
+
public string lpTitle;
|
|
43
|
+
public int dwX;
|
|
44
|
+
public int dwY;
|
|
45
|
+
public int dwXSize;
|
|
46
|
+
public int dwYSize;
|
|
47
|
+
public int dwXCountChars;
|
|
48
|
+
public int dwYCountChars;
|
|
49
|
+
public int dwFillAttribute;
|
|
50
|
+
public int dwFlags;
|
|
51
|
+
public short wShowWindow;
|
|
52
|
+
public short cbReserved2;
|
|
53
|
+
public IntPtr lpReserved2;
|
|
54
|
+
public IntPtr hStdInput;
|
|
55
|
+
public IntPtr hStdOutput;
|
|
56
|
+
public IntPtr hStdError;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
60
|
+
struct PROCESS_INFORMATION
|
|
61
|
+
{
|
|
62
|
+
public IntPtr hProcess;
|
|
63
|
+
public IntPtr hThread;
|
|
64
|
+
public int dwProcessId;
|
|
65
|
+
public int dwThreadId;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
69
|
+
static extern bool CreateProcess(string lpApplicationName, StringBuilder lpCommandLine,
|
|
70
|
+
IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles,
|
|
71
|
+
uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory,
|
|
72
|
+
ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
|
|
73
|
+
|
|
74
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
75
|
+
static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
|
76
|
+
|
|
77
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
78
|
+
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
79
|
+
|
|
80
|
+
[DllImport("kernel32.dll")]
|
|
81
|
+
static extern bool CloseHandle(IntPtr hObject);
|
|
82
|
+
|
|
83
|
+
static string ArgvQuote(string arg)
|
|
84
|
+
{
|
|
85
|
+
if (arg.Length > 0 && arg.IndexOfAny(new char[] { ' ', '\t', '\n', '\v', '"' }) < 0)
|
|
86
|
+
return arg;
|
|
87
|
+
|
|
88
|
+
var result = new StringBuilder();
|
|
89
|
+
result.Append('"');
|
|
90
|
+
int backslashes = 0;
|
|
91
|
+
foreach (char c in arg)
|
|
92
|
+
{
|
|
93
|
+
if (c == '\\')
|
|
94
|
+
{
|
|
95
|
+
backslashes++;
|
|
96
|
+
}
|
|
97
|
+
else if (c == '"')
|
|
98
|
+
{
|
|
99
|
+
result.Append('\\', backslashes * 2 + 1);
|
|
100
|
+
result.Append('"');
|
|
101
|
+
backslashes = 0;
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
{
|
|
105
|
+
if (backslashes > 0) { result.Append('\\', backslashes); backslashes = 0; }
|
|
106
|
+
result.Append(c);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (backslashes > 0) result.Append('\\', backslashes * 2);
|
|
110
|
+
result.Append('"');
|
|
111
|
+
return result.ToString();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Public: reused from PowerShell by the not-yet-elevated branch to build the
|
|
115
|
+
// -ArgumentList for a direct (non-cmd.exe) -Verb RunAs launch, so that path gets
|
|
116
|
+
// the same CRT argv quoting as this file's own direct-CreateProcess path.
|
|
117
|
+
public static string BuildArgvCommandLine(string[] argv)
|
|
118
|
+
{
|
|
119
|
+
var parts = new string[argv.Length];
|
|
120
|
+
for (int i = 0; i < argv.Length; i++) parts[i] = ArgvQuote(argv[i]);
|
|
121
|
+
return string.Join(" ", parts);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public static int Run(string[] argv, string cmdExeCommandLine)
|
|
125
|
+
{
|
|
126
|
+
var si = new STARTUPINFO();
|
|
127
|
+
si.cb = Marshal.SizeOf(si);
|
|
128
|
+
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
|
129
|
+
|
|
130
|
+
bool isBatOrCmd = argv.Length > 0 && (
|
|
131
|
+
argv[0].EndsWith(".bat", StringComparison.OrdinalIgnoreCase) ||
|
|
132
|
+
argv[0].EndsWith(".cmd", StringComparison.OrdinalIgnoreCase));
|
|
133
|
+
|
|
134
|
+
bool created = false;
|
|
135
|
+
if (!isBatOrCmd)
|
|
136
|
+
{
|
|
137
|
+
var directCommandLine = new StringBuilder(BuildArgvCommandLine(argv));
|
|
138
|
+
created = CreateProcess(null, directCommandLine, IntPtr.Zero, IntPtr.Zero, true,
|
|
139
|
+
0, IntPtr.Zero, null, ref si, out pi);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!created)
|
|
143
|
+
{
|
|
144
|
+
// Falling back to cmd.exe /c: a literal "%" in any argument could now
|
|
145
|
+
// trigger environment-variable expansion (cmd.exe pairs up "%" characters
|
|
146
|
+
// across the whole command line, even across separate arguments) and
|
|
147
|
+
// change what actually runs. Fail loudly here instead of silently risking
|
|
148
|
+
// that - there's no reliable per-character escape for "%" at this level.
|
|
149
|
+
foreach (var a in argv)
|
|
150
|
+
{
|
|
151
|
+
if (a.IndexOf('%') >= 0)
|
|
152
|
+
throw new InvalidOperationException(
|
|
153
|
+
"Refusing to run: argument contains '%' and the target needs the cmd.exe " +
|
|
154
|
+
"fallback (not a directly-launchable .exe), where '%' can trigger unintended " +
|
|
155
|
+
"environment-variable expansion. See README's Argument handling section.");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// /d /s /v:off plus wrapping cmdExeCommandLine in one more outer quote pair:
|
|
159
|
+
// cmd.exe's /C quote-stripping only cleanly strips the outer pair when it's
|
|
160
|
+
// the sole/last quote pair on the line; with cmdExeCommandLine's own internal
|
|
161
|
+
// quoted args present, cmd's "exactly two quotes" rule doesn't apply and it
|
|
162
|
+
// falls back to stripping the first char and the LAST quote anywhere on the
|
|
163
|
+
// line - which, without this extra wrap, is one of OUR internal quotes and
|
|
164
|
+
// corrupts the parse (reopening "&" injection). The extra pair guarantees the
|
|
165
|
+
// added closing quote is the true last character, so strip-first/strip-last
|
|
166
|
+
// removes exactly our wrap and nothing else. /v:off pre-empts delayed-expansion
|
|
167
|
+
// ("!VAR!") risk the same way the "%" check above pre-empts "%" expansion.
|
|
168
|
+
string cmdExe = Environment.SystemDirectory + "\\cmd.exe";
|
|
169
|
+
var shellCommandLine = new StringBuilder(
|
|
170
|
+
"\"" + cmdExe + "\" /d /s /v:off /c \"" + cmdExeCommandLine + "\"");
|
|
171
|
+
created = CreateProcess(null, shellCommandLine, IntPtr.Zero, IntPtr.Zero, true,
|
|
172
|
+
0, IntPtr.Zero, null, ref si, out pi);
|
|
173
|
+
if (!created)
|
|
174
|
+
throw new InvalidOperationException("CreateProcess failed: " + Marshal.GetLastWin32Error());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
WaitForSingleObject(pi.hProcess, 0xFFFFFFFF);
|
|
178
|
+
|
|
179
|
+
uint exitCode;
|
|
180
|
+
GetExitCodeProcess(pi.hProcess, out exitCode);
|
|
181
|
+
|
|
182
|
+
CloseHandle(pi.hThread);
|
|
183
|
+
CloseHandle(pi.hProcess);
|
|
184
|
+
|
|
185
|
+
return (int)exitCode;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
"@
|
|
189
|
+
Add-Type -TypeDefinition $source -Language CSharp
|
|
190
|
+
|
|
191
|
+
if ($isAdmin) {
|
|
192
|
+
# Already elevated - launch inline, sharing the current console.
|
|
193
|
+
try {
|
|
194
|
+
exit ([AdminLauncher]::Run([string[]]$Command, $commandLine))
|
|
195
|
+
} catch {
|
|
196
|
+
Write-Error $_.Exception.InnerException.Message
|
|
197
|
+
exit 1
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Not elevated - -Verb RunAs triggers the UAC consent prompt. ShellExecute-based, not
|
|
202
|
+
# CreateProcess, so this always opens its own console window (incompatible with
|
|
203
|
+
# -NoNewWindow). Same direct-launch-first, cmd.exe-fallback strategy as the
|
|
204
|
+
# already-elevated branch above (AdminLauncher.Run): a target that resolves to a real
|
|
205
|
+
# Application (.exe) launches directly via -FilePath - never touching cmd.exe and so
|
|
206
|
+
# never exposed to "%"/quote-stripping risk - while a .bat/.cmd target (no direct
|
|
207
|
+
# elevation-capable equivalent to CreateProcess's own .bat/.cmd auto-relaunch) and a
|
|
208
|
+
# target that resolves to no Application at all both go through the cmd.exe fallback
|
|
209
|
+
# below. The unresolvable case is the important one: cmd.exe BUILTINS (ver, dir,
|
|
210
|
+
# echo, set, start, ...) aren't files, so Start-Process -FilePath would die with
|
|
211
|
+
# "The system cannot find the file specified" before any UAC prompt - they must take
|
|
212
|
+
# the fallback like every other launcher's CreateProcess-failed branch does.
|
|
213
|
+
# Get-Command with -CommandType Application is the resolver: builtins and PowerShell
|
|
214
|
+
# aliases/functions (dir, echo, start) are invisible to it, which routes them to the
|
|
215
|
+
# fallback, while real executables resolve. Standalone function so the routing
|
|
216
|
+
# decision is testable without ever reaching -Verb RunAs (a real UAC prompt).
|
|
217
|
+
function Get-AdminLaunchRoute {
|
|
218
|
+
param([Parameter(Mandatory = $true)][string]$Target)
|
|
219
|
+
if ($Target -match '\.(bat|cmd)$') { return 'CmdFallback' }
|
|
220
|
+
$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)$') { return 'Direct' }
|
|
222
|
+
return 'CmdFallback'
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
$route = Get-AdminLaunchRoute -Target $Command[0]
|
|
226
|
+
|
|
227
|
+
if ($route -eq 'CmdFallback') {
|
|
228
|
+
foreach ($a in $Command) {
|
|
229
|
+
if ("$a".Contains('%')) {
|
|
230
|
+
Write-Error "Refusing to run: argument contains '%', which cmd.exe could expand as an environment variable during elevation. See README's Argument handling section."
|
|
231
|
+
exit 1
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
if ($route -eq 'CmdFallback') {
|
|
238
|
+
# Same /d /s /v:off + outer-quote-wrap fix as AdminLauncher.Run's cmd.exe
|
|
239
|
+
# fallback, and for the same reason: cmd.exe's /C quote-stripping.
|
|
240
|
+
$p = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/d', '/s', '/v:off', '/c', ('"' + $commandLine + '"')) -Verb RunAs -Wait -PassThru
|
|
241
|
+
} else {
|
|
242
|
+
$startArgs = @{
|
|
243
|
+
FilePath = $Command[0]
|
|
244
|
+
Verb = 'RunAs'
|
|
245
|
+
Wait = $true
|
|
246
|
+
PassThru = $true
|
|
247
|
+
}
|
|
248
|
+
if ($Command.Count -gt 1) {
|
|
249
|
+
$rest = [string[]]$Command[1..($Command.Count - 1)]
|
|
250
|
+
$startArgs.ArgumentList = [AdminLauncher]::BuildArgvCommandLine($rest)
|
|
251
|
+
}
|
|
252
|
+
$p = Start-Process @startArgs
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
Write-Error "Elevation was cancelled or failed: $($_.Exception.Message)"
|
|
256
|
+
exit 1
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
exit $p.ExitCode
|
package/bin/belownormal
ADDED
|
@@ -0,0 +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" "$@"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
:: SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
:: win-nice: managed-file
|
|
4
|
+
if "%~1"=="" (
|
|
5
|
+
echo usage: belownormal ^<command^> [args...] 1>&2
|
|
6
|
+
exit /b 1
|
|
7
|
+
)
|
|
8
|
+
:: A literal "%" in any argument gets corrupted here - see cap.bat for why (a
|
|
9
|
+
:: cmd.exe batch-parameter quirk, not fixable from inside a .bat). Every other
|
|
10
|
+
:: cmd.exe metacharacter (&|<>^) survives this hop untouched. Invoking
|
|
11
|
+
:: "belownormal" bare from an actual PowerShell session skips this file
|
|
12
|
+
:: (belownormal.ps1 preferred).
|
|
13
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0belownormal.ps1" %*
|