| // Copyright (c) 2020, 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:async'; | |
| import 'dart:collection'; | |
| import 'dart:io'; | |
| import 'dart:isolate'; | |
| /// Contains methods used to communicate DartDev results back to the VM. | |
| /// | |
| /// Messages are received in runtime/bin/dartdev_isolate.cc. | |
| abstract class VmInteropHandler { | |
| static final Map<String, String> _environmentOverrides = {}; | |
| /// Environment variables that have been modified via [setEnvironmentVariable]. | |
| /// | |
| /// This map can be passed to [Process.start] or [Process.run] to ensure | |
| /// child processes inherit modifications made to the environment during the | |
| /// execution of DartDev. | |
| static Map<String, String> get environmentOverrides => | |
| UnmodifiableMapView(_environmentOverrides); | |
| /// Initializes [VmInteropHandler] to utilize [port] to communicate with the | |
| /// VM. | |
| static void initialize(SendPort? port) => _port = port; | |
| /// Notifies the VM to run [script] with [args] upon DartDev exit. | |
| /// | |
| /// If [packageConfigOverride] is given, that is where the packageConfig is found. | |
| /// | |
| /// If [markMainIsolateAsSystemIsolate] is given and set to true, the spawned | |
| /// isolate will run with `--mark-main-isolate-as-system-isolate` enabled. | |
| /// | |
| /// If [useExecProcess] is given and set to true, the script is executed by | |
| /// execing it (On Linux/Mac the exec call is used, on Windows a new child | |
| /// process is started). | |
| static void run( | |
| String script, | |
| List<String> args, { | |
| String? packageConfigOverride, | |
| // TODO(bkonyi): remove once DartDev moves to AOT and this flag can be | |
| // provided directly to the process spawned by `dart run` and `dart test`. | |
| // | |
| // See https://github.com/dart-lang/sdk/issues/53576 | |
| bool markMainIsolateAsSystemIsolate = false, | |
| bool useExecProcess = false, | |
| /// When [useExecProcess] pass this path to signal that it originates from | |
| /// another file. Used for launching from a dill file while behaving as if | |
| /// launched directly from a dart file. | |
| String? scriptUriOverride, | |
| /// Directory path that will be recursively deleted on VM shutdown. | |
| String? deleteTempDirOnShutdown, | |
| }) { | |
| assert( | |
| useExecProcess || deleteTempDirOnShutdown == null, | |
| 'deleteTempDirOnShutdown is only supported with useExecProcess', | |
| ); | |
| List<String> argsList; | |
| if (useExecProcess && Platform.isWindows) { | |
| // On Windows if a new process is used to execute the script we | |
| // need to escape the script path and all the arguments as we | |
| // construct a single command line string to be passed to the | |
| // Windows process create call. | |
| if (script.contains(' ') && !script.contains('"')) { | |
| // Escape paths that may contain spaces | |
| script = '"$script"'; | |
| } | |
| argsList = [ | |
| for (int i = 0; i < args.length; i++) _windowsArgumentEscape(args[i]), | |
| ]; | |
| } else { | |
| // Copy the list so it doesn't get GC'd underneath us. | |
| argsList = args.toList(); | |
| } | |
| final port = _port; | |
| if (port == null) return; | |
| final message = <dynamic>[ | |
| useExecProcess ? _kResultRunExec : _kResultRun, | |
| script, | |
| if (useExecProcess) scriptUriOverride, | |
| packageConfigOverride, | |
| markMainIsolateAsSystemIsolate, | |
| argsList, | |
| if (useExecProcess) deleteTempDirOnShutdown, | |
| ]; | |
| port.send(message); | |
| } | |
| /// Notifies the VM that DartDev has completed running. If provided a | |
| /// non-zero [exitCode], the VM will terminate with the given exit code. | |
| static void exit(int? exitCode) { | |
| final port = _port; | |
| if (port == null) return; | |
| final message = <dynamic>[_kResultExit, exitCode]; | |
| port.send(message); | |
| } | |
| /// Sets the environment variable [name] to [value] for the current process. | |
| /// | |
| /// If [value] is null, the environment variable is removed. | |
| static Future<void> setEnvironmentVariable(String name, String? value) async { | |
| if (value != null) { | |
| _environmentOverrides[name] = value; | |
| } else { | |
| _environmentOverrides.remove(name); | |
| } | |
| final port = _port; | |
| if (port == null) return; | |
| final replyPort = RawReceivePort(); | |
| final completer = Completer<void>(); | |
| replyPort.handler = (message) { | |
| completer.complete(); | |
| replyPort.close(); | |
| }; | |
| final message = <dynamic>[ | |
| _kResultSetEnvironmentVariable, | |
| replyPort.sendPort, | |
| name, | |
| value, | |
| ]; | |
| port.send(message); | |
| await completer.future; | |
| } | |
| /// This code is identical to the one in process_patch.dart, please ensure | |
| /// changes made here are also done in process_patch.dart. | |
| /// TODO : figure out if this functionality can be abstracted out to a | |
| /// common place. | |
| static String _windowsArgumentEscape(String argument) { | |
| if (argument.isEmpty) { | |
| return '""'; | |
| } | |
| var result = argument; | |
| if (argument.contains('\t') || | |
| argument.contains(' ') || | |
| argument.contains('"')) { | |
| // Produce something that the C runtime on Windows will parse | |
| // back as this string. | |
| // Replace any number of '\' followed by '"' with | |
| // twice as many '\' followed by '\"'. | |
| var backslash = '\\'.codeUnitAt(0); | |
| var sb = StringBuffer(); | |
| var nextPos = 0; | |
| var quotePos = argument.indexOf('"', nextPos); | |
| while (quotePos != -1) { | |
| var numBackslash = 0; | |
| var pos = quotePos - 1; | |
| while (pos >= 0 && argument.codeUnitAt(pos) == backslash) { | |
| numBackslash++; | |
| pos--; | |
| } | |
| sb.write(argument.substring(nextPos, quotePos - numBackslash)); | |
| for (var i = 0; i < numBackslash; i++) { | |
| sb.write(r'\\'); | |
| } | |
| sb.write(r'\"'); | |
| nextPos = quotePos + 1; | |
| quotePos = argument.indexOf('"', nextPos); | |
| } | |
| sb.write(argument.substring(nextPos, argument.length)); | |
| result = sb.toString(); | |
| // Add '"' at the beginning and end and replace all '\' at | |
| // the end with two '\'. | |
| sb = StringBuffer('"'); | |
| sb.write(result); | |
| nextPos = argument.length - 1; | |
| while (argument.codeUnitAt(nextPos) == backslash) { | |
| sb.write('\\'); | |
| nextPos--; | |
| } | |
| sb.write('"'); | |
| result = sb.toString(); | |
| } | |
| return result; | |
| } | |
| // Note: keep in sync with runtime/bin/dartdev.cc | |
| static const int _kResultRun = 1; | |
| static const int _kResultRunExec = 2; | |
| static const int _kResultExit = 3; | |
| static const int _kResultSetEnvironmentVariable = 4; | |
| static SendPort? _port; | |
| } |