1 | // Copyright 2014 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 | |
5 | import 'dart:io'; |
6 | |
7 | import 'package:meta/meta.dart' ; |
8 | import 'package:process/process.dart' ; |
9 | |
10 | @immutable |
11 | class RunningProcessInfo { |
12 | const RunningProcessInfo(this.pid, this.commandLine, this.creationDate); |
13 | |
14 | final int pid; |
15 | final String commandLine; |
16 | final DateTime creationDate; |
17 | |
18 | @override |
19 | bool operator ==(Object other) { |
20 | return other is RunningProcessInfo |
21 | && other.pid == pid |
22 | && other.commandLine == commandLine |
23 | && other.creationDate == creationDate; |
24 | } |
25 | |
26 | Future<bool> terminate({required ProcessManager processManager}) async { |
27 | // This returns true when the signal is sent, not when the process goes away. |
28 | // See also https://github.com/dart-lang/sdk/issues/40759 (killPid should wait for process to be terminated). |
29 | if (Platform.isWindows) { |
30 | // TODO(ianh): Move Windows to killPid once we can. |
31 | // - killPid on Windows has not-useful return code: https://github.com/dart-lang/sdk/issues/47675 |
32 | final ProcessResult result = await processManager.run(<String>[ |
33 | 'taskkill.exe' , |
34 | '/pid' , |
35 | ' $pid' , |
36 | '/f' , |
37 | ]); |
38 | return result.exitCode == 0; |
39 | } |
40 | return processManager.killPid(pid, ProcessSignal.sigkill); |
41 | } |
42 | |
43 | @override |
44 | int get hashCode => Object.hash(pid, commandLine, creationDate); |
45 | |
46 | @override |
47 | String toString() { |
48 | return 'RunningProcesses(pid: $pid, commandLine: $commandLine, creationDate: $creationDate)' ; |
49 | } |
50 | } |
51 | |
52 | Future<Set<RunningProcessInfo>> getRunningProcesses({ |
53 | String? processName, |
54 | required ProcessManager processManager, |
55 | }) { |
56 | if (Platform.isWindows) { |
57 | return windowsRunningProcesses(processName, processManager); |
58 | } |
59 | return posixRunningProcesses(processName, processManager); |
60 | } |
61 | |
62 | @visibleForTesting |
63 | Future<Set<RunningProcessInfo>> windowsRunningProcesses( |
64 | String? processName, |
65 | ProcessManager processManager, |
66 | ) async { |
67 | // PowerShell script to get the command line arguments and create time of a process. |
68 | // See: https://docs.microsoft.com/en-us/windows/desktop/cimwin32prov/win32-process |
69 | final String script = processName != null |
70 | ? '"Get-CimInstance Win32_Process -Filter \\"name=\' $processName\'\\" | Select-Object ProcessId,CreationDate,CommandLine | Format-Table -AutoSize | Out-String -Width 4096"' |
71 | : '"Get-CimInstance Win32_Process | Select-Object ProcessId,CreationDate,CommandLine | Format-Table -AutoSize | Out-String -Width 4096"' ; |
72 | // TODO(ianh): Unfortunately, there doesn't seem to be a good way to get |
73 | // ProcessManager to run this. |
74 | final ProcessResult result = await Process.run( |
75 | 'powershell -command $script' , |
76 | <String>[], |
77 | ); |
78 | if (result.exitCode != 0) { |
79 | print('Could not list processes!' ); |
80 | print(result.stderr); |
81 | print(result.stdout); |
82 | return <RunningProcessInfo>{}; |
83 | } |
84 | return processPowershellOutput(result.stdout as String).toSet(); |
85 | } |
86 | |
87 | /// Parses the output of the PowerShell script from [windowsRunningProcesses]. |
88 | /// |
89 | /// E.g.: |
90 | /// ProcessId CreationDate CommandLine |
91 | /// --------- ------------ ----------- |
92 | /// 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 |
93 | @visibleForTesting |
94 | Iterable<RunningProcessInfo> processPowershellOutput(String output) sync* { |
95 | const int processIdHeaderSize = 'ProcessId' .length; |
96 | const int creationDateHeaderStart = processIdHeaderSize + 1; |
97 | late int creationDateHeaderEnd; |
98 | late int commandLineHeaderStart; |
99 | bool inTableBody = false; |
100 | for (final String line in output.split('\n' )) { |
101 | if (line.startsWith('ProcessId' )) { |
102 | commandLineHeaderStart = line.indexOf('CommandLine' ); |
103 | creationDateHeaderEnd = commandLineHeaderStart - 1; |
104 | } |
105 | if (line.startsWith('--------- ------------' )) { |
106 | inTableBody = true; |
107 | continue; |
108 | } |
109 | if (!inTableBody || line.isEmpty) { |
110 | continue; |
111 | } |
112 | if (line.length < commandLineHeaderStart) { |
113 | continue; |
114 | } |
115 | |
116 | // 3/11/2019 11:01:54 AM |
117 | // 12/11/2019 11:01:54 AM |
118 | String rawTime = line.substring( |
119 | creationDateHeaderStart, |
120 | creationDateHeaderEnd, |
121 | ).trim(); |
122 | |
123 | if (rawTime[1] == '/' ) { |
124 | rawTime = '0 $rawTime' ; |
125 | } |
126 | if (rawTime[4] == '/' ) { |
127 | rawTime = ' ${rawTime.substring(0, 3)}0 ${rawTime.substring(3)}' ; |
128 | } |
129 | final String year = rawTime.substring(6, 10); |
130 | final String month = rawTime.substring(3, 5); |
131 | final String day = rawTime.substring(0, 2); |
132 | String time = rawTime.substring(11, 19); |
133 | if (time[7] == ' ' ) { |
134 | time = '0 $time' .trim(); |
135 | } |
136 | if (rawTime.endsWith('PM' )) { |
137 | final int hours = int.parse(time.substring(0, 2)); |
138 | time = ' ${hours + 12}${time.substring(2)}' ; |
139 | } |
140 | |
141 | final int pid = int.parse(line.substring(0, processIdHeaderSize).trim()); |
142 | final DateTime creationDate = DateTime.parse(' $year- $month- ${day}T $time' ); |
143 | final String commandLine = line.substring(commandLineHeaderStart).trim(); |
144 | yield RunningProcessInfo(pid, commandLine, creationDate); |
145 | } |
146 | } |
147 | |
148 | @visibleForTesting |
149 | Future<Set<RunningProcessInfo>> posixRunningProcesses( |
150 | String? processName, |
151 | ProcessManager processManager, |
152 | ) async { |
153 | // Cirrus is missing this in Linux for some reason. |
154 | if (!processManager.canRun('ps' )) { |
155 | print('Cannot list processes on this system: "ps" not available.' ); |
156 | return <RunningProcessInfo>{}; |
157 | } |
158 | final ProcessResult result = await processManager.run(<String>[ |
159 | 'ps' , |
160 | '-eo' , |
161 | 'lstart,pid,command' , |
162 | ]); |
163 | if (result.exitCode != 0) { |
164 | print('Could not list processes!' ); |
165 | print(result.stderr); |
166 | print(result.stdout); |
167 | return <RunningProcessInfo>{}; |
168 | } |
169 | return processPsOutput(result.stdout as String, processName).toSet(); |
170 | } |
171 | |
172 | /// Parses the output of the command in [posixRunningProcesses]. |
173 | /// |
174 | /// E.g.: |
175 | /// |
176 | /// STARTED PID COMMAND |
177 | /// Sat Mar 9 20:12:47 2019 1 /sbin/launchd |
178 | /// Sat Mar 9 20:13:00 2019 49 /usr/sbin/syslogd |
179 | @visibleForTesting |
180 | Iterable<RunningProcessInfo> processPsOutput( |
181 | String output, |
182 | String? processName, |
183 | ) sync* { |
184 | bool inTableBody = false; |
185 | for (String line in output.split('\n' )) { |
186 | if (line.trim().startsWith('STARTED' )) { |
187 | inTableBody = true; |
188 | continue; |
189 | } |
190 | if (!inTableBody || line.isEmpty) { |
191 | continue; |
192 | } |
193 | |
194 | if (processName != null && !line.contains(processName)) { |
195 | continue; |
196 | } |
197 | if (line.length < 25) { |
198 | continue; |
199 | } |
200 | |
201 | // 'Sat Feb 16 02:29:55 2019' |
202 | // 'Sat Mar 9 20:12:47 2019' |
203 | const Map<String, String> months = <String, String>{ |
204 | 'Jan' : '01' , |
205 | 'Feb' : '02' , |
206 | 'Mar' : '03' , |
207 | 'Apr' : '04' , |
208 | 'May' : '05' , |
209 | 'Jun' : '06' , |
210 | 'Jul' : '07' , |
211 | 'Aug' : '08' , |
212 | 'Sep' : '09' , |
213 | 'Oct' : '10' , |
214 | 'Nov' : '11' , |
215 | 'Dec' : '12' , |
216 | }; |
217 | final String rawTime = line.substring(0, 24); |
218 | |
219 | final String year = rawTime.substring(20, 24); |
220 | final String month = months[rawTime.substring(4, 7)]!; |
221 | final String day = rawTime.substring(8, 10).replaceFirst(' ' , '0' ); |
222 | final String time = rawTime.substring(11, 19); |
223 | |
224 | final DateTime creationDate = DateTime.parse(' $year- $month- ${day}T $time' ); |
225 | line = line.substring(24).trim(); |
226 | final int nextSpace = line.indexOf(' ' ); |
227 | final int pid = int.parse(line.substring(0, nextSpace)); |
228 | final String commandLine = line.substring(nextSpace + 1); |
229 | yield RunningProcessInfo(pid, commandLine, creationDate); |
230 | } |
231 | } |
232 | |