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