MainViewModel.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.Collections.ObjectModel;
  8  using System.Diagnostics;
  9  using System.Linq;
 10  using System.Threading;
 11  using System.Threading.Tasks;
 12  
 13  using CommunityToolkit.Mvvm.ComponentModel;
 14  using CommunityToolkit.Mvvm.Input;
 15  using ManagedCommon;
 16  using PowerToys.FileLocksmithLib.Interop;
 17  
 18  namespace PowerToys.FileLocksmithUI.ViewModels
 19  {
 20  #pragma warning disable CA1708 // Identifiers should differ by more than case
 21      public partial class MainViewModel : ObservableObject, IDisposable
 22  #pragma warning restore CA1708 // Identifiers should differ by more than case
 23      {
 24          public IAsyncRelayCommand LoadProcessesCommand { get; }
 25  
 26          private bool _isLoading;
 27          private bool _isElevated;
 28          private string[] paths;
 29          private bool _disposed;
 30          private CancellationTokenSource _cancelProcessWatching;
 31  
 32          public ObservableCollection<ProcessResult> Processes { get; } = new();
 33  
 34          public bool IsLoading
 35          {
 36              get
 37              {
 38                  return _isLoading;
 39              }
 40  
 41              set
 42              {
 43                  _isLoading = value;
 44                  OnPropertyChanged(nameof(IsLoading));
 45              }
 46          }
 47  
 48          public bool IsElevated
 49          {
 50              get
 51              {
 52                  return _isElevated;
 53              }
 54  
 55              set
 56              {
 57                  _isElevated = value;
 58                  OnPropertyChanged(nameof(IsElevated));
 59              }
 60          }
 61  
 62          public string[] Paths
 63          {
 64              get => paths;
 65              set
 66              {
 67                  paths = value;
 68                  OnPropertyChanged(nameof(Paths));
 69              }
 70          }
 71  
 72          public string PathsToString
 73          {
 74              get
 75              {
 76                  return string.Join("\n", paths);
 77              }
 78          }
 79  
 80          public MainViewModel()
 81          {
 82              paths = NativeMethods.ReadPathsFromFile();
 83              Logger.LogInfo($"Starting FileLocksmith with {paths.Length} files selected.");
 84              LoadProcessesCommand = new AsyncRelayCommand(LoadProcessesAsync);
 85          }
 86  
 87          private async Task LoadProcessesAsync()
 88          {
 89              IsLoading = true;
 90              Processes.Clear();
 91  
 92              if (_cancelProcessWatching is not null)
 93              {
 94                  _cancelProcessWatching.Cancel();
 95              }
 96  
 97              _cancelProcessWatching = new CancellationTokenSource();
 98  
 99              var processes_found = await FindProcesses(paths);
100              if (processes_found is not null)
101              {
102                  foreach (ProcessResult p in processes_found)
103                  {
104                      Processes.Add(p);
105                      WatchProcess(p, _cancelProcessWatching.Token);
106                  }
107              }
108  
109              IsLoading = false;
110          }
111  
112          private async Task<List<ProcessResult>> FindProcesses(string[] paths)
113          {
114              var results = new List<ProcessResult>();
115              await Task.Run(() =>
116              {
117                  results = NativeMethods.FindProcessesRecursive(paths)?.ToList();
118              });
119              return results;
120          }
121  
122          private async void WatchProcess(ProcessResult process, CancellationToken token)
123          {
124              try
125              {
126                  Process handle = Process.GetProcessById((int)process.pid);
127                  try
128                  {
129                      await handle.WaitForExitAsync(token);
130                  }
131                  catch (TaskCanceledException)
132                  {
133                      // Nothing to do, normal operation
134                  }
135  
136                  if (handle.HasExited)
137                  {
138                      Processes.Remove(process);
139                  }
140              }
141              catch (Exception ex)
142              {
143                  Logger.LogError($"Couldn't add a waiter to wait for a process to exit. PID = {process.pid} and Name = {process.name}.", ex);
144                  Processes.Remove(process); // If we couldn't get a handle to the process or it has exited in the meanwhile, don't show it.
145              }
146          }
147  
148          [RelayCommand]
149          public void EndTask(ProcessResult selectedProcess)
150          {
151              try
152              {
153                  Process handle = Process.GetProcessById((int)selectedProcess.pid);
154                  try
155                  {
156                      handle.Kill();
157                  }
158                  catch (Exception ex)
159                  {
160                      Logger.LogError($"Couldn't kill process {selectedProcess.name} with PID {selectedProcess.pid}.", ex);
161                  }
162              }
163              catch (Exception ex)
164              {
165                  Logger.LogError($"Couldn't get a handle to kill process {selectedProcess.name} with PID {selectedProcess.pid}. Likely has been killed already.", ex);
166                  Processes.Remove(selectedProcess); // If we couldn't get a handle to the process, remove it from the list, since it's likely been killed already.
167              }
168          }
169  
170          [RelayCommand]
171          public void RestartElevated()
172          {
173              if (NativeMethods.StartAsElevated(paths))
174              {
175                  // TODO gentler exit
176                  Environment.Exit(0);
177              }
178              else
179              {
180                  // TODO report error?
181                  Logger.LogError($"Couldn't restart as elevated.");
182              }
183          }
184  
185          public void Dispose()
186          {
187              Dispose(disposing: true);
188              GC.SuppressFinalize(this);
189          }
190  
191          protected virtual void Dispose(bool disposing)
192          {
193              if (!_disposed)
194              {
195                  if (disposing)
196                  {
197                      _disposed = true;
198                  }
199              }
200          }
201      }
202  }