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/capm.ps1
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
2
|
+
# win-nice: managed-file
|
|
3
|
+
# Deliberately no param()/[CmdletBinding()]: a declared parameter name (even
|
|
4
|
+
# without a [Parameter()] attribute) can still be ambiguously prefix-matched by
|
|
5
|
+
# flags meant for the wrapped command (e.g. "-s" matching "-Size"). Reading
|
|
6
|
+
# everything from $args sidesteps PowerShell's parameter binder entirely.
|
|
7
|
+
$usage = "usage: capm <size> <command> [args...] (size: plain integer 1-100 " +
|
|
8
|
+
"= percent of total physical RAM, e.g. 50 - same convention as capc's " +
|
|
9
|
+
"<percent 1-100>; number+m/M = MB, e.g. 512m; number+g/G = GB, e.g. 2g)"
|
|
10
|
+
|
|
11
|
+
if ($args.Count -lt 2) {
|
|
12
|
+
Write-Error $usage
|
|
13
|
+
exit 1
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
$sizeArg = $args[0]
|
|
17
|
+
# No "%" suffix on purpose, unlike capc/capt's own numeric-only args this one
|
|
18
|
+
# could otherwise carry a unit character - but capm is meant to be chainable
|
|
19
|
+
# with the other tools by bare name (e.g. "capc 50 capm 50 <command>"), and a
|
|
20
|
+
# "%" in an argument trips every tool's fail-closed check the moment a chain
|
|
21
|
+
# hop needs the cmd.exe fallback (which bare-name resolution always does,
|
|
22
|
+
# since none of these ship a .exe) - so "capc 50 capm 25% ..." used to fail
|
|
23
|
+
# while "capm 25% capc 50 ..." worked, an order-dependent foot-gun. A bare
|
|
24
|
+
# integer (capc's own convention) sidesteps that entirely.
|
|
25
|
+
if ($sizeArg -notmatch '^(?<num>\d+(\.\d+)?)(?<unit>[mMgG]?)$') {
|
|
26
|
+
Write-Error $usage
|
|
27
|
+
exit 1
|
|
28
|
+
}
|
|
29
|
+
# TryParse, not a raw [double] cast: an arbitrarily long digit string (the
|
|
30
|
+
# regex above has no length limit) overflows a plain [double] cast with a
|
|
31
|
+
# raw, unhandled PowerShell conversion error (path/line number and all) -
|
|
32
|
+
# TryParse fails cleanly instead, so every invalid <size> hits the same
|
|
33
|
+
# single usage message regardless of why it's invalid.
|
|
34
|
+
$sizeNum = 0.0
|
|
35
|
+
$numOk = [double]::TryParse($Matches['num'], [System.Globalization.NumberStyles]::Float,
|
|
36
|
+
[System.Globalization.CultureInfo]::InvariantCulture, [ref]$sizeNum)
|
|
37
|
+
if (-not $numOk -or [double]::IsNaN($sizeNum) -or [double]::IsInfinity($sizeNum)) {
|
|
38
|
+
Write-Error "capm: <size> is out of range. $usage"
|
|
39
|
+
exit 1
|
|
40
|
+
}
|
|
41
|
+
$sizeUnit = $Matches['unit']
|
|
42
|
+
if ($sizeUnit -eq '') {
|
|
43
|
+
# No suffix: percent of total RAM, matching capc's own <percent 1-100>
|
|
44
|
+
# exactly - a plain integer only (TryParse rejects "50.5"), folded into
|
|
45
|
+
# the internal "%" conversion path below ("%" is never a valid *input*
|
|
46
|
+
# character here - see above - only an internal marker for that path).
|
|
47
|
+
$percentValue = 0
|
|
48
|
+
if (-not [int]::TryParse($sizeArg, [ref]$percentValue) -or $percentValue -lt 1 -or $percentValue -gt 100) {
|
|
49
|
+
Write-Error $usage
|
|
50
|
+
exit 1
|
|
51
|
+
}
|
|
52
|
+
$sizeUnit = '%'
|
|
53
|
+
$sizeNum = $percentValue
|
|
54
|
+
} elseif ($sizeNum -le 0) {
|
|
55
|
+
Write-Error $usage
|
|
56
|
+
exit 1
|
|
57
|
+
}
|
|
58
|
+
$Command = @($args[1..($args.Count - 1)])
|
|
59
|
+
|
|
60
|
+
# Fallback command line for when the target isn't a directly-launchable .exe (see
|
|
61
|
+
# CapmLauncher.Run below) - re-parsed by cmd.exe (via "cmd.exe /c"), so quoting must
|
|
62
|
+
# neutralize its operators (&|<>^) and not just whitespace - see capc.ps1 for the
|
|
63
|
+
# same logic and its documented "%" limitation.
|
|
64
|
+
$commandLine = ($Command | ForEach-Object {
|
|
65
|
+
$escaped = $_ -replace '"', '\"'
|
|
66
|
+
if ($escaped -eq '' -or $escaped -match '[\s"&|<>^]') { '"' + $escaped + '"' } else { $escaped }
|
|
67
|
+
}) -join ' '
|
|
68
|
+
|
|
69
|
+
$source = @"
|
|
70
|
+
using System;
|
|
71
|
+
using System.Runtime.InteropServices;
|
|
72
|
+
using System.Text;
|
|
73
|
+
|
|
74
|
+
public static class CapmLauncher
|
|
75
|
+
{
|
|
76
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
77
|
+
struct STARTUPINFO
|
|
78
|
+
{
|
|
79
|
+
public int cb;
|
|
80
|
+
public string lpReserved;
|
|
81
|
+
public string lpDesktop;
|
|
82
|
+
public string lpTitle;
|
|
83
|
+
public int dwX;
|
|
84
|
+
public int dwY;
|
|
85
|
+
public int dwXSize;
|
|
86
|
+
public int dwYSize;
|
|
87
|
+
public int dwXCountChars;
|
|
88
|
+
public int dwYCountChars;
|
|
89
|
+
public int dwFillAttribute;
|
|
90
|
+
public int dwFlags;
|
|
91
|
+
public short wShowWindow;
|
|
92
|
+
public short cbReserved2;
|
|
93
|
+
public IntPtr lpReserved2;
|
|
94
|
+
public IntPtr hStdInput;
|
|
95
|
+
public IntPtr hStdOutput;
|
|
96
|
+
public IntPtr hStdError;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
100
|
+
struct PROCESS_INFORMATION
|
|
101
|
+
{
|
|
102
|
+
public IntPtr hProcess;
|
|
103
|
+
public IntPtr hThread;
|
|
104
|
+
public int dwProcessId;
|
|
105
|
+
public int dwThreadId;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
109
|
+
struct JOBOBJECT_BASIC_LIMIT_INFORMATION
|
|
110
|
+
{
|
|
111
|
+
public long PerProcessUserTimeLimit;
|
|
112
|
+
public long PerJobUserTimeLimit;
|
|
113
|
+
public uint LimitFlags;
|
|
114
|
+
public UIntPtr MinimumWorkingSetSize;
|
|
115
|
+
public UIntPtr MaximumWorkingSetSize;
|
|
116
|
+
public uint ActiveProcessLimit;
|
|
117
|
+
public UIntPtr Affinity;
|
|
118
|
+
public uint PriorityClass;
|
|
119
|
+
public uint SchedulingClass;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
123
|
+
struct IO_COUNTERS
|
|
124
|
+
{
|
|
125
|
+
public ulong ReadOperationCount;
|
|
126
|
+
public ulong WriteOperationCount;
|
|
127
|
+
public ulong OtherOperationCount;
|
|
128
|
+
public ulong ReadTransferCount;
|
|
129
|
+
public ulong WriteTransferCount;
|
|
130
|
+
public ulong OtherTransferCount;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
134
|
+
struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
|
135
|
+
{
|
|
136
|
+
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
|
137
|
+
public IO_COUNTERS IoInfo;
|
|
138
|
+
public UIntPtr ProcessMemoryLimit;
|
|
139
|
+
public UIntPtr JobMemoryLimit;
|
|
140
|
+
public UIntPtr PeakProcessMemoryUsed;
|
|
141
|
+
public UIntPtr PeakJobMemoryUsed;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
145
|
+
struct MEMORYSTATUSEX
|
|
146
|
+
{
|
|
147
|
+
public uint dwLength;
|
|
148
|
+
public uint dwMemoryLoad;
|
|
149
|
+
public ulong ullTotalPhys;
|
|
150
|
+
public ulong ullAvailPhys;
|
|
151
|
+
public ulong ullTotalPageFile;
|
|
152
|
+
public ulong ullAvailPageFile;
|
|
153
|
+
public ulong ullTotalVirtual;
|
|
154
|
+
public ulong ullAvailVirtual;
|
|
155
|
+
public ulong ullAvailExtendedVirtual;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
159
|
+
static extern bool CreateProcess(string lpApplicationName, StringBuilder lpCommandLine,
|
|
160
|
+
IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles,
|
|
161
|
+
uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory,
|
|
162
|
+
ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
|
|
163
|
+
|
|
164
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
165
|
+
static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName);
|
|
166
|
+
|
|
167
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
168
|
+
static extern bool SetInformationJobObject(IntPtr hJob, int JobObjectInfoClass, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
|
|
169
|
+
|
|
170
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
171
|
+
static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
|
|
172
|
+
|
|
173
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
174
|
+
static extern uint ResumeThread(IntPtr hThread);
|
|
175
|
+
|
|
176
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
177
|
+
static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
|
178
|
+
|
|
179
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
180
|
+
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
181
|
+
|
|
182
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
183
|
+
static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
|
|
184
|
+
|
|
185
|
+
[DllImport("kernel32.dll")]
|
|
186
|
+
static extern bool CloseHandle(IntPtr hObject);
|
|
187
|
+
|
|
188
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
189
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
190
|
+
static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
|
|
191
|
+
|
|
192
|
+
const uint CREATE_SUSPENDED = 0x00000004;
|
|
193
|
+
const int JobObjectExtendedLimitInformation = 9;
|
|
194
|
+
const uint JOB_OBJECT_LIMIT_JOB_MEMORY = 0x00000200;
|
|
195
|
+
const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
|
196
|
+
|
|
197
|
+
// Used by the PowerShell side to resolve a "<N>%" size argument into bytes
|
|
198
|
+
// before Run() is ever called - the percentage is relative to total physical
|
|
199
|
+
// RAM, not whatever's currently free, so the cap means the same thing
|
|
200
|
+
// regardless of what else is running on the machine at invocation time.
|
|
201
|
+
public static ulong GetTotalPhysicalMemoryBytes()
|
|
202
|
+
{
|
|
203
|
+
var status = new MEMORYSTATUSEX();
|
|
204
|
+
status.dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
|
|
205
|
+
if (!GlobalMemoryStatusEx(ref status))
|
|
206
|
+
throw new InvalidOperationException("GlobalMemoryStatusEx failed: " + Marshal.GetLastWin32Error());
|
|
207
|
+
return status.ullTotalPhys;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Standard MSVCRT/CommandLineToArgvW quoting: safe for a directly-launched .exe's
|
|
211
|
+
// own argv parsing. No cmd.exe involved on this path, so none of its operator or
|
|
212
|
+
// "%" expansion semantics apply - this is the safe path, used whenever possible.
|
|
213
|
+
static string ArgvQuote(string arg)
|
|
214
|
+
{
|
|
215
|
+
if (arg.Length > 0 && arg.IndexOfAny(new char[] { ' ', '\t', '\n', '\v', '"' }) < 0)
|
|
216
|
+
return arg;
|
|
217
|
+
|
|
218
|
+
var result = new StringBuilder();
|
|
219
|
+
result.Append('"');
|
|
220
|
+
int backslashes = 0;
|
|
221
|
+
foreach (char c in arg)
|
|
222
|
+
{
|
|
223
|
+
if (c == '\\')
|
|
224
|
+
{
|
|
225
|
+
backslashes++;
|
|
226
|
+
}
|
|
227
|
+
else if (c == '"')
|
|
228
|
+
{
|
|
229
|
+
result.Append('\\', backslashes * 2 + 1);
|
|
230
|
+
result.Append('"');
|
|
231
|
+
backslashes = 0;
|
|
232
|
+
}
|
|
233
|
+
else
|
|
234
|
+
{
|
|
235
|
+
if (backslashes > 0) { result.Append('\\', backslashes); backslashes = 0; }
|
|
236
|
+
result.Append(c);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (backslashes > 0) result.Append('\\', backslashes * 2);
|
|
240
|
+
result.Append('"');
|
|
241
|
+
return result.ToString();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
static string BuildArgvCommandLine(string[] argv)
|
|
245
|
+
{
|
|
246
|
+
var parts = new string[argv.Length];
|
|
247
|
+
for (int i = 0; i < argv.Length; i++) parts[i] = ArgvQuote(argv[i]);
|
|
248
|
+
return string.Join(" ", parts);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
public static int Run(ulong memoryLimitBytes, string[] argv, string cmdExeCommandLine)
|
|
252
|
+
{
|
|
253
|
+
IntPtr hJob = CreateJobObject(IntPtr.Zero, null);
|
|
254
|
+
if (hJob == IntPtr.Zero)
|
|
255
|
+
throw new InvalidOperationException("CreateJobObject failed: " + Marshal.GetLastWin32Error());
|
|
256
|
+
|
|
257
|
+
// Single owner for every handle this method acquires. The finally below closes
|
|
258
|
+
// hThread/hProcess/hJob - in that order - on EVERY way out: normal return, any
|
|
259
|
+
// of the InvalidOperationExceptions thrown here, and an unexpected managed
|
|
260
|
+
// exception (allocation/marshalling failure) between acquisition and use.
|
|
261
|
+
// hProcess/hThread stay IntPtr.Zero until CreateProcess has actually succeeded,
|
|
262
|
+
// so each handle is closed exactly once and only if it was really acquired.
|
|
263
|
+
IntPtr hProcess = IntPtr.Zero;
|
|
264
|
+
IntPtr hThread = IntPtr.Zero;
|
|
265
|
+
try
|
|
266
|
+
{
|
|
267
|
+
// JOB_OBJECT_LIMIT_JOB_MEMORY caps the whole job's aggregate committed memory,
|
|
268
|
+
// not any single process - same "whole subtree, one ceiling" semantics as
|
|
269
|
+
// capc's CPU% and capt's affinity, not a per-process limit. Exceeding it fails
|
|
270
|
+
// the allocation call that would have breached it (VirtualAlloc-family APIs
|
|
271
|
+
// return an error / .NET throws OutOfMemoryException) - Windows doesn't kill
|
|
272
|
+
// the process outright, it just refuses to hand out more committed memory;
|
|
273
|
+
// most programs don't handle that gracefully, so it usually looks like a
|
|
274
|
+
// crash in practice, but it's the allocation failing, not an OS-issued kill.
|
|
275
|
+
var extInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
|
276
|
+
{
|
|
277
|
+
BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
|
|
278
|
+
{
|
|
279
|
+
// KILL_ON_JOB_CLOSE: the cleanup in the finally below only runs
|
|
280
|
+
// if this launcher process survives to execute it. Killed from
|
|
281
|
+
// outside (taskkill without /T, a crash), nothing in-process
|
|
282
|
+
// ever runs - without this flag the last job handle dying with
|
|
283
|
+
// the process would leave every process still assigned to the
|
|
284
|
+
// job running on, untracked and unmanaged. With it, Windows
|
|
285
|
+
// itself terminates the whole job at that moment.
|
|
286
|
+
//
|
|
287
|
+
// A backstop only, never the normal exit mechanism: the last
|
|
288
|
+
// handle closing terminates every process still assigned to
|
|
289
|
+
// the job FOR ANY reason, including this wrapper's own
|
|
290
|
+
// orderly close in the finally - and the wait below only
|
|
291
|
+
// waits on the directly wrapped root process, so a daemon it
|
|
292
|
+
// spawned and left running can still be in the job at that
|
|
293
|
+
// point, the root long gone. Killing a daemon on a SUCCESSFUL
|
|
294
|
+
// exit would break the documented daemon-survival contract
|
|
295
|
+
// (README: the memory ceiling sticks to any daemon the
|
|
296
|
+
// wrapped command leaves running, for that daemon's whole
|
|
297
|
+
// lifetime), so the success path below clears this flag
|
|
298
|
+
// first - see capc.ps1 for the full write-up.
|
|
299
|
+
LimitFlags = JOB_OBJECT_LIMIT_JOB_MEMORY | JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
|
300
|
+
},
|
|
301
|
+
JobMemoryLimit = (UIntPtr)memoryLimitBytes
|
|
302
|
+
};
|
|
303
|
+
int size = Marshal.SizeOf(extInfo);
|
|
304
|
+
IntPtr ptr = Marshal.AllocHGlobal(size);
|
|
305
|
+
bool ok;
|
|
306
|
+
try
|
|
307
|
+
{
|
|
308
|
+
Marshal.StructureToPtr(extInfo, ptr, false);
|
|
309
|
+
ok = SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, ptr, (uint)size);
|
|
310
|
+
}
|
|
311
|
+
finally
|
|
312
|
+
{
|
|
313
|
+
Marshal.FreeHGlobal(ptr);
|
|
314
|
+
}
|
|
315
|
+
if (!ok)
|
|
316
|
+
throw new InvalidOperationException("SetInformationJobObject failed: " + Marshal.GetLastWin32Error());
|
|
317
|
+
|
|
318
|
+
var si = new STARTUPINFO();
|
|
319
|
+
si.cb = Marshal.SizeOf(si);
|
|
320
|
+
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
|
321
|
+
|
|
322
|
+
// See capc.ps1 for why .bat/.cmd targets skip the direct attempt entirely:
|
|
323
|
+
// CreateProcess silently re-invokes them through cmd.exe on its own, using
|
|
324
|
+
// unescaped text, instead of failing the way a genuinely missing exe would.
|
|
325
|
+
bool isBatOrCmd = argv.Length > 0 && (
|
|
326
|
+
argv[0].EndsWith(".bat", StringComparison.OrdinalIgnoreCase) ||
|
|
327
|
+
argv[0].EndsWith(".cmd", StringComparison.OrdinalIgnoreCase));
|
|
328
|
+
|
|
329
|
+
bool created = false;
|
|
330
|
+
if (!isBatOrCmd)
|
|
331
|
+
{
|
|
332
|
+
var directCommandLine = new StringBuilder(BuildArgvCommandLine(argv));
|
|
333
|
+
created = CreateProcess(null, directCommandLine, IntPtr.Zero, IntPtr.Zero, true,
|
|
334
|
+
CREATE_SUSPENDED, IntPtr.Zero, null, ref si, out pi);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!created)
|
|
338
|
+
{
|
|
339
|
+
// Falling back to cmd.exe /c: a literal "%" in any argument could now
|
|
340
|
+
// trigger environment-variable expansion (cmd.exe pairs up "%" characters
|
|
341
|
+
// across the whole command line, even across separate arguments) and
|
|
342
|
+
// change what actually runs. Fail loudly here instead of silently risking
|
|
343
|
+
// that - there's no reliable per-character escape for "%" at this level.
|
|
344
|
+
foreach (var a in argv)
|
|
345
|
+
{
|
|
346
|
+
if (a.IndexOf('%') >= 0)
|
|
347
|
+
throw new InvalidOperationException(
|
|
348
|
+
"Refusing to run: argument contains '%' and the target needs the cmd.exe " +
|
|
349
|
+
"fallback (not a directly-launchable .exe), where '%' can trigger unintended " +
|
|
350
|
+
"environment-variable expansion. See README's Argument handling section.");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
string cmdExe = Environment.SystemDirectory + "\\cmd.exe";
|
|
354
|
+
// /d: skip HKCU AutoRun (user-writable registry key). /v:off: disable delayed
|
|
355
|
+
// expansion so "!var!" in an argument can't be expanded. /s plus the extra outer
|
|
356
|
+
// quote pair: cmd's /S rule strips exactly that outer pair and leaves the rest of
|
|
357
|
+
// the string untouched - without /S, cmd strips the first and last quote of the
|
|
358
|
+
// whole line instead, which breaks quoting whenever the target path itself needs
|
|
359
|
+
// quotes AND another argument is also quoted.
|
|
360
|
+
var shellCommandLine = new StringBuilder("\"" + cmdExe + "\" /d /v:off /s /c \"" + cmdExeCommandLine + "\"");
|
|
361
|
+
created = CreateProcess(null, shellCommandLine, IntPtr.Zero, IntPtr.Zero, true,
|
|
362
|
+
CREATE_SUSPENDED, IntPtr.Zero, null, ref si, out pi);
|
|
363
|
+
if (!created)
|
|
364
|
+
throw new InvalidOperationException("CreateProcess failed: " + Marshal.GetLastWin32Error());
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Ownership of the child's handles transfers here, once CreateProcess has
|
|
368
|
+
// actually succeeded - from this point the finally below is what closes them.
|
|
369
|
+
hProcess = pi.hProcess;
|
|
370
|
+
hThread = pi.hThread;
|
|
371
|
+
|
|
372
|
+
if (!AssignProcessToJobObject(hJob, hProcess))
|
|
373
|
+
{
|
|
374
|
+
// Can't guarantee the cap - kill instead of letting it run uncapped and orphaned.
|
|
375
|
+
int err = Marshal.GetLastWin32Error();
|
|
376
|
+
string message = "AssignProcessToJobObject failed: " + err;
|
|
377
|
+
// Report if the best-effort kill itself also failed.
|
|
378
|
+
if (!TerminateProcess(hProcess, 1))
|
|
379
|
+
message += "; TerminateProcess also failed: " + Marshal.GetLastWin32Error();
|
|
380
|
+
throw new InvalidOperationException(message);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (ResumeThread(hThread) == 0xFFFFFFFF)
|
|
384
|
+
{
|
|
385
|
+
// Still suspended - an unbounded wait below would hang forever. Kill
|
|
386
|
+
// it instead of leaving an orphaned, permanently-suspended process.
|
|
387
|
+
int resumeErr = Marshal.GetLastWin32Error();
|
|
388
|
+
string message = "ResumeThread failed: " + resumeErr;
|
|
389
|
+
// Report if the best-effort kill itself also failed.
|
|
390
|
+
if (!TerminateProcess(hProcess, 1))
|
|
391
|
+
message += "; TerminateProcess also failed: " + Marshal.GetLastWin32Error();
|
|
392
|
+
throw new InvalidOperationException(message);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (WaitForSingleObject(hProcess, 0xFFFFFFFF) == 0xFFFFFFFF)
|
|
396
|
+
{
|
|
397
|
+
// The child's actual state is unknown here - don't just report
|
|
398
|
+
// failure and potentially leave it running unmanaged in the
|
|
399
|
+
// background. Best-effort kill before giving up.
|
|
400
|
+
int waitErr = Marshal.GetLastWin32Error();
|
|
401
|
+
string message = "WaitForSingleObject failed: " + waitErr;
|
|
402
|
+
// Report if the best-effort kill itself also failed.
|
|
403
|
+
if (!TerminateProcess(hProcess, 1))
|
|
404
|
+
message += "; TerminateProcess also failed: " + Marshal.GetLastWin32Error();
|
|
405
|
+
throw new InvalidOperationException(message);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
uint exitCode;
|
|
409
|
+
if (!GetExitCodeProcess(hProcess, out exitCode))
|
|
410
|
+
throw new InvalidOperationException("GetExitCodeProcess failed: " + Marshal.GetLastWin32Error());
|
|
411
|
+
|
|
412
|
+
// Normal success: the root process finished - release the
|
|
413
|
+
// kill-on-close backstop so the finally below closes hJob WITHOUT
|
|
414
|
+
// terminating anything still assigned to the job (the documented
|
|
415
|
+
// daemon-survival contract: the memory ceiling keeps applying to
|
|
416
|
+
// whatever the command left running, it just outlives this
|
|
417
|
+
// wrapper's handle). Deliberately best-effort, not a throw: the
|
|
418
|
+
// wrapped command succeeded, and a failed cleanup syscall must not
|
|
419
|
+
// turn its real exit code below into a wrapper error - warn on
|
|
420
|
+
// stderr instead. Same struct and field values as the original
|
|
421
|
+
// set above, minus the kill-on-close bit, so the daemon keeps its
|
|
422
|
+
// memory ceiling.
|
|
423
|
+
var releaseInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
|
424
|
+
{
|
|
425
|
+
BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
|
|
426
|
+
{
|
|
427
|
+
LimitFlags = JOB_OBJECT_LIMIT_JOB_MEMORY
|
|
428
|
+
},
|
|
429
|
+
JobMemoryLimit = (UIntPtr)memoryLimitBytes
|
|
430
|
+
};
|
|
431
|
+
int releaseSize = Marshal.SizeOf(releaseInfo);
|
|
432
|
+
IntPtr releasePtr = Marshal.AllocHGlobal(releaseSize);
|
|
433
|
+
bool releaseOk;
|
|
434
|
+
int releaseErr = 0;
|
|
435
|
+
try
|
|
436
|
+
{
|
|
437
|
+
Marshal.StructureToPtr(releaseInfo, releasePtr, false);
|
|
438
|
+
releaseOk = SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, releasePtr, (uint)releaseSize);
|
|
439
|
+
// Capture the Win32 error immediately, before any other call can
|
|
440
|
+
// overwrite it - every other native failure branch in this file
|
|
441
|
+
// reports the code for the same diagnosability reason.
|
|
442
|
+
if (!releaseOk)
|
|
443
|
+
releaseErr = Marshal.GetLastWin32Error();
|
|
444
|
+
}
|
|
445
|
+
finally
|
|
446
|
+
{
|
|
447
|
+
Marshal.FreeHGlobal(releasePtr);
|
|
448
|
+
}
|
|
449
|
+
if (!releaseOk)
|
|
450
|
+
Console.Error.WriteLine("warning: could not release the job's kill-on-close guard (SetInformationJobObject failed with Win32 error " + releaseErr + ") - a still-running background process left by the wrapped command may be terminated when this wrapper exits");
|
|
451
|
+
|
|
452
|
+
return (int)exitCode;
|
|
453
|
+
}
|
|
454
|
+
finally
|
|
455
|
+
{
|
|
456
|
+
// Same order as the code this replaces: thread handle, process handle, job handle.
|
|
457
|
+
if (hThread != IntPtr.Zero) CloseHandle(hThread);
|
|
458
|
+
if (hProcess != IntPtr.Zero) CloseHandle(hProcess);
|
|
459
|
+
if (hJob != IntPtr.Zero) CloseHandle(hJob);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
"@
|
|
464
|
+
|
|
465
|
+
Add-Type -TypeDefinition $source -Language CSharp
|
|
466
|
+
|
|
467
|
+
# $sizeUnit is always 'g'/'G'/'m'/'M'/'%' by this point - the validation above
|
|
468
|
+
# either matched an m/M/g/G suffix or resolved a bare integer to '%'.
|
|
469
|
+
try {
|
|
470
|
+
switch ($sizeUnit) {
|
|
471
|
+
{ $_ -in 'g', 'G' } { $memoryLimitBytes = [uint64]($sizeNum * 1GB) }
|
|
472
|
+
{ $_ -in 'm', 'M' } { $memoryLimitBytes = [uint64]($sizeNum * 1MB) }
|
|
473
|
+
'%' {
|
|
474
|
+
$totalPhysBytes = [CapmLauncher]::GetTotalPhysicalMemoryBytes()
|
|
475
|
+
$memoryLimitBytes = [uint64]([double]$totalPhysBytes * ($sizeNum / 100.0))
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} catch {
|
|
479
|
+
# An absurdly large <size> can overflow the double->uint64 cast above
|
|
480
|
+
# (e.g. a huge decimal string) - report it as a usage error, not a crash.
|
|
481
|
+
Write-Error "capm: <size> is out of range. $usage"
|
|
482
|
+
exit 1
|
|
483
|
+
}
|
|
484
|
+
if ($memoryLimitBytes -eq 0) {
|
|
485
|
+
Write-Error "capm: <size> is too small - rounds to 0 bytes. $usage"
|
|
486
|
+
exit 1
|
|
487
|
+
}
|
|
488
|
+
# SIZE_T/UIntPtr is process-width: 32-bit PowerShell can't address a limit
|
|
489
|
+
# above 4GiB regardless of the machine's actual RAM or architecture. Computed
|
|
490
|
+
# from [UIntPtr]::Size rather than [UIntPtr]::MaxValue - the latter doesn't
|
|
491
|
+
# exist on .NET Framework (Windows PowerShell 5.1) and silently reads as $null
|
|
492
|
+
# there, which would make this check compare against 0 and always trip.
|
|
493
|
+
$sizeTMaxBytes = if ([UIntPtr]::Size -eq 4) { [uint64][uint32]::MaxValue } else { [uint64]::MaxValue }
|
|
494
|
+
if ($memoryLimitBytes -gt $sizeTMaxBytes) {
|
|
495
|
+
Write-Error ("capm: <size> ($memoryLimitBytes bytes) exceeds the addressable limit " +
|
|
496
|
+
"for this PowerShell process ($sizeTMaxBytes bytes, $([UIntPtr]::Size * 8)-bit) - " +
|
|
497
|
+
"use 64-bit PowerShell for larger caps, or lower <size>.")
|
|
498
|
+
exit 1
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
exit ([CapmLauncher]::Run($memoryLimitBytes, [string[]]$Command, $commandLine))
|
|
503
|
+
} catch {
|
|
504
|
+
Write-Error $_.Exception.InnerException.Message
|
|
505
|
+
exit 1
|
|
506
|
+
}
|
package/bin/capn
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")/capn.ps1" 2>/dev/null) || script="$(dirname "$0")/capn.ps1"
|
|
11
|
+
exec powershell -NoProfile -ExecutionPolicy Bypass -File "$script" "$@"
|
package/bin/capn.bat
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
:: SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
:: win-nice: managed-file
|
|
4
|
+
:: A literal "%" in any argument gets corrupted here - see capc.bat for why (a
|
|
5
|
+
:: cmd.exe batch-parameter quirk, not fixable from inside a .bat). Every other
|
|
6
|
+
:: cmd.exe metacharacter (&|<>^) survives this hop untouched. Invoking "capn"
|
|
7
|
+
:: bare from an actual PowerShell session skips this file (capn.ps1 preferred).
|
|
8
|
+
"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "%~dp0capn.ps1" %*
|