/ src / settings-ui / Settings.UI / ViewModels / CmdNotFoundViewModel.cs
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  }