CmdNotFoundViewModel.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Collections.Generic; 7 using System.Diagnostics; 8 using System.IO; 9 using System.Linq; 10 using System.Reflection; 11 using System.Runtime.InteropServices; 12 13 using global::PowerToys.GPOWrapper; 14 using ManagedCommon; 15 using Microsoft.PowerToys.Settings.UI.Library.Helpers; 16 using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events; 17 using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; 18 using Microsoft.PowerToys.Telemetry; 19 20 namespace Microsoft.PowerToys.Settings.UI.ViewModels 21 { 22 public partial class CmdNotFoundViewModel : Observable 23 { 24 public ButtonClickCommand CheckRequirementsEventHandler => new ButtonClickCommand(CheckCommandNotFoundRequirements); 25 26 public ButtonClickCommand InstallPowerShell7EventHandler => new ButtonClickCommand(InstallPowerShell7); 27 28 public ButtonClickCommand InstallWinGetClientModuleEventHandler => new ButtonClickCommand(InstallWinGetClientModule); 29 30 public ButtonClickCommand InstallModuleEventHandler => new ButtonClickCommand(InstallModule); 31 32 public ButtonClickCommand UninstallModuleEventHandler => new ButtonClickCommand(UninstallModule); 33 34 private GpoRuleConfigured _enabledGpoRuleConfiguration; 35 private bool _moduleIsGpoEnabled; 36 private bool _moduleIsGpoDisabled; 37 38 public static string AssemblyDirectory 39 { 40 get 41 { 42 return Path.TrimEndingDirectorySeparator(AppContext.BaseDirectory); 43 } 44 } 45 46 public CmdNotFoundViewModel() 47 { 48 InitializeEnabledValue(); 49 } 50 51 private void InitializeEnabledValue() 52 { 53 _enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredCmdNotFoundEnabledValue(); 54 _moduleIsGpoEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; 55 _moduleIsGpoDisabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled; 56 57 // Update PATH environment variable to get pwsh.exe on further calls. 58 Environment.SetEnvironmentVariable("PATH", (Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? string.Empty) + ";" + (Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty), EnvironmentVariableTarget.Process); 59 60 CheckCommandNotFoundRequirements(); 61 } 62 63 private string _commandOutputLog; 64 65 public string CommandOutputLog 66 { 67 get => _commandOutputLog; 68 set 69 { 70 if (_commandOutputLog != value) 71 { 72 _commandOutputLog = value; 73 OnPropertyChanged(nameof(CommandOutputLog)); 74 } 75 } 76 } 77 78 private bool _isPowerShell7Detected; 79 80 private bool isPowerShellPreviewDetected; 81 private string powerShellPreviewPath; 82 83 public bool IsPowerShell7Detected 84 { 85 get => _isPowerShell7Detected; 86 set 87 { 88 if (_isPowerShell7Detected != value) 89 { 90 _isPowerShell7Detected = value; 91 OnPropertyChanged(nameof(IsPowerShell7Detected)); 92 } 93 } 94 } 95 96 private bool _isWinGetClientModuleDetected; 97 98 public bool IsWinGetClientModuleDetected 99 { 100 get => _isWinGetClientModuleDetected; 101 set 102 { 103 if (_isWinGetClientModuleDetected != value) 104 { 105 _isWinGetClientModuleDetected = value; 106 OnPropertyChanged(nameof(IsWinGetClientModuleDetected)); 107 } 108 } 109 } 110 111 private bool _isCommandNotFoundModuleInstalled; 112 113 public bool IsCommandNotFoundModuleInstalled 114 { 115 get => _isCommandNotFoundModuleInstalled; 116 set 117 { 118 if (_isCommandNotFoundModuleInstalled != value) 119 { 120 _isCommandNotFoundModuleInstalled = value; 121 OnPropertyChanged(nameof(IsCommandNotFoundModuleInstalled)); 122 } 123 } 124 } 125 126 public bool IsModuleGpoEnabled 127 { 128 get => _moduleIsGpoEnabled; 129 } 130 131 public bool IsModuleGpoDisabled 132 { 133 get => _moduleIsGpoDisabled; 134 } 135 136 public string RunPowerShellOrPreviewScript(string powershellExecutable, string powershellArguments, bool hidePowerShellWindow = false) 137 { 138 if (isPowerShellPreviewDetected) 139 { 140 return RunPowerShellScript(Path.Combine(powerShellPreviewPath, "pwsh-preview.cmd"), powershellArguments, hidePowerShellWindow); 141 } 142 else 143 { 144 return RunPowerShellScript(powershellExecutable, powershellArguments, hidePowerShellWindow); 145 } 146 } 147 148 public string RunPowerShellScript(string powershellExecutable, string powershellArguments, bool hidePowerShellWindow = false) 149 { 150 string outputLog = string.Empty; 151 try 152 { 153 var startInfo = new ProcessStartInfo() 154 { 155 FileName = powershellExecutable, 156 Arguments = powershellArguments, 157 CreateNoWindow = hidePowerShellWindow, 158 UseShellExecute = false, 159 RedirectStandardOutput = true, 160 }; 161 startInfo.EnvironmentVariables["NO_COLOR"] = "1"; 162 var process = Process.Start(startInfo); 163 while (!process.StandardOutput.EndOfStream) 164 { 165 outputLog += process.StandardOutput.ReadLine() + "\r\n"; // Weirdly, PowerShell 7 won't give us new lines. 166 } 167 } 168 catch (Exception ex) 169 { 170 outputLog = ex.ToString(); 171 } 172 173 CommandOutputLog = outputLog; 174 return outputLog; 175 } 176 177 public void CheckCommandNotFoundRequirements() 178 { 179 isPowerShellPreviewDetected = false; 180 var ps1File = AssemblyDirectory + "\\Assets\\Settings\\Scripts\\CheckCmdNotFoundRequirements.ps1"; 181 var arguments = $"-NoProfile -NonInteractive -ExecutionPolicy Unrestricted -File \"{ps1File}\""; 182 var result = RunPowerShellScript("pwsh.exe", arguments, true); 183 184 if (result.Contains("PowerShell 7.4 or greater detected.")) 185 { 186 IsPowerShell7Detected = true; 187 } 188 else if (result.Contains("PowerShell 7.4 or greater not detected.")) 189 { 190 IsPowerShell7Detected = false; 191 } 192 else if (result.Contains("pwsh.exe")) 193 { 194 // Likely an error saying there was an error starting pwsh.exe, so we can assume Powershell 7 was not detected. 195 CommandOutputLog += "PowerShell 7.4 or greater not detected. Installation instructions can be found on https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-windows \r\n"; 196 IsPowerShell7Detected = false; 197 } 198 199 if (!IsPowerShell7Detected) 200 { 201 // powerShell Preview might be installed, check it. 202 try 203 { 204 // we have to search for the directory where the PowerShell preview command is located. It is added to the PATH environment variable, so we have to search for it there 205 foreach (string pathCandidate in Environment.GetEnvironmentVariable("PATH").Split(';')) 206 { 207 if (File.Exists(Path.Combine(pathCandidate, "pwsh-preview.cmd"))) 208 { 209 result = RunPowerShellScript(Path.Combine(pathCandidate, "pwsh-preview.cmd"), arguments, true); 210 if (result.Contains("PowerShell 7.4 or greater detected.")) 211 { 212 isPowerShellPreviewDetected = true; 213 IsPowerShell7Detected = true; 214 powerShellPreviewPath = pathCandidate; 215 break; 216 } 217 } 218 } 219 } 220 catch (Exception) 221 { 222 // nothing to do. No additional PowerShell installation found 223 } 224 } 225 226 if (result.Contains("WinGet Client module detected.")) 227 { 228 IsWinGetClientModuleDetected = true; 229 } 230 else if (result.Contains("WinGet Client module not detected.") || result.Contains("WinGet Client module needs to be updated.")) 231 { 232 IsWinGetClientModuleDetected = false; 233 } 234 235 if (result.Contains("Command Not Found module is registered in the profile file.")) 236 { 237 IsCommandNotFoundModuleInstalled = true; 238 } 239 else if (result.Contains("Command Not Found module is not registered in the profile file.") || result.Contains("Outdated version of Command Not Found module found in the profile file.")) 240 { 241 IsCommandNotFoundModuleInstalled = false; 242 } 243 244 Logger.LogInfo(result); 245 } 246 247 public void InstallPowerShell7() 248 { 249 var ps1File = AssemblyDirectory + "\\Assets\\Settings\\Scripts\\InstallPowerShell7.ps1"; 250 var arguments = $"-NoProfile -ExecutionPolicy Unrestricted -File \"{ps1File}\""; 251 var result = RunPowerShellOrPreviewScript("powershell.exe", arguments); 252 if (result.Contains("Powershell 7 successfully installed.")) 253 { 254 IsPowerShell7Detected = true; 255 } 256 257 Logger.LogInfo(result); 258 259 // Update PATH environment variable to get pwsh.exe on further calls. 260 Environment.SetEnvironmentVariable("PATH", (Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? string.Empty) + ";" + (Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty), EnvironmentVariableTarget.Process); 261 } 262 263 public void InstallWinGetClientModule() 264 { 265 var ps1File = AssemblyDirectory + "\\Assets\\Settings\\Scripts\\InstallWinGetClientModule.ps1"; 266 var arguments = $"-NoProfile -ExecutionPolicy Unrestricted -File \"{ps1File}\""; 267 var result = RunPowerShellOrPreviewScript("pwsh.exe", arguments); 268 if (result.Contains("WinGet Client module detected.") || result.Contains("WinGet Client module updated.")) 269 { 270 IsWinGetClientModuleDetected = true; 271 } 272 else if (result.Contains("WinGet Client module not detected.")) 273 { 274 IsWinGetClientModuleDetected = false; 275 } 276 277 Logger.LogInfo(result); 278 } 279 280 public void InstallModule() 281 { 282 var ps1File = AssemblyDirectory + "\\Assets\\Settings\\Scripts\\EnableModule.ps1"; 283 var arguments = $"-NoProfile -ExecutionPolicy Unrestricted -File \"{ps1File}\" -scriptPath \"{AssemblyDirectory}\\..\""; 284 var result = RunPowerShellOrPreviewScript("pwsh.exe", arguments); 285 286 if (result.Contains("Module is already registered in the profile file.") 287 || result.Contains("Module was successfully registered in the profile file.") 288 || result.Contains("Module was successfully upgraded in the profile file.")) 289 { 290 IsCommandNotFoundModuleInstalled = true; 291 PowerToysTelemetry.Log.WriteEvent(new CmdNotFoundInstallEvent()); 292 } 293 294 Logger.LogInfo(result); 295 } 296 297 public void UninstallModule() 298 { 299 var ps1File = AssemblyDirectory + "\\Assets\\Settings\\Scripts\\DisableModule.ps1"; 300 var arguments = $"-NoProfile -ExecutionPolicy Unrestricted -File \"{ps1File}\""; 301 var result = RunPowerShellOrPreviewScript("pwsh.exe", arguments); 302 303 if (result.Contains("Removed the Command Not Found reference from the profile file.") || result.Contains("No instance of Command Not Found was found in the profile file.")) 304 { 305 IsCommandNotFoundModuleInstalled = false; 306 PowerToysTelemetry.Log.WriteEvent(new CmdNotFoundUninstallEvent()); 307 } 308 309 Logger.LogInfo(result); 310 } 311 } 312 }