| // Copyright (c) 2013, 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. |
| |
| library vmservice_io; |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'dart:_vmservice'; |
| |
| part 'vmservice_server.dart'; |
| |
| // The TCP ip/port that the HTTP server listens on. |
| @pragma('vm:entry-point') |
| int _port = 0; |
| @pragma('vm:entry-point') |
| String _ip = ''; |
| // Should the HTTP server auto start? |
| @pragma('vm:entry-point') |
| bool _autoStart = false; |
| // Should the HTTP server require an auth code? |
| @pragma('vm:entry-point') |
| bool _authCodesDisabled = false; |
| // Should the HTTP server run in devmode? |
| @pragma('vm:entry-point') |
| bool _originCheckDisabled = false; |
| // Location of file to output VM service connection info. |
| @pragma('vm:entry-point') |
| String? _serviceInfoFilename; |
| @pragma('vm:entry-point') |
| bool _isWindows = false; |
| @pragma('vm:entry-point') |
| bool _isFuchsia = false; |
| @pragma('vm:entry-point') |
| var _signalWatch = null; |
| var _signalSubscription; |
| @pragma("vm:entry-point") |
| bool _enableServicePortFallback = false; |
| @pragma("vm:entry-point") |
| bool _waitForDdsToAdvertiseService = false; |
| |
| // HTTP server. |
| Server? server; |
| Future<Server>? serverFuture; |
| _DebuggingSession? ddsInstance; |
| |
| Server _lazyServerBoot() { |
| var localServer = server; |
| if (localServer != null) { |
| return localServer; |
| } |
| // Lazily create service. |
| final service = VMService(); |
| // Lazily create server. |
| localServer = Server(service, _ip, _port, _originCheckDisabled, |
| _authCodesDisabled, _serviceInfoFilename, _enableServicePortFallback); |
| server = localServer; |
| return localServer; |
| } |
| |
| /// Responsible for launching a DevTools instance when the service is started |
| /// via SIGQUIT. |
| class _DebuggingSession { |
| Future<bool> start( |
| String host, |
| String port, |
| bool disableServiceAuthCodes, |
| bool enableDevTools, |
| ) async { |
| final dartPath = Uri.parse(Platform.resolvedExecutable); |
| final dartDir = [ |
| '', // Include leading '/' |
| ...dartPath.pathSegments.sublist( |
| 0, |
| dartPath.pathSegments.length - 1, |
| ), |
| ].join('/'); |
| |
| final fullSdk = dartDir.endsWith('bin'); |
| |
| final ddsSnapshot = [ |
| dartDir, |
| fullSdk ? 'snapshots' : 'gen', |
| 'dds.dart.snapshot', |
| ].join('/'); |
| |
| final devToolsBinaries = [ |
| dartDir, |
| if (fullSdk) 'resources', |
| 'devtools', |
| ].join('/'); |
| |
| const enableLogging = false; |
| _process = await Process.start( |
| dartPath.toString(), |
| [ |
| ddsSnapshot, |
| server!.serverAddress!.toString(), |
| host, |
| port, |
| disableServiceAuthCodes.toString(), |
| enableDevTools.toString(), |
| devToolsBinaries, |
| enableLogging.toString(), |
| _enableServicePortFallback.toString(), |
| ], |
| mode: ProcessStartMode.detachedWithStdio, |
| ); |
| final completer = Completer<void>(); |
| late StreamSubscription stderrSub; |
| stderrSub = _process!.stderr.transform(utf8.decoder).listen((event) { |
| final result = json.decode(event) as Map<String, dynamic>; |
| final state = result['state']; |
| if (state == 'started') { |
| if (result.containsKey('devToolsUri')) { |
| // NOTE: update pkg/dartdev/lib/src/commands/run.dart if this message |
| // is changed to ensure consistency. |
| const devToolsMessagePrefix = |
| 'The Dart DevTools debugger and profiler is available at:'; |
| final devToolsUri = result['devToolsUri']; |
| print('$devToolsMessagePrefix $devToolsUri'); |
| } |
| stderrSub.cancel(); |
| completer.complete(); |
| } else { |
| final error = result['error'] ?? event; |
| final stacktrace = result['stacktrace'] ?? ''; |
| stderrSub.cancel(); |
| completer.completeError( |
| 'Could not start Observatory HTTP server:\n$error\n$stacktrace\n'); |
| } |
| }); |
| try { |
| await completer.future; |
| return true; |
| } catch (e) { |
| stderr.write(e); |
| return false; |
| } |
| } |
| |
| void shutdown() => _process!.kill(); |
| |
| Process? _process; |
| } |
| |
| Future cleanupCallback() async { |
| // Cancel the sigquit subscription. |
| if (_signalSubscription != null) { |
| await _signalSubscription.cancel(); |
| _signalSubscription = null; |
| } |
| final localServer = server; |
| if (localServer != null) { |
| try { |
| await localServer.cleanup(true); |
| } catch (e, st) { |
| print('Error in vm-service shutdown: $e\n$st\n'); |
| } |
| } |
| if (_registerSignalHandlerTimer != null) { |
| _registerSignalHandlerTimer!.cancel(); |
| _registerSignalHandlerTimer = null; |
| } |
| // Call out to embedder's shutdown callback. |
| _shutdown(); |
| } |
| |
| Future<void> ddsConnectedCallback() async { |
| final serviceAddress = server!.serverAddress.toString(); |
| _notifyServerState(serviceAddress); |
| onServerAddressChange(serviceAddress); |
| if (_waitForDdsToAdvertiseService) { |
| await server!.outputConnectionInformation(); |
| } |
| } |
| |
| Future<void> ddsDisconnectedCallback() async { |
| final serviceAddress = server!.serverAddress.toString(); |
| _notifyServerState(serviceAddress); |
| onServerAddressChange(serviceAddress); |
| } |
| |
| Future<Uri> createTempDirCallback(String base) async { |
| final temp = await Directory.systemTemp.createTemp(base); |
| // Underneath the temporary directory, create a directory with the |
| // same name as the DevFS name [base]. |
| final fsUri = temp.uri.resolveUri(Uri.directory(base)); |
| await Directory.fromUri(fsUri).create(); |
| return fsUri; |
| } |
| |
| Future deleteDirCallback(Uri path) async => |
| await Directory.fromUri(path).delete(recursive: true); |
| |
| class PendingWrite { |
| PendingWrite(this.uri, this.bytes); |
| final completer = Completer<void>(); |
| final Uri uri; |
| final List<int> bytes; |
| |
| Future write() async { |
| final file = File.fromUri(uri); |
| final parent_directory = file.parent; |
| await parent_directory.create(recursive: true); |
| if (await file.exists()) { |
| await file.delete(); |
| } |
| await file.writeAsBytes(bytes); |
| completer.complete(); |
| WriteLimiter._writeCompleted(); |
| } |
| } |
| |
| class WriteLimiter { |
| static final pendingWrites = <PendingWrite>[]; |
| |
| // non-rooted Android devices have a very low limit for the number of |
| // open files. Artificially cap ourselves to 16. |
| static const kMaxOpenWrites = 16; |
| static int openWrites = 0; |
| |
| static Future scheduleWrite(Uri path, List<int> bytes) async { |
| // Create a new pending write. |
| final pw = PendingWrite(path, bytes); |
| pendingWrites.add(pw); |
| _maybeWriteFiles(); |
| return pw.completer.future; |
| } |
| |
| static _maybeWriteFiles() { |
| while (openWrites < kMaxOpenWrites) { |
| if (pendingWrites.length > 0) { |
| final pw = pendingWrites.removeLast(); |
| pw.write(); |
| openWrites++; |
| } else { |
| break; |
| } |
| } |
| } |
| |
| static _writeCompleted() { |
| openWrites--; |
| assert(openWrites >= 0); |
| _maybeWriteFiles(); |
| } |
| } |
| |
| Future writeFileCallback(Uri path, List<int> bytes) async => |
| WriteLimiter.scheduleWrite(path, bytes); |
| |
| Future<void> writeStreamFileCallback(Uri path, Stream<List<int>> bytes) async { |
| final file = File.fromUri(path); |
| final parent_directory = file.parent; |
| await parent_directory.create(recursive: true); |
| if (await file.exists()) { |
| await file.delete(); |
| } |
| final sink = await file.openWrite(); |
| await sink.addStream(bytes); |
| await sink.close(); |
| } |
| |
| Future<List<int>> readFileCallback(Uri path) async => |
| await File.fromUri(path).readAsBytes(); |
| |
| Future<List<Map<String, dynamic>>> listFilesCallback(Uri dirPath) async { |
| final dir = Directory.fromUri(dirPath); |
| final dirPathStr = dirPath.path; |
| final stream = dir.list(recursive: true); |
| final result = <Map<String, dynamic>>[]; |
| await for (var fileEntity in stream) { |
| final filePath = Uri.file(fileEntity.path).path; |
| final stat = await fileEntity.stat(); |
| if (stat.type == FileSystemEntityType.file && |
| filePath.startsWith(dirPathStr)) { |
| final map = <String, dynamic>{}; |
| map['name'] = '/' + filePath.substring(dirPathStr.length); |
| map['size'] = stat.size; |
| map['modified'] = stat.modified.millisecondsSinceEpoch; |
| result.add(map); |
| } |
| } |
| return result; |
| } |
| |
| Uri? serverInformationCallback() => _lazyServerBoot().serverAddress; |
| |
| Future<Uri?> webServerControlCallback(bool enable, bool? silenceOutput) async { |
| if (silenceOutput != null) { |
| silentObservatory = silenceOutput; |
| } |
| final _server = _lazyServerBoot(); |
| if (_server.running != enable) { |
| if (enable) { |
| await _server.startup(); |
| } else { |
| await _server.shutdown(true); |
| } |
| } |
| return _server.serverAddress; |
| } |
| |
| void webServerAcceptNewWebSocketConnections(bool enable) { |
| final _server = _lazyServerBoot(); |
| _server.acceptNewWebSocketConnections = enable; |
| } |
| |
| _onSignal(ProcessSignal signal) { |
| if (serverFuture != null) { |
| // Still waiting. |
| return; |
| } |
| final _server = _lazyServerBoot(); |
| // Toggle HTTP server. |
| if (_server.running) { |
| _server.shutdown(true).then((_) async { |
| ddsInstance?.shutdown(); |
| await VMService().clearState(); |
| serverFuture = null; |
| }); |
| } else { |
| _server.startup().then((_) { |
| ddsInstance = _DebuggingSession() |
| ..start( |
| _server._ip, |
| _server._port.toString(), |
| false, |
| true, |
| ); |
| }); |
| } |
| } |
| |
| Timer? _registerSignalHandlerTimer; |
| |
| _registerSignalHandler() { |
| if (VMService().isExiting) { |
| // If the VM started shutting down we don't want to register this signal |
| // handler, otherwise we'll cause the VM to hang after killing the service |
| // isolate. |
| return; |
| } |
| _registerSignalHandlerTimer = null; |
| if (_signalWatch == null) { |
| // Cannot register for signals. |
| return; |
| } |
| if (_isWindows || _isFuchsia) { |
| // Cannot register for signals on Windows or Fuchsia. |
| return; |
| } |
| _signalSubscription = _signalWatch(ProcessSignal.sigquit).listen(_onSignal); |
| } |
| |
| @pragma('vm:entry-point', !const bool.fromEnvironment('dart.vm.product')) |
| main() { |
| // Set embedder hooks. |
| VMServiceEmbedderHooks.cleanup = cleanupCallback; |
| VMServiceEmbedderHooks.createTempDir = createTempDirCallback; |
| VMServiceEmbedderHooks.ddsConnected = ddsConnectedCallback; |
| VMServiceEmbedderHooks.ddsDisconnected = ddsDisconnectedCallback; |
| VMServiceEmbedderHooks.deleteDir = deleteDirCallback; |
| VMServiceEmbedderHooks.writeFile = writeFileCallback; |
| VMServiceEmbedderHooks.writeStreamFile = writeStreamFileCallback; |
| VMServiceEmbedderHooks.readFile = readFileCallback; |
| VMServiceEmbedderHooks.listFiles = listFilesCallback; |
| VMServiceEmbedderHooks.serverInformation = serverInformationCallback; |
| VMServiceEmbedderHooks.webServerControl = webServerControlCallback; |
| VMServiceEmbedderHooks.acceptNewWebSocketConnections = |
| webServerAcceptNewWebSocketConnections; |
| // Always instantiate the vmservice object so that the exit message |
| // can be delivered and waiting loaders can be cancelled. |
| VMService(); |
| if (_autoStart) { |
| final _server = _lazyServerBoot(); |
| _server.startup(); |
| // It's just here to push an event on the event loop so that we invoke the |
| // scheduled microtasks. |
| Timer.run(() {}); |
| } |
| // Register signal handler after a small delay to avoid stalling main |
| // isolate startup. |
| _registerSignalHandlerTimer = Timer(shortDelay, _registerSignalHandler); |
| } |
| |
| @pragma("vm:external-name", "VMServiceIO_Shutdown") |
| external _shutdown(); |