| // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:io'; | |
| import 'package:meta/meta.dart'; | |
| /// A utility class to get information about the Dart related process running on | |
| /// this machine. | |
| class ProcessInfo { | |
| static final wsRegex = RegExp(r'\s+'); | |
| final String command; | |
| final String commandLine; | |
| final int memoryMb; | |
| final double? cpuPercent; | |
| final String? elapsedTime; | |
| @visibleForTesting | |
| static ProcessInfo parseMacos(String line, {bool elideFilePaths = true}) { | |
| // "33712 0.0 01-19:07:19 launchd ..." | |
| line = line.replaceAll(wsRegex, ' '); | |
| String nextWord() { | |
| var index = line.indexOf(' '); | |
| var word = line.substring(0, index); | |
| line = line.substring(index + 1); | |
| return word; | |
| } | |
| var mb = nextWord(); | |
| var cpu = nextWord(); | |
| var elapsedTime = nextWord(); | |
| var commandLine = line.trim(); | |
| if (elideFilePaths) { | |
| return ProcessInfo.create( | |
| command: _getCommandFrom(commandLine), | |
| commandLine: _sanitizeCommandLine(commandLine, preferSnapshot: true), | |
| memoryMb: int.parse(mb) ~/ 1024, | |
| cpuPercent: double.tryParse(cpu.replaceAll(',', '.')), | |
| elapsedTime: elapsedTime, | |
| ); | |
| } else { | |
| return ProcessInfo.create( | |
| command: _getCommandFrom(commandLine), | |
| commandLine: commandLine, | |
| memoryMb: int.parse(mb) ~/ 1024, | |
| cpuPercent: double.tryParse(cpu.replaceAll(',', '.')), | |
| elapsedTime: elapsedTime, | |
| ); | |
| } | |
| } | |
| static ProcessInfo? _parseLinux(String line, {bool elideFilePaths = true}) { | |
| // "33712 0.0 01-19:07:19 launchd ..." | |
| line = line.replaceAll(wsRegex, ' '); | |
| String nextWord() { | |
| var index = line.indexOf(' '); | |
| var word = line.substring(0, index); | |
| line = line.substring(index + 1); | |
| return word; | |
| } | |
| var mb = nextWord(); | |
| var cpu = nextWord(); | |
| var elapsedTime = nextWord(); | |
| var commandLine = line.trim(); | |
| if (commandLine.startsWith('[') && commandLine.endsWith(']')) { | |
| return null; | |
| } | |
| if (elideFilePaths) { | |
| return ProcessInfo.create( | |
| command: _getCommandFrom(commandLine), | |
| commandLine: _sanitizeCommandLine(commandLine, preferSnapshot: true), | |
| memoryMb: int.parse(mb) ~/ 1024, | |
| cpuPercent: double.tryParse(cpu.replaceAll(',', '.')), | |
| elapsedTime: elapsedTime, | |
| ); | |
| } else { | |
| return ProcessInfo.create( | |
| command: _getCommandFrom(commandLine), | |
| commandLine: commandLine, | |
| memoryMb: int.parse(mb) ~/ 1024, | |
| cpuPercent: double.tryParse(cpu.replaceAll(',', '.')), | |
| elapsedTime: elapsedTime, | |
| ); | |
| } | |
| } | |
| @visibleForTesting | |
| static ProcessInfo? parseWindows(String line) { | |
| String stripQuotes(String item) { | |
| if (item.startsWith('"')) item = item.substring(1); | |
| if (item.endsWith('"')) item = item.substring(0, item.length - 1); | |
| return item; | |
| } | |
| // "dart.exe","12068","Console","1","233,384 K" | |
| var items = stripQuotes(line).split('","'); | |
| if (items.isEmpty) { | |
| return null; | |
| } | |
| int parseMemory(String value) { | |
| if (value.contains(' ')) value = value.substring(0, value.indexOf(' ')); | |
| value = value.replaceAll(',', ''); | |
| return (int.tryParse(value) ?? 0) ~/ 1024; | |
| } | |
| return ProcessInfo.create( | |
| command: items.first, | |
| commandLine: items.first, | |
| memoryMb: parseMemory(items.length >= 5 ? items[4] : '0'), | |
| cpuPercent: null, | |
| elapsedTime: null, | |
| ); | |
| } | |
| factory ProcessInfo.create({ | |
| required String command, | |
| required String commandLine, | |
| required int memoryMb, | |
| required double? cpuPercent, | |
| required String? elapsedTime, | |
| }) { | |
| // Patch up the name if necessary (spaces in the path can throw off the | |
| // parsers). | |
| if (command.endsWith('.snapshot')) { | |
| commandLine = '$command $commandLine'.trim(); | |
| command = 'dart'; | |
| } | |
| return ProcessInfo._( | |
| command: command, | |
| commandLine: commandLine, | |
| memoryMb: memoryMb, | |
| cpuPercent: cpuPercent, | |
| elapsedTime: elapsedTime, | |
| ); | |
| } | |
| const ProcessInfo._({ | |
| required this.command, | |
| required this.commandLine, | |
| required this.memoryMb, | |
| required this.cpuPercent, | |
| required this.elapsedTime, | |
| }); | |
| /// Return the Dart related processes. | |
| /// | |
| /// This will try to exclude the process for the VM currently running | |
| /// 'dart info'. | |
| /// | |
| /// This will return `null` if we don't support listing the process on the | |
| /// current platform. | |
| static List<ProcessInfo>? getProcessInfo({bool elideFilePaths = true}) { | |
| List<ProcessInfo>? processInfo; | |
| if (Platform.isMacOS) { | |
| processInfo = _getProcessInfoMacOS(elideFilePaths: elideFilePaths); | |
| } else if (Platform.isLinux) { | |
| processInfo = _getProcessInfoLinux(elideFilePaths: elideFilePaths); | |
| } else if (Platform.isWindows) { | |
| processInfo = _getProcessInfoWindows(); | |
| } | |
| if (processInfo != null) { | |
| // Remove the 'dart info' entry. | |
| processInfo = processInfo | |
| .where((process) => process.commandLine != 'dart info') | |
| .toList(); | |
| // Sort. | |
| processInfo.sort((a, b) => a.commandLine.compareTo(b.commandLine)); | |
| } | |
| return processInfo; | |
| } | |
| /// Return the given [commandLine] with path-like elements replaced with | |
| /// shorter placeholders. | |
| static String _sanitizeCommandLine( | |
| String commandLine, { | |
| bool preferSnapshot = true, | |
| }) { | |
| final sep = Platform.pathSeparator; | |
| var args = commandLine.split(' '); | |
| // If we're running 'dart foo.snapshot ...', and we're already adjusting | |
| // the command line, shorten the command line so it appears that the | |
| // snapshot is being run directly (some command lines can be very long | |
| // otherwise). | |
| var index = args.indexWhere((arg) => arg.endsWith('.snapshot')); | |
| if (index != -1) { | |
| args = args.skip(index).toList(); | |
| } | |
| String sanitizeArg(String arg) { | |
| if (!arg.contains(sep)) return arg; | |
| int start = arg.indexOf(sep); | |
| int end = arg.lastIndexOf(sep); | |
| if (start == end) return arg; | |
| return '${arg.substring(0, start)}<path>${arg.substring(end)}'; | |
| } | |
| return [ | |
| args.first.split(sep).last, | |
| ...args.skip(1).map(sanitizeArg), | |
| ].join(' '); | |
| } | |
| @override | |
| String toString() => | |
| 'ProcessInfo(memoryMb: $memoryMb, cpuPercent: $cpuPercent, elapsedTime:' | |
| ' $elapsedTime, command: $command, commandLine: $commandLine)'; | |
| } | |
| List<ProcessInfo> _getProcessInfoMacOS({bool elideFilePaths = true}) { | |
| var result = Process.runSync('ps', ['-eo', 'rss,pcpu,etime,args']); | |
| if (result.exitCode != 0) { | |
| return const []; | |
| } | |
| // RSS %CPU ELAPSED ARGS | |
| // 33712 0.0 01-19:07:19 launchd | |
| // 52624 0.0 01-19:06:18 logd | |
| // 4848 0.0 01-19:06:18 smd | |
| var lines = (result.stdout as String).split('\n'); | |
| return lines | |
| .skip(1) | |
| .map((line) => line.trim()) | |
| .where((line) => line.isNotEmpty) | |
| .map( | |
| (line) => ProcessInfo.parseMacos(line, elideFilePaths: elideFilePaths), | |
| ) | |
| .where(_isProcessDartRelated) | |
| .toList(); | |
| } | |
| List<ProcessInfo> _getProcessInfoLinux({bool elideFilePaths = true}) { | |
| var result = Process.runSync('ps', ['-eo', 'rss,pcpu,etime,args']); | |
| if (result.exitCode != 0) { | |
| return const []; | |
| } | |
| var lines = (result.stdout as String).split('\n'); | |
| return lines | |
| .skip(1) | |
| .map((line) => line.trim()) | |
| .where((line) => line.isNotEmpty) | |
| .map( | |
| (line) => ProcessInfo._parseLinux(line, elideFilePaths: elideFilePaths), | |
| ) | |
| .whereType<ProcessInfo>() | |
| .where(_isProcessDartRelated) | |
| .toList(); | |
| } | |
| List<ProcessInfo> _getProcessInfoWindows() { | |
| // TODO(devoncarew): Use tasklist /v to retrieve process elapsed time info. | |
| var result = Process.runSync('tasklist', ['/nh', '/fo', 'csv']); | |
| if (result.exitCode != 0) { | |
| return const []; | |
| } | |
| // "smss.exe","608","Services","0","288 K" | |
| // "csrss.exe","888","Services","0","3,084 K" | |
| // "wininit.exe","628","Services","0","1,104 K" | |
| // "dart.exe","12068","Console","1","233,384 K" | |
| var lines = (result.stdout as String).split('\n'); | |
| return lines | |
| .skip(1) | |
| .map((line) => line.trim()) | |
| .where((line) => line.isNotEmpty) | |
| .map((line) => ProcessInfo.parseWindows(line)) | |
| .whereType<ProcessInfo>() | |
| .where(_isProcessDartRelated) | |
| .toList(); | |
| } | |
| bool _isProcessDartRelated(ProcessInfo process) { | |
| return process.command == 'dart' || | |
| process.command == 'dart.exe' || | |
| process.command == 'dartvm' || | |
| process.command == 'dartvm.exe'; | |
| } | |
| String _getCommandFrom(String commandLine) { | |
| var command = commandLine.split(' ').first; | |
| return command.split(Platform.pathSeparator).last; | |
| } |