-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathElevation.cs
More file actions
193 lines (173 loc) · 9 KB
/
Copy pathElevation.cs
File metadata and controls
193 lines (173 loc) · 9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Security.Principal;
using System.Windows;
using System.Windows.Controls;
using KillerShell.Terminal;
// Running as admin: getting there, and making it obvious. Partial of MainWindow.
//
// An elevated process cannot attach to an unelevated pseudoconsole. That is a UAC integrity
// boundary rather than a gap in the API: CreateProcess can pass an attribute list but cannot
// request elevation, and ShellExecuteEx can request elevation but cannot pass an attribute
// list. There is no combination that gets both, which is why Windows Terminal opens a separate
// elevated window for this too.
//
// So an admin shell relaunches KILLERSHELL elevated and lets that instance host it. Everything
// the elevated instance spawns is elevated for free, because the whole process is.
namespace KillerShell
{
public partial class MainWindow
{
/// <summary>True when this whole process is running elevated.</summary>
internal static bool IsElevated { get; } = CheckElevated();
private static bool CheckElevated()
{
try
{
using var id = WindowsIdentity.GetCurrent();
return new WindowsPrincipal(id).IsInRole(WindowsBuiltInRole.Administrator);
}
catch { return false; }
}
// ═══════════════════════════════════════════════════════════
// RELAUNCH
// ═══════════════════════════════════════════════════════════
/// <summary>
/// Ask for elevation and hand the shell to the new instance. Nothing happens in THIS
/// window: the request either becomes a second, elevated window or is declined.
/// </summary>
internal void RelaunchElevated(TerminalProfile profile, string folder)
{
string exe = Process.GetCurrentProcess().MainModule?.FileName ?? string.Empty;
if (string.IsNullOrEmpty(exe)) return;
var psi = new ProcessStartInfo(exe)
{
UseShellExecute = true, // required for the runas verb
Verb = "runas",
// The trailing slash is stripped so the quote that follows is not read as an
// escape - but NOT off a drive root, because "C:" is drive-RELATIVE and would
// resolve against the new process's current directory rather than naming the
// root. Same trap that sent Up-from-C: back to the home folder.
Arguments = "--shell " + (profile.Skin == TerminalSkin.Lcd ? "cmd" : "pwsh")
+ " --cwd \"" + TrimForArg(folder) + "\"",
WorkingDirectory = folder,
};
try
{
Process.Start(psi);
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
// ERROR_CANCELLED: the user said no at the prompt. That is an answer, not a
// failure, so it passes silently - they know what they just clicked.
}
catch (Exception ex)
{
SetTabStatusKey(_active, "Str_Status_ElevateFailed", ex.Message);
}
}
/// <summary>
/// Retry a recycle the unelevated process was refused. Same shape as the shell relaunch
/// above: a second instance is started elevated, does the one job it was given and exits.
/// Nothing happens in THIS window - if the prompt is declined, nothing was deleted.
/// </summary>
internal void RecycleElevated(System.Collections.Generic.IReadOnlyList<string> paths)
{
string exe = Process.GetCurrentProcess().MainModule?.FileName ?? string.Empty;
if (string.IsNullOrEmpty(exe) || paths.Count == 0) return;
var args = new System.Text.StringBuilder("--recycle");
foreach (string p in paths) args.Append(" \"").Append(TrimForArg(p)).Append('"');
var psi = new ProcessStartInfo(exe)
{
UseShellExecute = true, // required for the runas verb
Verb = "runas",
Arguments = args.ToString(),
};
try
{
var proc = Process.Start(psi);
if (proc == null) return;
// The helper has no UI, so its exit code is the only thing it can tell us. Left
// unwatched, a delete that Controlled Folder Access refused looked exactly like
// one that worked: a UAC prompt, then nothing, and the file still there.
proc.EnableRaisingEvents = true;
proc.Exited += (_, _) =>
{
int code = proc.ExitCode;
proc.Dispose();
if (code == 0) return;
Dispatcher.BeginInvoke((Action)(() =>
SetTabStatusKey(_active, "Str_Status_ElevatedDeleteFailed")));
};
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
// ERROR_CANCELLED: declined at the prompt. Same as above, that is an answer.
}
catch (Exception ex)
{
SetTabStatusKey(_active, "Str_Status_ElevateFailed", ex.Message);
}
}
/// <summary>A path safe to quote on a command line: no trailing slash unless it is a root.</summary>
private static string TrimForArg(string folder)
{
if (folder.Length <= 3) return folder; // "C:\" and shorter stay whole
return folder.TrimEnd('\\');
}
/// <summary>
/// Act on --shell / --cwd from an elevated relaunch. Called once the window is up,
/// because opening a shell needs the panes to exist.
/// </summary>
internal void ApplyStartupShell()
{
var args = Environment.GetCommandLineArgs();
string? kind = null, cwd = null;
for (int i = 1; i < args.Length - 1; i++)
{
if (string.Equals(args[i], "--shell", StringComparison.OrdinalIgnoreCase)) kind = args[i + 1];
else if (string.Equals(args[i], "--cwd", StringComparison.OrdinalIgnoreCase)) cwd = args[i + 1];
}
// --cwd on its own is a plain new window (NewWindow.cs) asking to open where the
// window it came from was. No shell, no bare layout - just land in the folder.
if (kind == null)
{
if (!string.IsNullOrEmpty(cwd) && System.IO.Directory.Exists(cwd))
_ = NavigateTo(cwd!); // Browse.cs
return;
}
var profile = string.Equals(kind, "cmd", StringComparison.OrdinalIgnoreCase)
? TerminalProfile.Cmd()
: TerminalProfile.PowerShell();
OpenStartupShell(profile, cwd); // TerminalTabs.cs
}
// ═══════════════════════════════════════════════════════════
// HALO
// ═══════════════════════════════════════════════════════════
/// <summary>
/// Ring the whole window in the accent when it is elevated, so an admin window is
/// never mistaken for an ordinary one.
/// </summary>
/// <remarks>
/// Driven off the process's real token rather than off the command-line flag, so a
/// window you started as administrator yourself is marked too, however it was started.
/// </remarks>
internal void ApplyElevationHalo()
{
if (!IsElevated) return;
// SetResourceReference, not a brush snapshot: the accent is switchable at runtime
// and a snapshot would leave the halo on the old color after a theme change.
ElevationHalo.SetResourceReference(Border.BorderBrushProperty, "PrimaryBrush");
ElevationHaloInner.SetResourceReference(Border.BorderBrushProperty, "PrimaryBrush");
ElevationHalo.Visibility = Visibility.Visible;
// The ring reads as "something is different" from the corner of the eye; the caption
// is what says WHAT. Accent, same as the ring, so the two are obviously one signal.
ElevatedTag.SetResourceReference(TextBlock.ForegroundProperty, "PrimaryBrush");
ElevatedTag.Visibility = Visibility.Visible;
// The taskbar and Alt+Tab read this, so an admin window is identifiable even when
// it is not the one you are looking at.
Title = "KillerShell [Administrator]";
}
}
}