blob: 3df0e4cc9067e5207335d2ebd4ace7d5b7e6c225 [file] [log] [blame]
// 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(),
],
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();