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 = |
70 | processName != null |
71 | ? '"Get-CimInstance Win32_Process -Filter \\"name=\' $processName\'\\" | Select-Object ProcessId,CreationDate,CommandLine | Format-Table -AutoSize | Out-String -Width 4096"' |
72 | : '"Get-CimInstance Win32_Process | Select-Object ProcessId,CreationDate,CommandLine | Format-Table -AutoSize | Out-String -Width 4096"' ; |
73 | // TODO(ianh): Unfortunately, there doesn't seem to be a good way to get |
74 | // ProcessManager to run this. |
75 | final ProcessResult result = await Process.run('powershell -command $script' , <String>[]); |
76 | if (result.exitCode != 0) { |
77 | print('Could not list processes!' ); |
78 | print(result.stderr); |
79 | print(result.stdout); |
80 | return <RunningProcessInfo>{}; |
81 | } |
82 | return processPowershellOutput(result.stdout as String).toSet(); |
83 | } |
84 | |
85 | /// Parses the output of the PowerShell script from [windowsRunningProcesses]. |
86 | /// |
87 | /// E.g.: |
88 | /// ProcessId CreationDate CommandLine |
89 | /// --------- ------------ ----------- |
90 | /// 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 |
91 | @visibleForTesting |
92 | Iterable<RunningProcessInfo> processPowershellOutput(String output) sync* { |
93 | const int processIdHeaderSize = 'ProcessId' .length; |
94 | const int creationDateHeaderStart = processIdHeaderSize + 1; |
95 | late int creationDateHeaderEnd; |
96 | late int commandLineHeaderStart; |
97 | bool inTableBody = false; |
98 | for (final String line in output.split('\n' )) { |
99 | if (line.startsWith('ProcessId' )) { |
100 | commandLineHeaderStart = line.indexOf('CommandLine' ); |
101 | creationDateHeaderEnd = commandLineHeaderStart - 1; |
102 | } |
103 | if (line.startsWith('--------- ------------' )) { |
104 | inTableBody = true; |
105 | continue; |
106 | } |
107 | if (!inTableBody || line.isEmpty) { |
108 | continue; |
109 | } |
110 | if (line.length < commandLineHeaderStart) { |
111 | continue; |
112 | } |
113 | |
114 | // 3/11/2019 11:01:54 AM |
115 | // 12/11/2019 11:01:54 AM |
116 | String rawTime = line.substring(creationDateHeaderStart, creationDateHeaderEnd).trim(); |
117 | |
118 | if (rawTime[1] == '/' ) { |
119 | rawTime = '0 $rawTime' ; |
120 | } |
121 | if (rawTime[4] == '/' ) { |
122 | rawTime = ' ${rawTime.substring(0, 3)}0 ${rawTime.substring(3)}' ; |
123 | } |
124 | final String year = rawTime.substring(6, 10); |
125 | final String month = rawTime.substring(3, 5); |
126 | final String day = rawTime.substring(0, 2); |
127 | String time = rawTime.substring(11, 19); |
128 | if (time[7] == ' ' ) { |
129 | time = '0 $time' .trim(); |
130 | } |
131 | if (rawTime.endsWith('PM' )) { |
132 | final int hours = int.parse(time.substring(0, 2)); |
133 | time = ' ${hours + 12}${time.substring(2)}' ; |
134 | } |
135 | |
136 | final int pid = int.parse(line.substring(0, processIdHeaderSize).trim()); |
137 | final DateTime creationDate = DateTime.parse(' $year- $month- ${day}T $time' ); |
138 | final String commandLine = line.substring(commandLineHeaderStart).trim(); |
139 | yield RunningProcessInfo(pid, commandLine, creationDate); |
140 | } |
141 | } |
142 | |
143 | @visibleForTesting |
144 | Future<Set<RunningProcessInfo>> posixRunningProcesses( |
145 | String? processName, |
146 | ProcessManager processManager, |
147 | ) async { |
148 | final ProcessResult result = await processManager.run(<String>[ |
149 | 'ps' , |
150 | '-eo' , |
151 | 'lstart,pid,command' , |
152 | ]); |
153 | if (result.exitCode != 0) { |
154 | print('Could not list processes!' ); |
155 | print(result.stderr); |
156 | print(result.stdout); |
157 | return <RunningProcessInfo>{}; |
158 | } |
159 | return processPsOutput(result.stdout as String, processName).toSet(); |
160 | } |
161 | |
162 | /// Parses the output of the command in [posixRunningProcesses]. |
163 | /// |
164 | /// E.g.: |
165 | /// |
166 | /// STARTED PID COMMAND |
167 | /// Sat Mar 9 20:12:47 2019 1 /sbin/launchd |
168 | /// Sat Mar 9 20:13:00 2019 49 /usr/sbin/syslogd |
169 | @visibleForTesting |
170 | Iterable<RunningProcessInfo> processPsOutput(String output, String? processName) sync* { |
171 | bool inTableBody = false; |
172 | for (String line in output.split('\n' )) { |
173 | if (line.trim().startsWith('STARTED' )) { |
174 | inTableBody = true; |
175 | continue; |
176 | } |
177 | if (!inTableBody || line.isEmpty) { |
178 | continue; |
179 | } |
180 | |
181 | if (processName != null && !line.contains(processName)) { |
182 | continue; |
183 | } |
184 | if (line.length < 25) { |
185 | continue; |
186 | } |
187 | |
188 | // 'Sat Feb 16 02:29:55 2019' |
189 | // 'Sat Mar 9 20:12:47 2019' |
190 | const Map<String, String> months = <String, String>{ |
191 | 'Jan' : '01' , |
192 | 'Feb' : '02' , |
193 | 'Mar' : '03' , |
194 | 'Apr' : '04' , |
195 | 'May' : '05' , |
196 | 'Jun' : '06' , |
197 | 'Jul' : '07' , |
198 | 'Aug' : '08' , |
199 | 'Sep' : '09' , |
200 | 'Oct' : '10' , |
201 | 'Nov' : '11' , |
202 | 'Dec' : '12' , |
203 | }; |
204 | final String rawTime = line.substring(0, 24); |
205 | |
206 | final String year = rawTime.substring(20, 24); |
207 | final String month = months[rawTime.substring(4, 7)]!; |
208 | final String day = rawTime.substring(8, 10).replaceFirst(' ' , '0' ); |
209 | final String time = rawTime.substring(11, 19); |
210 | |
211 | final DateTime creationDate = DateTime.parse(' $year- $month- ${day}T $time' ); |
212 | line = line.substring(24).trim(); |
213 | final int nextSpace = line.indexOf(' ' ); |
214 | final int pid = int.parse(line.substring(0, nextSpace)); |
215 | final String commandLine = line.substring(nextSpace + 1); |
216 | yield RunningProcessInfo(pid, commandLine, creationDate); |
217 | } |
218 | } |
219 | |