1// Copyright 2019 The Flutter Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:io'; 6 7import 'package:meta/meta.dart'; 8import 'package:process/process.dart'; 9 10@immutable 11class RunningProcessInfo { 12 const RunningProcessInfo(this.pid, this.creationDate, this.commandLine) 13 : assert(pid != null), 14 assert(commandLine != null); 15 16 final String commandLine; 17 final int pid; 18 final DateTime creationDate; 19 20 @override 21 bool operator ==(Object other) { 22 return other is RunningProcessInfo && 23 other.pid == pid && 24 other.commandLine == commandLine && 25 other.creationDate == creationDate; 26 } 27 28 @override 29 int get hashCode { 30 // TODO(dnfield): Replace this when Object.hashValues lands. 31 int hash = 17; 32 if (pid != null) { 33 hash = hash * 23 + pid.hashCode; 34 } 35 if (commandLine != null) { 36 hash = hash * 23 + commandLine.hashCode; 37 } 38 if (creationDate != null) { 39 hash = hash * 23 + creationDate.hashCode; 40 } 41 return hash; 42 } 43 44 @override 45 String toString() { 46 return 'RunningProcesses{pid: $pid, commandLine: $commandLine, creationDate: $creationDate}'; 47 } 48} 49 50Future<bool> killProcess(int pid, {ProcessManager processManager}) async { 51 assert(pid != null, 'Must specify a pid to kill'); 52 processManager ??= const LocalProcessManager(); 53 ProcessResult result; 54 if (Platform.isWindows) { 55 result = await processManager.run(<String>[ 56 'taskkill.exe', 57 '/pid', 58 pid.toString(), 59 '/f', 60 ]); 61 } else { 62 result = await processManager.run(<String>[ 63 'kill', 64 '-9', 65 pid.toString(), 66 ]); 67 } 68 return result.exitCode == 0; 69} 70 71Stream<RunningProcessInfo> getRunningProcesses({ 72 String processName, 73 ProcessManager processManager, 74}) { 75 processManager ??= const LocalProcessManager(); 76 if (Platform.isWindows) { 77 return windowsRunningProcesses(processName); 78 } 79 return posixRunningProcesses(processName, processManager); 80} 81 82@visibleForTesting 83Stream<RunningProcessInfo> windowsRunningProcesses(String processName) async* { 84 // PowerShell script to get the command line arguments and create time of 85 // a process. 86 // See: https://docs.microsoft.com/en-us/windows/desktop/cimwin32prov/win32-process 87 final String script = processName != null 88 ? '"Get-CimInstance Win32_Process -Filter \\\"name=\'$processName\'\\\" | Select-Object ProcessId,CreationDate,CommandLine | Format-Table -AutoSize | Out-String -Width 4096"' 89 : '"Get-CimInstance Win32_Process | Select-Object ProcessId,CreationDate,CommandLine | Format-Table -AutoSize | Out-String -Width 4096"'; 90 // Unfortunately, there doesn't seem to be a good way to get ProcessManager to 91 // run this. May be a bug in Dart. 92 // TODO(dnfield): fix this when https://github.com/dart-lang/sdk/issues/36175 is resolved. 93 final ProcessResult result = await Process.run( 94 'powershell -command $script', 95 <String>[], 96 ); 97 if (result.exitCode != 0) { 98 print('Could not list processes!'); 99 print(result.stderr); 100 print(result.stdout); 101 return; 102 } 103 for (RunningProcessInfo info in processPowershellOutput(result.stdout)) { 104 yield info; 105 } 106} 107 108/// Parses the output of the PowerShell script from [windowsRunningProcesses]. 109/// 110/// E.g.: 111/// ProcessId CreationDate CommandLine 112/// --------- ------------ ----------- 113/// 2904 3/11/2019 11:01:54 AM "C:\Program Files\Android\Android Studio\jre\bin\java.exe" -Xmx1536M -Dfile.encoding=windows-1252 -Duser.country=US -Duser.language=en -Duser.variant -cp C:\Users\win1\.gradle\wrapper\dists\gradle-4.10.2-all\9fahxiiecdb76a5g3aw9oi8rv\gradle-4.10.2\lib\gradle-launcher-4.10.2.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 4.10.2 114@visibleForTesting 115Iterable<RunningProcessInfo> processPowershellOutput(String output) sync* { 116 if (output == null) { 117 return; 118 } 119 120 const int processIdHeaderSize = 'ProcessId'.length; 121 const int creationDateHeaderStart = processIdHeaderSize + 1; 122 int creationDateHeaderEnd; 123 int commandLineHeaderStart; 124 bool inTableBody = false; 125 for (String line in output.split('\n')) { 126 if (line.startsWith('ProcessId')) { 127 commandLineHeaderStart = line.indexOf('CommandLine'); 128 creationDateHeaderEnd = commandLineHeaderStart - 1; 129 } 130 if (line.startsWith('--------- ------------')) { 131 inTableBody = true; 132 continue; 133 } 134 if (!inTableBody || line.isEmpty) { 135 continue; 136 } 137 if (line.length < commandLineHeaderStart) { 138 continue; 139 } 140 141 // 3/11/2019 11:01:54 AM 142 // 12/11/2019 11:01:54 AM 143 String rawTime = line.substring( 144 creationDateHeaderStart, 145 creationDateHeaderEnd, 146 ).trim(); 147 148 if (rawTime[1] == '/') { 149 rawTime = '0$rawTime'; 150 } 151 if (rawTime[4] == '/') { 152 rawTime = rawTime.substring(0, 3) + '0' + rawTime.substring(3); 153 } 154 final String year = rawTime.substring(6, 10); 155 final String month = rawTime.substring(3, 5); 156 final String day = rawTime.substring(0, 2); 157 String time = rawTime.substring(11, 19); 158 if (time[7] == ' ') { 159 time = '0$time'.trim(); 160 } 161 if (rawTime.endsWith('PM')) { 162 final int hours = int.parse(time.substring(0, 2)); 163 time = '${hours + 12}${time.substring(2)}'; 164 } 165 166 final int pid = int.parse(line.substring(0, processIdHeaderSize).trim()); 167 final DateTime creationDate = DateTime.parse('$year-$month-${day}T$time'); 168 final String commandLine = line.substring(commandLineHeaderStart).trim(); 169 yield RunningProcessInfo(pid, creationDate, commandLine); 170 } 171} 172 173@visibleForTesting 174Stream<RunningProcessInfo> posixRunningProcesses( 175 String processName, 176 ProcessManager processManager, 177) async* { 178 // Cirrus is missing this in Linux for some reason. 179 if (!processManager.canRun('ps')) { 180 print('Cannot list processes on this system: `ps` not available.'); 181 return; 182 } 183 final ProcessResult result = await processManager.run(<String>[ 184 'ps', 185 '-eo', 186 'lstart,pid,command', 187 ]); 188 if (result.exitCode != 0) { 189 print('Could not list processes!'); 190 print(result.stderr); 191 print(result.stdout); 192 return; 193 } 194 for (RunningProcessInfo info in processPsOutput(result.stdout, processName)) { 195 yield info; 196 } 197} 198 199/// Parses the output of the command in [posixRunningProcesses]. 200/// 201/// E.g.: 202/// 203/// STARTED PID COMMAND 204/// Sat Mar 9 20:12:47 2019 1 /sbin/launchd 205/// Sat Mar 9 20:13:00 2019 49 /usr/sbin/syslogd 206@visibleForTesting 207Iterable<RunningProcessInfo> processPsOutput( 208 String output, 209 String processName, 210) sync* { 211 if (output == null) { 212 return; 213 } 214 bool inTableBody = false; 215 for (String line in output.split('\n')) { 216 if (line.trim().startsWith('STARTED')) { 217 inTableBody = true; 218 continue; 219 } 220 if (!inTableBody || line.isEmpty) { 221 continue; 222 } 223 224 if (processName != null && !line.contains(processName)) { 225 continue; 226 } 227 if (line.length < 25) { 228 continue; 229 } 230 231 // 'Sat Feb 16 02:29:55 2019' 232 // 'Sat Mar 9 20:12:47 2019' 233 const Map<String, String> months = <String, String>{ 234 'Jan': '01', 235 'Feb': '02', 236 'Mar': '03', 237 'Apr': '04', 238 'May': '05', 239 'Jun': '06', 240 'Jul': '07', 241 'Aug': '08', 242 'Sep': '09', 243 'Oct': '10', 244 'Nov': '11', 245 'Dec': '12', 246 }; 247 final String rawTime = line.substring(0, 24); 248 249 final String year = rawTime.substring(20, 24); 250 final String month = months[rawTime.substring(4, 7)]; 251 final String day = rawTime.substring(8, 10).replaceFirst(' ', '0'); 252 final String time = rawTime.substring(11, 19); 253 254 final DateTime creationDate = DateTime.parse('$year-$month-${day}T$time'); 255 line = line.substring(24).trim(); 256 final int nextSpace = line.indexOf(' '); 257 final int pid = int.parse(line.substring(0, nextSpace)); 258 final String commandLine = line.substring(nextSpace + 1); 259 yield RunningProcessInfo(pid, creationDate, commandLine); 260 } 261} 262