• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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