-
Notifications
You must be signed in to change notification settings - Fork 1
/
PathUtils.cs
186 lines (161 loc) · 6.92 KB
/
PathUtils.cs
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
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Microsoft.Win32;
namespace Flow.Launcher.Plugin.Add2Path;
public class NotInPathException : Exception
{
public NotInPathException(string message)
: base(message)
{
}
}
public class AlreadyInPathException : Exception
{
public AlreadyInPathException(string message)
: base(message)
{
}
}
internal static class PathUtils // Renamed from Path to PathUtils, to avoid naming conflict with System.IO.Path
{
private static string _ExecuteCommand(string command, bool requestadmin = false)
{
/*
* Oh boy, this took me ages. Let me explain...
* So the issue I have is that, when requestadmin is true, I need to run as admin. But I also need to capture output.
* Apparently C# doesn't want you to do that and makes it the 9th level of Hell (damn, why can't this shit just be easy like in python),
* so when I google my problem the first result is an obscure StackOverflow post: https://stackoverflow.com/questions/15746716/run-new-process-as-admin-and-read-standard-output
* That post discusses the crazy lengths you have to go to in order for this to work. I decided to just go with the OP's hacky solution of redirecting to a temp file.
* Apparently the issue stems from funky issues with C# use of Windows functions to run commands:
* RedirectStandardOutput (for capturing output) and UseShellExecute (for running as admin) are mutually exclusive by design.
* Here's a post that makes the problem clear: https://www.codeproject.com/Questions/772905/run-process-as-admin-and-redirect-the-process-outp
*/
/*
// Debug
Process process2 = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c echo {command} & pause & {command} & pause",
RedirectStandardOutput = false,
UseShellExecute = true,
CreateNoWindow = false,
}
};
process2.Start();
process2.WaitForExit();
*/
string tempfile = Path.GetTempFileName();
Process process = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c {command} > \"{tempfile}\"",
RedirectStandardOutput = false,
UseShellExecute = true,
CreateNoWindow = true,
}
};
if (requestadmin)
{
process.StartInfo.Verb = "runas";
}
process.Start();
process.WaitForExit();
string output = File.ReadAllText(tempfile).TrimEnd();
File.Delete(tempfile);
return output;
}
public static string _EncodeParameterArgument(string original)
{
// From https://stackoverflow.com/questions/5510343/escape-command-line-arguments-in-c-sharp
if (string.IsNullOrEmpty(original))
return original;
string value = Regex.Replace(original, @"(\\*)" + "\"", @"$1\$0");
value = Regex.Replace(value, @"^(.*\s.*?)(\\*)$", "\"$1$2$2\"");
return value;
}
private static string _GetRegistryKey(bool system = false)
{
return system ? """HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment""" : """HKEY_CURRENT_USER\Environment""";
}
public static string GetFullString(bool system = false)
{
string registrykey = _GetRegistryKey(system);
try
{
return _ExecuteCommand($"for /f \"tokens=2*\" %a in ('REG QUERY \"{registrykey}\" /v \"Path\" ^| findstr \"Path\"') do @echo %b", system);
}
catch (System.ComponentModel.Win32Exception)
{
throw new Exception($"Failed to set path - admin access was denied, or a different Windows error occurred");
}
}
public static List<string> Get(bool system = false)
{
string pathstring = PathUtils.GetFullString(system);
// Split by semicolon, but only for items not in quotes (because semicolon can appear literally in a folder path)
Regex splitregex = new Regex(";(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))"); // Regex from https://stackoverflow.com/a/48275050/22081657
List<string> folderPaths = splitregex.Split(pathstring).ToList();
// Trim whitespace and quotes from all paths
folderPaths = (from folderPath in folderPaths select folderPath.Trim(new char[3] { '"', ' ', '\t' })).ToList();
// Remove all items that are empty or whitespace - path will get corrupted and not work, otherwise (Don't know if this program could possibly cause this corruption, but just in case)
folderPaths.RemoveAll(String.IsNullOrWhiteSpace);
return folderPaths;
}
public static void SetFullString(string value, bool system = false)
{
string registrykey = _GetRegistryKey(system);
string resulttext;
try
{
resulttext = _ExecuteCommand($"reg add \"{registrykey}\" /v Path /d {_EncodeParameterArgument(value)} /f", system);
} catch (System.ComponentModel.Win32Exception)
{
throw new Exception($"Failed to set path - admin access was denied, or a different Windows error occurred");
}
if (!resulttext.Contains("The operation completed successfully."))
{
throw new Exception($"Failed to set path - got output: {resulttext}");
}
}
public static void Set(List<string> folderPaths, bool system = false)
{
// Wrap folder paths in quotes
folderPaths = (from folderPath in folderPaths select $"\"{folderPath}\"").ToList();
PathUtils.SetFullString(String.Join(";", folderPaths), system);
}
public static bool Contains(string folderPath, bool system = false)
{
List<string> folderPaths = PathUtils.Get(system);
return folderPaths.Contains(folderPath);
}
public static void Add(string folderPath, bool system = false)
{
if (PathUtils.Contains(folderPath, system))
{
throw new AlreadyInPathException($"{folderPath} is already in PATH");
}
List<string> folderPaths = PathUtils.Get(system);
folderPaths.Add(folderPath);
PathUtils.Set(folderPaths, system);
}
public static void Remove(string folderPath, bool system = false)
{
if (!PathUtils.Contains(folderPath, system))
{
throw new NotInPathException($"{folderPath} is not in PATH");
}
List<string> folderPaths = PathUtils.Get(system);
folderPaths.RemoveAll(existing_folder_path => existing_folder_path == folderPath);
PathUtils.Set(folderPaths, system);
}
}