[ Service ] Add HTTP and DevFS support to package:dart_runtime_service
This change adds support for invoking RPCs via HTTP requests, allowing for interacting with the service without needing to establish a web socket connection. This change also adds support for the development file system, otherwise known as DevFS. DevFS is a (currently) undocumented feature provided by the VM service that gives clients limited file system access within a directory contained in the system's temp directory. This is currently used by Flutter to push kernel files to the device when performing a hot reload. The DevFS implementation for package:dart_runtime_service_vm removes the long deprecated support for `path` parameters, leaving `uri`s as the only supported format for specifying file system types. Existing DevFS tests have been updated to replace `path` with `uri` in preparation for dart_runtime_service_vm becoming the new default VM service. This change brings the package:vm_service test suite pass rate to ~95%. Change-Id: Ib7d95db5788c37408d3dec79926ca7206660a43f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/488280 Reviewed-by: Jessy Yameogo <yjessy@google.com> Reviewed-by: Nicholas Shahan <nshahan@google.com> Commit-Queue: Ben Konyi <bkonyi@google.com>
This commit is contained in:
@@ -11,6 +11,7 @@ export 'src/dart_runtime_service_rpcs.dart'
|
||||
RpcHandlerWithNoParameters,
|
||||
RpcHandlerWithParameters,
|
||||
ServiceRpcHandler;
|
||||
export 'src/devfs.dart';
|
||||
export 'src/event_streams.dart';
|
||||
export 'src/exceptions.dart';
|
||||
export 'src/expression_evaluator.dart';
|
||||
|
||||
@@ -177,11 +177,11 @@ class DartRuntimeService {
|
||||
|
||||
/// Creates an artificial client to process JSON-RPC requests from
|
||||
/// non-standard sources (e.g., from native code).
|
||||
void addArtificialClient({
|
||||
Client addArtificialClient({
|
||||
required StreamChannel<String> connection,
|
||||
required String name,
|
||||
}) {
|
||||
clientManager.addClient(connection: connection, name: name);
|
||||
return clientManager.addClient(connection: connection, name: name);
|
||||
}
|
||||
|
||||
Future<void> _startServer() async {
|
||||
@@ -272,6 +272,11 @@ class DartRuntimeService {
|
||||
);
|
||||
}
|
||||
|
||||
if (!config.disableOriginCheck) {
|
||||
_logger.info('Origin checks are enabled. Adding CORS check handler.');
|
||||
pipeline = pipeline.addMiddleware(originCheckMiddleware(frontend: this));
|
||||
}
|
||||
|
||||
var handlerCascade = shelf.Cascade();
|
||||
if (config.sseHandlerPath != null) {
|
||||
_logger.info(
|
||||
@@ -294,6 +299,9 @@ class DartRuntimeService {
|
||||
webSocketClientHandler(clientManager: clientManager),
|
||||
);
|
||||
|
||||
_logger.info('HTTP requests are accepted. Adding HTTP request handler.');
|
||||
handlerCascade = handlerCascade.add(httpRequestHandler(frontend: this));
|
||||
|
||||
_logger.info('Shelf handlers generated.');
|
||||
return pipeline.addHandler(handlerCascade.handler);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
// 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 'package:meta/meta.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
|
||||
import 'dart_runtime_service.dart';
|
||||
import 'dart_runtime_service_rpcs.dart';
|
||||
@@ -12,6 +14,8 @@ import 'event_streams.dart';
|
||||
import 'expression_evaluator.dart';
|
||||
import 'isolate_manager.dart';
|
||||
|
||||
typedef OptionalHandler = FutureOr<Response?> Function(Request);
|
||||
|
||||
/// A backend implementation of a service used to inject non-common
|
||||
/// functionality into a [DartRuntimeService].
|
||||
abstract class DartRuntimeServiceBackend<IM extends IsolateManager> {
|
||||
@@ -84,4 +88,13 @@ abstract class DartRuntimeServiceBackend<IM extends IsolateManager> {
|
||||
/// any registered RPCs or service extensions provided by other clients.
|
||||
UnmodifiableListView<RpcHandlerWithParameters> get fallbacks =>
|
||||
UnmodifiableListView(const []);
|
||||
|
||||
/// A custom handler for handling HTTP requests.
|
||||
///
|
||||
/// This handler is invoked before attempting to execute the HTTP request as
|
||||
/// an RPC invocation or performing a redirection to a developer tool (e.g.,
|
||||
/// DevTools). Returning null from the handler indicates that the request was
|
||||
/// not handled by the custom handler.
|
||||
OptionalHandler get httpHandler =>
|
||||
(_) => null;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ class DartRuntimeServiceOptions {
|
||||
this.enableLogging = false,
|
||||
this.port = 0,
|
||||
this.disableAuthCodes = false,
|
||||
this.disableOriginCheck = false,
|
||||
this.sseHandlerPath,
|
||||
this.autoStart = true,
|
||||
});
|
||||
@@ -28,6 +29,11 @@ class DartRuntimeServiceOptions {
|
||||
/// Defaults to false.
|
||||
final bool disableAuthCodes;
|
||||
|
||||
/// If true, CORS requests to the service will be accepted.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool disableOriginCheck;
|
||||
|
||||
/// If non-null, allow for SSE connections to be established at
|
||||
/// [sseHandlerPath].
|
||||
///
|
||||
@@ -41,6 +47,7 @@ class DartRuntimeServiceOptions {
|
||||
bool? enableLogging,
|
||||
int? port,
|
||||
bool? disableAuthCodes,
|
||||
bool? disableOriginCheck,
|
||||
String? sseHandlerPath,
|
||||
bool? autoStart,
|
||||
}) {
|
||||
@@ -48,6 +55,7 @@ class DartRuntimeServiceOptions {
|
||||
enableLogging: enableLogging ?? this.enableLogging,
|
||||
port: port ?? this.port,
|
||||
disableAuthCodes: disableAuthCodes ?? this.disableAuthCodes,
|
||||
disableOriginCheck: disableOriginCheck ?? this.disableOriginCheck,
|
||||
sseHandlerPath: sseHandlerPath ?? this.sseHandlerPath,
|
||||
autoStart: autoStart ?? this.autoStart,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
// Copyright (c) 2026, 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:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:vm_service/vm_service.dart' hide Response;
|
||||
|
||||
import 'dart_runtime_service.dart';
|
||||
import 'dart_runtime_service_rpcs.dart';
|
||||
import 'rpc_exceptions.dart';
|
||||
|
||||
/// A [DevelopmentFileSystem] rooted at [rootUri], providing restricted file
|
||||
/// system access to clients.
|
||||
abstract base class DevelopmentFileSystem {
|
||||
DevelopmentFileSystem({required this.name, required this.rootUri});
|
||||
|
||||
final String name;
|
||||
final Uri rootUri;
|
||||
|
||||
static Never throwInvalidUriParameter({
|
||||
required String method,
|
||||
required Object? uri,
|
||||
}) => RpcException.invalidParams.throwExceptionWithDetails(
|
||||
details: "$method: invalid 'uri' parameter: $uri",
|
||||
);
|
||||
|
||||
static Never throwMissingUriParameter({required String method}) =>
|
||||
RpcException.invalidParams.throwExceptionWithDetails(
|
||||
details: "$method: expects the 'uri' parameter",
|
||||
);
|
||||
|
||||
/// Reads the contents of the file at [uri].
|
||||
///
|
||||
/// If the file does not exist, a [RpcException.fileDoesNotExist] exception
|
||||
/// is thrown.
|
||||
Future<RpcResponse> readFile({required String uri});
|
||||
|
||||
/// Writes [bytes] to [uri].
|
||||
Future<void> writeFile({required String uri, required List<int> bytes});
|
||||
|
||||
/// Writes a stream of [bytes] to [uri].
|
||||
Future<void> writeStreamFile({
|
||||
required String uri,
|
||||
required Stream<List<int>> bytes,
|
||||
});
|
||||
|
||||
/// Lists all files contained in the [DevelopmentFileSystem].
|
||||
///
|
||||
/// Each file is reported with its size in bytes and last modified timestamp
|
||||
/// in milliseconds since epoch.
|
||||
Future<RpcResponse> listFiles();
|
||||
|
||||
/// Resolves the [uri] against the [rootUri] of the [DevelopmentFileSystem].
|
||||
///
|
||||
/// [uri] must be a valid file URI with an optional leading `/`. If the
|
||||
/// resolved URI is not within the file systems [rootUri], an
|
||||
/// [RpcException.invalidParams] exception is thrown.
|
||||
Uri resolve({required String method, required String uri}) {
|
||||
// The leading '/' is optional but must be removed before resolving the
|
||||
// URI, otherwise it will be treated as the file system root.
|
||||
if (uri.startsWith('/')) {
|
||||
uri = uri.substring(1);
|
||||
}
|
||||
final parsedUri = Uri.tryParse(uri);
|
||||
if (parsedUri == null) {
|
||||
throwInvalidUriParameter(method: method, uri: uri);
|
||||
}
|
||||
try {
|
||||
// Make sure that this pathUri can be converted to a file path.
|
||||
parsedUri.toFilePath();
|
||||
// ignore: avoid_catching_errors
|
||||
} on UnsupportedError {
|
||||
throwInvalidUriParameter(method: method, uri: uri);
|
||||
}
|
||||
|
||||
final resolvedUri = rootUri.resolveUri(parsedUri);
|
||||
if (!resolvedUri.toString().startsWith(rootUri.toString())) {
|
||||
// Resolved uri must be within the filesystem's base uri.
|
||||
throwInvalidUriParameter(method: method, uri: uri);
|
||||
}
|
||||
return resolvedUri;
|
||||
}
|
||||
|
||||
Map<String, String> toJson() => {
|
||||
'type': 'FileSystem',
|
||||
'name': name,
|
||||
'uri': rootUri.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
/// A collection of [DevelopmentFileSystem]s.
|
||||
abstract base class DevelopmentFileSystemCollection {
|
||||
List<String> get fsNames;
|
||||
|
||||
/// Destroys all [DevelopmentFileSystem]s in the collection.
|
||||
Future<void> cleanup();
|
||||
|
||||
/// Creates a new [DevelopmentFileSystem] named [name].
|
||||
///
|
||||
/// Throws a [RpcException.fileSystemAlreadyExists] if the file system has
|
||||
/// already been created.
|
||||
Future<DevelopmentFileSystem> createFileSystem({required String name});
|
||||
|
||||
/// Destroys the [DevelopmentFileSystem] with name [name].
|
||||
///
|
||||
/// Throws a [RpcException.fileSystemDoesNotExist] if the file system does
|
||||
/// not exist.
|
||||
Future<void> deleteFileSystem({required String name});
|
||||
|
||||
/// Retrieves an existing [DevelopmentFileSystem] based on a JSON-RPC
|
||||
/// request.
|
||||
///
|
||||
/// Throws a [RpcException.fileSystemDoesNotExist] if the file system does
|
||||
/// not exist.
|
||||
DevelopmentFileSystem getFileSystem({required String name});
|
||||
}
|
||||
|
||||
/// A development file system used by service clients to upload compilation
|
||||
/// artifacts and assets for use by the runtime.
|
||||
class DevFS<DevFSBackend extends DevelopmentFileSystemCollection> {
|
||||
DevFS({required this._fileSystems});
|
||||
|
||||
final DevFSBackend _fileSystems;
|
||||
final _logger = Logger('$DevFS');
|
||||
|
||||
static const _kFsName = 'fsName';
|
||||
static const _kUri = 'uri';
|
||||
static const _kFiles = 'files';
|
||||
static const _kFileContents = 'fileContents';
|
||||
|
||||
late final rpcs = UnmodifiableListView<ServiceRpcHandler>([
|
||||
('_listDevFS', listDevFS),
|
||||
('_createDevFS', createDevFS),
|
||||
('_deleteDevFS', deleteDevFS),
|
||||
('_readDevFSFile', readDevFSFile),
|
||||
('_writeDevFSFile', writeDevFSFile),
|
||||
('_writeDevFSFiles', writeDevFSFiles),
|
||||
('_listDevFSFiles', listDevFSFiles),
|
||||
]);
|
||||
|
||||
// Destroy the development file systems.
|
||||
Future<void> cleanup() => _fileSystems.cleanup();
|
||||
|
||||
/// Responsible for processing file system writes initiated via an HTTP PUT
|
||||
/// request.
|
||||
///
|
||||
/// In order to write a file, the HTTP PUT request must include the following
|
||||
/// query parameters:
|
||||
/// - `dev_fs_name`: the name of the [DevelopmentFileSystem] to write to.
|
||||
/// - `dev_fs_uri_b64`: the base-64 encoded URI for the file to be written.
|
||||
///
|
||||
/// The request body will be treated as the contents of the file and written
|
||||
/// to the provided URI rooted in the [DevelopmentFileSystem].
|
||||
Future<Response?> handlePutStreamRequest(Request request) async {
|
||||
if (request.method != 'PUT') {
|
||||
return null;
|
||||
}
|
||||
_logger.info('Handling DevFS PUT request: ${request.headers}');
|
||||
String? fsUri;
|
||||
|
||||
const kDevFsName = 'dev_fs_name';
|
||||
const kDevFsUriBase64 = 'dev_fs_uri_b64';
|
||||
|
||||
// Extract the fs name and fs path from the request headers.
|
||||
final fsName = request.headers[kDevFsName];
|
||||
if (fsName == null) {
|
||||
_logger.info('Invalid $kDevFsName. Returning.');
|
||||
// TODO(bkonyi): this is wrong
|
||||
return Response.internalServerError(body: 'Invalid $kDevFsName.');
|
||||
}
|
||||
if (request.headers[kDevFsUriBase64] case final String base64Uri) {
|
||||
fsUri = utf8.decode(base64.decode(base64Uri));
|
||||
}
|
||||
|
||||
if (fsUri == null) {
|
||||
DevelopmentFileSystem.throwMissingUriParameter(method: '_writeDevFSFile');
|
||||
}
|
||||
|
||||
_logger.info('Invoking handlePutStream.');
|
||||
|
||||
final result = await _handlePutStream(
|
||||
fsName: fsName,
|
||||
uri: fsUri,
|
||||
bytes: request.read().cast<List<int>>().transform(gzip.decoder),
|
||||
);
|
||||
_logger.info('handlePutStream response: $result');
|
||||
|
||||
return Response.ok(
|
||||
json.encode({'result': result}),
|
||||
headers: {
|
||||
// We closed the connection for bad origins earlier.
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'content-type': ContentType.json.mimeType,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<RpcResponse> _handlePutStream({
|
||||
required String fsName,
|
||||
required String uri,
|
||||
required Stream<List<int>> bytes,
|
||||
}) async {
|
||||
_logger.info('Handling PUT write to $uri in $fsName');
|
||||
final fs = _fileSystems.getFileSystem(name: fsName);
|
||||
await fs.writeStreamFile(uri: uri, bytes: bytes);
|
||||
return Success().toJson();
|
||||
}
|
||||
|
||||
/// Lists the names of all active [DevelopmentFileSystem]s.
|
||||
RpcResponse listDevFS() =>
|
||||
// TODO(bkonyi): create package:vm_service type if we make this public.
|
||||
{'type': 'FileSystemList', 'fsNames': _fileSystems.fsNames};
|
||||
|
||||
/// Creates a new [DevelopmentFileSystem] with a given `fsName`.
|
||||
///
|
||||
/// If a [DevelopmentFileSystem] with `fsName` already exists, an error is
|
||||
/// returned.
|
||||
Future<RpcResponse> createDevFS(json_rpc.Parameters parameters) async {
|
||||
final fs = await _fileSystems.createFileSystem(
|
||||
name: parameters[_kFsName].asString,
|
||||
);
|
||||
return fs.toJson();
|
||||
}
|
||||
|
||||
/// Deletes the [DevelopmentFileSystem] with name `fsName`.
|
||||
///
|
||||
/// If a [DevelopmentFileSystem] with `fsName` does not exist, an error is
|
||||
/// returned.
|
||||
Future<RpcResponse> deleteDevFS(json_rpc.Parameters parameters) async {
|
||||
await _fileSystems.deleteFileSystem(name: parameters[_kFsName].asString);
|
||||
return Success().toJson();
|
||||
}
|
||||
|
||||
/// Reads a file from `uri` within the [DevelopmentFileSystem] `fsName`.
|
||||
///
|
||||
/// If a [DevelopmentFileSystem] with `fsName` does not exist, or `uri` is
|
||||
/// does not point to a valid file, an error is returned.
|
||||
Future<RpcResponse> readDevFSFile(json_rpc.Parameters parameters) async {
|
||||
final fs = _fileSystems.getFileSystem(name: parameters[_kFsName].asString);
|
||||
final uri = parameters[_kUri].asString;
|
||||
return await fs.readFile(uri: uri);
|
||||
}
|
||||
|
||||
/// Writes `fileContents` to `uri` within the [DevelopmentFileSystem]
|
||||
/// `fsName`.
|
||||
///
|
||||
/// If a [DevelopmentFileSystem] with `fsName` does not exist, an error is
|
||||
/// returned.
|
||||
Future<RpcResponse> writeDevFSFile(json_rpc.Parameters parameters) async {
|
||||
final fs = _fileSystems.getFileSystem(name: parameters[_kFsName].asString);
|
||||
final path = parameters[_kUri].asString;
|
||||
final fileContents = parameters[_kFileContents].asString;
|
||||
final decodedFileContents = base64.decode(fileContents);
|
||||
await fs.writeFile(uri: path, bytes: decodedFileContents);
|
||||
return Success().toJson();
|
||||
}
|
||||
|
||||
/// Writes multiple `files` within the [DevelopmentFileSystem] `fsName`.
|
||||
///
|
||||
/// Each entry in `files` is a list with two entries:
|
||||
/// - The URI of the file to be written to.
|
||||
/// - The contents of the file.
|
||||
///
|
||||
/// If a [DevelopmentFileSystem] with `fsName` does not exist, an error is
|
||||
/// returned.
|
||||
Future<RpcResponse> writeDevFSFiles(json_rpc.Parameters parameters) async {
|
||||
final fs = _fileSystems.getFileSystem(name: parameters[_kFsName].asString);
|
||||
final files = parameters[_kFiles].asList.cast<Object?>();
|
||||
final processed = <(String, Uint8List)>[];
|
||||
|
||||
Never throwInvalidFiles({required int index, required Object? fileInfo}) =>
|
||||
RpcException.invalidParams.throwExceptionWithDetails(
|
||||
details:
|
||||
"_writeDevFSFiles: invalid '$_kFiles' parameter at index $index: "
|
||||
'$fileInfo',
|
||||
);
|
||||
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
final fileInfo = files[i];
|
||||
if (fileInfo case [final String uriString, final String contents]) {
|
||||
try {
|
||||
fs.resolve(method: '_writeDevFSFiles', uri: uriString);
|
||||
processed.add((uriString, base64.decode(contents)));
|
||||
} catch (_) {
|
||||
throwInvalidFiles(index: i, fileInfo: fileInfo);
|
||||
}
|
||||
} else {
|
||||
throwInvalidFiles(index: i, fileInfo: fileInfo);
|
||||
}
|
||||
}
|
||||
final pendingWrites = <Future<void>>[];
|
||||
for (final (path, decodedContents) in processed) {
|
||||
pendingWrites.add(fs.writeFile(uri: path, bytes: decodedContents));
|
||||
}
|
||||
await Future.wait(pendingWrites);
|
||||
return Success().toJson();
|
||||
}
|
||||
|
||||
/// Lists the set of files contained within the [DevelopmentFileSystem],
|
||||
/// `fsName`.
|
||||
///
|
||||
/// If a [DevelopmentFileSystem] with `fsName` does not exist, an error is
|
||||
/// returned.
|
||||
Future<RpcResponse> listDevFSFiles(json_rpc.Parameters parameters) async {
|
||||
final fs = _fileSystems.getFileSystem(name: parameters[_kFsName].asString);
|
||||
return await fs.listFiles();
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,21 @@
|
||||
// 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:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
||||
import 'package:sse/server/sse_handler.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
import 'clients.dart';
|
||||
import 'dart_runtime_service.dart';
|
||||
import 'dart_runtime_service_backend.dart';
|
||||
|
||||
/// Creates [Middleware] responsible for logging the result of HTTP requests.
|
||||
///
|
||||
@@ -54,6 +62,104 @@ Middleware authCodeVerificationMiddleware({required String authCode}) =>
|
||||
return innerHandler(request.change(path: clientProvidedCode));
|
||||
};
|
||||
|
||||
Middleware originCheckMiddleware({required DartRuntimeService frontend}) =>
|
||||
(Handler innerHandler) => (Request request) {
|
||||
// First check the web-socket specific origin.
|
||||
var origins = request.headers['Sec-WebSocket-Origin'];
|
||||
// Fall back to the general Origin field.
|
||||
origins ??= request.headers['Origin'];
|
||||
if (origins == null) {
|
||||
// No origin sent. This is a non-browser client or a same-origin
|
||||
// request.
|
||||
return innerHandler(request);
|
||||
}
|
||||
|
||||
bool isAllowedOrigin(String origin) {
|
||||
Uri uri;
|
||||
try {
|
||||
uri = Uri.parse(origin);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Explicitly add localhost and 127.0.0.1 on any port (necessary for
|
||||
// adb port forwarding).
|
||||
if ((uri.host == 'localhost') ||
|
||||
(uri.host == InternetAddress.loopbackIPv6.address) ||
|
||||
(uri.host == InternetAddress.loopbackIPv4.address)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final serverUri = frontend.uri;
|
||||
if (uri.port == serverUri.port && uri.host == serverUri.host) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final origin in origins.split(',')) {
|
||||
if (isAllowedOrigin(origin)) {
|
||||
return innerHandler(request);
|
||||
}
|
||||
}
|
||||
return Response.forbidden('forbidden origin');
|
||||
};
|
||||
|
||||
/// Creates a [Handler] responsible for processing HTTP requests.
|
||||
///
|
||||
/// If [frontend] has a [DartRuntimeServiceBackend] with a
|
||||
/// [DartRuntimeServiceBackend.httpHandler] override, the backend's handler
|
||||
/// will be invoked first. Otherwise, the HTTP request is treated as a JSON-RPC
|
||||
/// invocation.
|
||||
Handler httpRequestHandler({required DartRuntimeService frontend}) =>
|
||||
(Request request) async {
|
||||
final logger = Logger('HttpRequestHandler');
|
||||
final method = request.url.pathSegments.firstOrNull ?? '';
|
||||
final params = request.url.queryParameters;
|
||||
logger.info('(${request.method}) ${request.url}');
|
||||
|
||||
try {
|
||||
final backendResult = await frontend.backend.httpHandler(request);
|
||||
if (backendResult != null) {
|
||||
logger.info(
|
||||
'Returning backend provided result: ${backendResult.statusCode}',
|
||||
);
|
||||
return backendResult;
|
||||
}
|
||||
|
||||
final httpClient = StreamChannelController<String>(sync: true);
|
||||
try {
|
||||
frontend.addArtificialClient(
|
||||
connection: httpClient.foreign,
|
||||
name: 'HTTP request',
|
||||
);
|
||||
|
||||
final jsonRpcClient = json_rpc.Client(httpClient.local);
|
||||
unawaited(jsonRpcClient.listen());
|
||||
final result = await jsonRpcClient.sendRequest(method, params);
|
||||
logger.info('HTTP result: $result');
|
||||
return Response.ok(
|
||||
json.encode({'result': result}),
|
||||
headers: {
|
||||
// We closed the connection for bad origins earlier.
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'content-type': ContentType.json.mimeType,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await Future.wait([
|
||||
httpClient.foreign.sink.close(),
|
||||
httpClient.local.sink.close(),
|
||||
]);
|
||||
}
|
||||
} on json_rpc.RpcException catch (e) {
|
||||
return Response.ok(json.encode(e.serialize(method)));
|
||||
} catch (e) {
|
||||
return Response.badRequest(body: e.toString());
|
||||
}
|
||||
};
|
||||
|
||||
/// Creates a [Handler] for incoming web socket connections.
|
||||
Handler webSocketClientHandler({required ClientManager clientManager}) =>
|
||||
webSocketHandler((WebSocketChannel ws, _) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
|
||||
enum RpcException {
|
||||
// These error codes must be kept in sync with those in vm/json_stream.h and
|
||||
// vmservice.dart.
|
||||
invalidParams(code: INVALID_PARAMS, message: 'Invalid parameter.'),
|
||||
serverError(code: SERVER_ERROR, message: 'Server error.'),
|
||||
methodNotFound(code: METHOD_NOT_FOUND, message: 'Method not found.'),
|
||||
internalError(code: INTERNAL_ERROR, message: 'Internal error.'),
|
||||
@@ -20,13 +21,21 @@ enum RpcException {
|
||||
expressionCompilationError(
|
||||
code: 113,
|
||||
message: 'Expression compilation error.',
|
||||
);
|
||||
),
|
||||
fileSystemAlreadyExists(code: 1001, message: 'File system already exists.'),
|
||||
fileSystemDoesNotExist(code: 1002, message: 'File system does not exist.'),
|
||||
fileDoesNotExist(code: 1003, message: 'File does not exist.');
|
||||
|
||||
const RpcException({required this.code, required this.message});
|
||||
|
||||
/// Throws a [json_rpc.RpcException] with [code] and [message].
|
||||
Never throwException({Object? data}) => throw toException(data: data);
|
||||
|
||||
/// Throws a [json_rpc.RpcException] with [code] and [message], with [details]
|
||||
/// included in the exception's `data` field.
|
||||
Never throwExceptionWithDetails({required String details}) =>
|
||||
throw toException(data: <String, String>{'details': details});
|
||||
|
||||
/// Builds a [json_rpc.RpcException] with [code] and [message] without
|
||||
/// throwing.
|
||||
json_rpc.RpcException toException({Object? data}) =>
|
||||
|
||||
@@ -121,6 +121,7 @@ Future<void> main([List<String> args = const []]) async {
|
||||
enableLogging: true,
|
||||
port: _port,
|
||||
disableAuthCodes: _authCodesDisabled,
|
||||
disableOriginCheck: _originCheckDisabled,
|
||||
autoStart: _autoStart,
|
||||
),
|
||||
backendBuilder: (frontend) => DartRuntimeServiceVMBackend(
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:stream_channel/stream_channel.dart';
|
||||
|
||||
import 'src/dart_runtime_service_vm_rpcs.dart';
|
||||
import 'src/native_bindings.dart';
|
||||
import 'src/vm_dev_fs.dart';
|
||||
import 'src/vm_expression_evaluator.dart';
|
||||
import 'src/vm_isolate_manager.dart';
|
||||
|
||||
@@ -68,6 +69,7 @@ class DartRuntimeServiceVMBackend
|
||||
|
||||
final _nativeBindings = NativeBindings();
|
||||
final _logger = Logger('$DartRuntimeServiceVMBackend');
|
||||
final _devFs = VMDevelopmentFileSystemCollection.createDevFS();
|
||||
|
||||
@override
|
||||
final VmIsolateManager isolateManager;
|
||||
@@ -79,7 +81,7 @@ class DartRuntimeServiceVMBackend
|
||||
|
||||
@override
|
||||
UnmodifiableListView<ServiceRpcHandler> get rpcs =>
|
||||
UnmodifiableListView(_vmServiceRpcs.rpcs);
|
||||
UnmodifiableListView([..._vmServiceRpcs.rpcs, ..._devFs.rpcs]);
|
||||
|
||||
@override
|
||||
UnmodifiableListView<RpcHandlerWithParameters>
|
||||
@@ -89,6 +91,9 @@ class DartRuntimeServiceVMBackend
|
||||
sendToRuntime,
|
||||
]);
|
||||
|
||||
@override
|
||||
OptionalHandler get httpHandler => _devFs.handlePutStreamRequest;
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {
|
||||
_logger.info('Initializing...');
|
||||
@@ -116,6 +121,7 @@ class DartRuntimeServiceVMBackend
|
||||
await Future.wait([
|
||||
_sigquitSubscription?.cancel() ?? Future<void>.value(),
|
||||
_nativeRpcClientStreamChannelController.local.sink.close(),
|
||||
_devFs.cleanup(),
|
||||
]);
|
||||
isolateControlPort.close();
|
||||
_nativeBindings.onExit();
|
||||
|
||||
@@ -15,7 +15,6 @@ import 'native_bindings.dart';
|
||||
final class DartRuntimeServiceVmRpcs {
|
||||
final _logger = Logger('$DartRuntimeServiceVmRpcs');
|
||||
final _nativeBindings = NativeBindings();
|
||||
|
||||
late final rpcs = UnmodifiableListView<ServiceRpcHandler>([
|
||||
('getSupportedProtocols', getSupportedProtocols),
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
// Copyright (c) 2026, 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:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dart_runtime_service/dart_runtime_service.dart';
|
||||
import 'package:file/local.dart';
|
||||
|
||||
/// An outstanding write request managed by [_WriteLimiter].
|
||||
class _PendingWrite {
|
||||
_PendingWrite({
|
||||
required this._localFs,
|
||||
required this.uri,
|
||||
required this.bytes,
|
||||
});
|
||||
final completer = Completer<void>();
|
||||
|
||||
final LocalFileSystem _localFs;
|
||||
final Uri uri;
|
||||
final Stream<List<int>> bytes;
|
||||
|
||||
Future<void> write() async {
|
||||
final file = _localFs.file(uri);
|
||||
final parentDir = file.parent;
|
||||
await parentDir.create(recursive: true);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
final sink = file.openWrite();
|
||||
await sink.addStream(bytes);
|
||||
await sink.close();
|
||||
completer.complete();
|
||||
_WriteLimiter._writeCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
/// A utility class to schedule and limit the number of concurrent file system
|
||||
/// writes as non-rooted Android devices have a very low limit for the number
|
||||
/// of open files.
|
||||
abstract class _WriteLimiter {
|
||||
static final pendingWrites = <_PendingWrite>[];
|
||||
|
||||
// Artificially cap ourselves to 16.
|
||||
static const _kMaxOpenWrites = 16;
|
||||
static int _openWrites = 0;
|
||||
|
||||
static Future<void> scheduleWrite({
|
||||
required LocalFileSystem localFs,
|
||||
required Uri uri,
|
||||
required List<int> bytes,
|
||||
}) => scheduleWriteStream(
|
||||
localFs: localFs,
|
||||
uri: uri,
|
||||
bytes: Stream.fromIterable([bytes]),
|
||||
);
|
||||
|
||||
static Future<void> scheduleWriteStream({
|
||||
required LocalFileSystem localFs,
|
||||
required Uri uri,
|
||||
required Stream<List<int>> bytes,
|
||||
}) {
|
||||
// Create a new pending write.
|
||||
final pw = _PendingWrite(localFs: localFs, uri: uri, bytes: bytes);
|
||||
pendingWrites.add(pw);
|
||||
_maybeWriteFiles();
|
||||
return pw.completer.future;
|
||||
}
|
||||
|
||||
static void _maybeWriteFiles() {
|
||||
while (_openWrites < _kMaxOpenWrites) {
|
||||
if (pendingWrites.isEmpty) {
|
||||
break;
|
||||
}
|
||||
final pw = pendingWrites.removeLast();
|
||||
pw.write();
|
||||
_openWrites++;
|
||||
}
|
||||
}
|
||||
|
||||
static void _writeCompleted() {
|
||||
_openWrites--;
|
||||
assert(_openWrites >= 0);
|
||||
_maybeWriteFiles();
|
||||
}
|
||||
}
|
||||
|
||||
/// A [DevelopmentFileSystem] rooted at [rootUri], providing restricted file
|
||||
/// system access to clients.
|
||||
final class VMDevelopmentFileSystem extends DevelopmentFileSystem {
|
||||
VMDevelopmentFileSystem({
|
||||
required this._localFs,
|
||||
required super.name,
|
||||
required super.rootUri,
|
||||
});
|
||||
|
||||
final LocalFileSystem _localFs;
|
||||
|
||||
/// Reads the contents of the file at [uri].
|
||||
///
|
||||
/// If the file does not exist, a [RpcException.fileDoesNotExist] exception
|
||||
/// is thrown.
|
||||
@override
|
||||
Future<RpcResponse> readFile({required String uri}) async {
|
||||
try {
|
||||
final bytes = await _localFs
|
||||
.file(resolve(method: '_readDevFSFile', uri: uri))
|
||||
.readAsBytes();
|
||||
// TODO(bkonyi): create package:vm_service type if we make this public.
|
||||
return {'type': 'FSFile', 'fileContents': base64.encode(bytes)};
|
||||
} on PathNotFoundException catch (e) {
|
||||
RpcException.fileDoesNotExist.throwExceptionWithDetails(
|
||||
details: '_readDevFSFile: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes [bytes] to [uri].
|
||||
@override
|
||||
Future<void> writeFile({
|
||||
required String uri,
|
||||
required List<int> bytes,
|
||||
}) async {
|
||||
await _WriteLimiter.scheduleWrite(
|
||||
localFs: _localFs,
|
||||
uri: resolve(method: '_writeDevFSFile', uri: uri),
|
||||
bytes: bytes,
|
||||
);
|
||||
}
|
||||
|
||||
/// Writes a stream of [bytes] to [uri].
|
||||
@override
|
||||
Future<void> writeStreamFile({
|
||||
required String uri,
|
||||
required Stream<List<int>> bytes,
|
||||
}) async {
|
||||
await _WriteLimiter.scheduleWriteStream(
|
||||
localFs: _localFs,
|
||||
uri: resolve(method: '_writeDevFSFile', uri: uri),
|
||||
bytes: bytes,
|
||||
);
|
||||
}
|
||||
|
||||
/// Lists all files contained in the [DevelopmentFileSystem].
|
||||
///
|
||||
/// Each file is reported with its size in bytes and last modified timestamp
|
||||
/// in milliseconds since epoch.
|
||||
@override
|
||||
Future<RpcResponse> listFiles() async {
|
||||
final dir = _localFs.directory(rootUri);
|
||||
final dirPathStr = dir.path;
|
||||
final stream = dir.list(recursive: true);
|
||||
final files = <Map<String, Object?>>[];
|
||||
await for (final fileEntity in stream) {
|
||||
final filePath = Uri.file(fileEntity.path).path;
|
||||
final stat = await fileEntity.stat();
|
||||
if (stat.type == FileSystemEntityType.file &&
|
||||
filePath.startsWith(dirPathStr)) {
|
||||
files.add(<String, Object?>{
|
||||
// Remove any url-encoding in the filenames.
|
||||
'name': Uri.decodeFull('/${filePath.substring(dirPathStr.length)}'),
|
||||
'size': stat.size,
|
||||
'modified': stat.modified.millisecondsSinceEpoch,
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO(bkonyi): create package:vm_service type if we make this public.
|
||||
return <String, Object?>{'type': 'FSFileList', 'files': files};
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of [DevelopmentFileSystem]s.
|
||||
final class VMDevelopmentFileSystemCollection
|
||||
extends DevelopmentFileSystemCollection {
|
||||
/// Creates a [DevFS] instance with a [VMDevelopmentFileSystemCollection]
|
||||
/// backend.
|
||||
static DevFS<VMDevelopmentFileSystemCollection> createDevFS() =>
|
||||
DevFS(fileSystems: VMDevelopmentFileSystemCollection());
|
||||
|
||||
final _fsMap = <String, VMDevelopmentFileSystem>{};
|
||||
final _localFs = const LocalFileSystem();
|
||||
|
||||
@override
|
||||
List<String> get fsNames => _fsMap.keys.toList();
|
||||
|
||||
/// Destroys all [DevelopmentFileSystem]s in the collection.
|
||||
@override
|
||||
Future<void> cleanup() async {
|
||||
await Future.wait(<Future<void>>[
|
||||
for (final fs in _fsMap.values)
|
||||
_localFs.directory(fs.rootUri).delete(recursive: true),
|
||||
]);
|
||||
_fsMap.clear();
|
||||
}
|
||||
|
||||
/// Creates a new [DevelopmentFileSystem] named [name].
|
||||
///
|
||||
/// Throws a [RpcException.fileSystemAlreadyExists] if the file system has
|
||||
/// already been created.
|
||||
@override
|
||||
Future<DevelopmentFileSystem> createFileSystem({required String name}) async {
|
||||
if (_fsMap.containsKey(name)) {
|
||||
RpcException.fileSystemAlreadyExists.throwExceptionWithDetails(
|
||||
details: "_createDevFS: file system '$name' already exists",
|
||||
);
|
||||
}
|
||||
final temp = await _localFs.systemTempDirectory.createTemp(name);
|
||||
final uri = (await temp.childDirectory(name).create()).uri;
|
||||
return _fsMap[name] = VMDevelopmentFileSystem(
|
||||
localFs: _localFs,
|
||||
name: name,
|
||||
rootUri: uri,
|
||||
);
|
||||
}
|
||||
|
||||
/// Destroys the [DevelopmentFileSystem] with name [name].
|
||||
///
|
||||
/// Throws a [RpcException.fileSystemDoesNotExist] if the file system does
|
||||
/// not exist.
|
||||
@override
|
||||
Future<void> deleteFileSystem({required String name}) async {
|
||||
final fs = _fsMap.remove(name);
|
||||
if (fs == null) {
|
||||
RpcException.fileSystemDoesNotExist.throwExceptionWithDetails(
|
||||
details: "_deleteDevFS: file system '$name' does not exist",
|
||||
);
|
||||
}
|
||||
await _localFs.directory(fs.rootUri).delete(recursive: true);
|
||||
}
|
||||
|
||||
/// Retrieves an existing [DevelopmentFileSystem] based on a JSON-RPC
|
||||
/// request.
|
||||
///
|
||||
/// Throws a [RpcException.fileSystemDoesNotExist] if the file system does
|
||||
/// not exist.
|
||||
@override
|
||||
DevelopmentFileSystem getFileSystem({required String name}) {
|
||||
final fs = _fsMap[name];
|
||||
if (fs == null) {
|
||||
RpcException.fileSystemDoesNotExist.throwException();
|
||||
}
|
||||
return fs;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ resolution: workspace
|
||||
# Use 'any' constraints here; we get our versions from the DEPS file.
|
||||
dependencies:
|
||||
dart_runtime_service: any
|
||||
file: any
|
||||
json_rpc_2: any
|
||||
logging: any
|
||||
stream_channel: any
|
||||
|
||||
@@ -25,12 +25,11 @@ Future<String> readResponse(HttpClientResponse response) {
|
||||
}
|
||||
|
||||
final tests = <VMTest>[
|
||||
// Write a file with a ? in the filename.
|
||||
(VmService service) async {
|
||||
const fsId = 'test';
|
||||
const filePath = '/foo/b?ar.dart';
|
||||
const fileUri = 'foo/bar.dart';
|
||||
const fileContents = [0, 1, 2, 3, 4, 5, 6, 255];
|
||||
final filePathBase64 = base64Encode(utf8.encode(filePath));
|
||||
final fileUriBase64 = base64Encode(utf8.encode(fileUri));
|
||||
final fileContentsBase64 = base64Encode(fileContents);
|
||||
|
||||
Future<Map<String, dynamic>> postToDevFS({
|
||||
@@ -41,7 +40,7 @@ final tests = <VMTest>[
|
||||
final request = await client.putUrl(Uri.parse(serviceHttpAddress));
|
||||
request.headers.add('dev_fs_name', fsId);
|
||||
if (!omitDevFsPath) {
|
||||
request.headers.add('dev_fs_path_b64', filePathBase64);
|
||||
request.headers.add('dev_fs_uri_b64', fileUriBase64);
|
||||
}
|
||||
request.add(gzip.encode(content));
|
||||
final response = await request.close();
|
||||
@@ -81,7 +80,12 @@ final tests = <VMTest>[
|
||||
}
|
||||
}
|
||||
}) {
|
||||
expect(details.contains("expects the 'path' parameter"), true);
|
||||
expect(
|
||||
// TODO(bkonyi): remove the 'path' case once we move to the new VM
|
||||
// service implementation.
|
||||
details.contains(RegExp("expects the '(path|uri)' parameter")),
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
invalidResponse(result);
|
||||
}
|
||||
@@ -100,7 +104,7 @@ final tests = <VMTest>[
|
||||
'_readDevFSFile',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
'path': filePath,
|
||||
'uri': fileUri,
|
||||
},
|
||||
);
|
||||
if (result case {'type': 'FSFile', 'fileContents': final String contents}) {
|
||||
@@ -117,7 +121,9 @@ final tests = <VMTest>[
|
||||
'fsName': fsId,
|
||||
},
|
||||
);
|
||||
if (result case {'type': 'FSFileList', 'files': [{'name': filePath}]}) {
|
||||
if (result
|
||||
case {'type': 'FSFileList', 'files': [{'name': final String uri}]}
|
||||
when uri.endsWith(fileUri)) {
|
||||
// Expected
|
||||
} else {
|
||||
invalidResponse(result);
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
// Copyright (c) 2023, 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:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:vm_service/vm_service.dart';
|
||||
|
||||
import '../common/test_helper.dart';
|
||||
import 'private_rpc_common.dart';
|
||||
|
||||
Future<String> readResponse(HttpClientResponse response) {
|
||||
final completer = Completer<String>();
|
||||
final contents = StringBuffer();
|
||||
response.transform(utf8.decoder).listen(
|
||||
(String data) {
|
||||
contents.write(data);
|
||||
},
|
||||
onDone: () => completer.complete(contents.toString()),
|
||||
);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
final tests = <VMTest>[
|
||||
// Write a file with the \r character in the filename.
|
||||
(VmService service) async {
|
||||
const fsId = 'test';
|
||||
const filePath = '/foo/b\rar.dart';
|
||||
const fileContents = [0, 1, 2, 3, 4, 5, 6, 255];
|
||||
final filePathBase64 = base64Encode(utf8.encode(filePath));
|
||||
final fileContentsBase64 = base64Encode(fileContents);
|
||||
|
||||
Future<Map<String, dynamic>> postToDevFS({
|
||||
required List<int> content,
|
||||
bool omitDevFsPath = false,
|
||||
}) async {
|
||||
final client = HttpClient();
|
||||
final request = await client.putUrl(Uri.parse(serviceHttpAddress));
|
||||
request.headers.add('dev_fs_name', fsId);
|
||||
if (!omitDevFsPath) {
|
||||
request.headers.add('dev_fs_path_b64', filePathBase64);
|
||||
}
|
||||
request.add(gzip.encode(content));
|
||||
final response = await request.close();
|
||||
final responseBody = await readResponse(response);
|
||||
client.close();
|
||||
return jsonDecode(responseBody);
|
||||
}
|
||||
|
||||
// Create DevFS.
|
||||
Map<String, dynamic> result = await callMethod(
|
||||
service,
|
||||
'_createDevFS',
|
||||
args: {'fsName': fsId},
|
||||
);
|
||||
|
||||
if (result case {'type': 'FileSystem', 'name': fsId, 'uri': String _}) {
|
||||
// Expected
|
||||
} else {
|
||||
invalidResponse(result);
|
||||
}
|
||||
|
||||
// Write the file by issuing an HTTP PUT.
|
||||
result = await postToDevFS(content: [9]);
|
||||
if (result case {'result': final Map<String, dynamic> innerResult}) {
|
||||
expectSuccess(innerResult);
|
||||
} else {
|
||||
invalidResponse(result);
|
||||
}
|
||||
|
||||
// Trigger an error by issuing an HTTP PUT.
|
||||
result = await postToDevFS(content: fileContents, omitDevFsPath: true);
|
||||
if (result
|
||||
case {
|
||||
'error': {
|
||||
'data': {
|
||||
'details': final String details,
|
||||
}
|
||||
}
|
||||
}) {
|
||||
expect(details.contains("expects the 'path' parameter"), true);
|
||||
} else {
|
||||
invalidResponse(result);
|
||||
}
|
||||
|
||||
// Write the file again but this time with the true file contents.
|
||||
result = await postToDevFS(content: fileContents);
|
||||
if (result case {'result': final Map<String, dynamic> innerResult}) {
|
||||
expectSuccess(innerResult);
|
||||
} else {
|
||||
invalidResponse(result);
|
||||
}
|
||||
|
||||
// Read the file back.
|
||||
result = await callMethod(
|
||||
service,
|
||||
'_readDevFSFile',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
'path': filePath,
|
||||
},
|
||||
);
|
||||
if (result case {'type': 'FSFile', 'fileContents': final String contents}) {
|
||||
expect(contents, fileContentsBase64);
|
||||
} else {
|
||||
invalidResponse(result);
|
||||
}
|
||||
|
||||
// List all the files in the file system.
|
||||
result = await callMethod(
|
||||
service,
|
||||
'_listDevFSFiles',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
},
|
||||
);
|
||||
if (result case {'type': 'FSFileList', 'files': [{'name': filePath}]}) {
|
||||
// Expected
|
||||
} else {
|
||||
invalidResponse(result);
|
||||
}
|
||||
|
||||
// Delete DevFS.
|
||||
result = await callMethod(
|
||||
service,
|
||||
'_deleteDevFS',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
},
|
||||
);
|
||||
expectSuccess(result);
|
||||
},
|
||||
];
|
||||
|
||||
void main(args) => runVMTests(
|
||||
args,
|
||||
tests,
|
||||
'dev_fs_http_put_weird_char_test.dart',
|
||||
);
|
||||
@@ -72,7 +72,7 @@ final tests = <VMTest>[
|
||||
},
|
||||
(VmService service) async {
|
||||
const fsId = 'banana';
|
||||
const filePath = '/foo/bar.dat';
|
||||
final fileUri = 'foo/bar.dat';
|
||||
final fileContents = base64Encode(utf8.encode('fileContents'));
|
||||
|
||||
// Create DevFS.
|
||||
@@ -93,7 +93,7 @@ final tests = <VMTest>[
|
||||
'_readDevFSFile',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
'path': filePath,
|
||||
'uri': fileUri,
|
||||
},
|
||||
);
|
||||
fail('Unreachable');
|
||||
@@ -108,7 +108,7 @@ final tests = <VMTest>[
|
||||
'_writeDevFSFile',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
'path': filePath,
|
||||
'uri': fileUri,
|
||||
'fileContents': fileContents,
|
||||
},
|
||||
);
|
||||
@@ -120,7 +120,7 @@ final tests = <VMTest>[
|
||||
'_readDevFSFile',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
'path': filePath,
|
||||
'uri': fileUri,
|
||||
},
|
||||
);
|
||||
if (result case {'type': 'FSFile', 'fileContents': final String contents}) {
|
||||
@@ -129,19 +129,6 @@ final tests = <VMTest>[
|
||||
invalidResponse(result);
|
||||
}
|
||||
|
||||
// The leading '/' is optional.
|
||||
result = await callMethod(
|
||||
service,
|
||||
'_readDevFSFile',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
'path': filePath.substring(1),
|
||||
},
|
||||
);
|
||||
if (result case {'type': 'FSFile', 'fileContents': final String contents}) {
|
||||
expect(contents, fileContents);
|
||||
}
|
||||
|
||||
// Read a file outside of the fs.
|
||||
try {
|
||||
await callMethod(
|
||||
@@ -149,13 +136,13 @@ final tests = <VMTest>[
|
||||
'_readDevFSFile',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
'path': '../foo',
|
||||
'uri': '../foo',
|
||||
},
|
||||
);
|
||||
fail('Unreachable');
|
||||
} on RPCError catch (e) {
|
||||
expect(e.code, RPCErrorKind.kInvalidParams.code);
|
||||
expect(e.details, "_readDevFSFile: invalid 'path' parameter: ../foo");
|
||||
expect(e.details, "_readDevFSFile: invalid 'uri' parameter: ../foo");
|
||||
}
|
||||
|
||||
// Write a set of files.
|
||||
@@ -165,8 +152,8 @@ final tests = <VMTest>[
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
'files': [
|
||||
['/a', base64Encode(utf8.encode('a_contents'))],
|
||||
['/b', base64Encode(utf8.encode('b_contents'))],
|
||||
['a', base64Encode(utf8.encode('a_contents'))],
|
||||
['b', base64Encode(utf8.encode('b_contents'))],
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -178,7 +165,7 @@ final tests = <VMTest>[
|
||||
'_readDevFSFile',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
'path': '/b',
|
||||
'uri': 'b',
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -74,7 +74,13 @@ final tests = <VMTest>[
|
||||
// Trigger an error by issuing an HTTP PUT.
|
||||
result = await postToDevFS(content: fileContents, omitDevFsUri: true);
|
||||
if (result case {'error': {'data': {'details': final String details}}}) {
|
||||
expect(details.contains("expects the 'path' parameter"), true);
|
||||
print(details);
|
||||
expect(
|
||||
// TODO(bkonyi): remove the 'path' case once we move to the new VM
|
||||
// service implementation.
|
||||
details.contains(RegExp("expects the '(uri|path)' parameter")),
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
invalidResponse(result);
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright (c) 2023, 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:convert';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:vm_service/vm_service.dart';
|
||||
|
||||
import '../common/test_helper.dart';
|
||||
import 'private_rpc_common.dart';
|
||||
|
||||
final tests = <VMTest>[
|
||||
// Write a file with the ? character in the filename.
|
||||
(VmService service) async {
|
||||
const fsId = 'test';
|
||||
const filePath = '/foo/bar?dat';
|
||||
final fileContents = base64Encode(utf8.encode('fileContents'));
|
||||
|
||||
// Create DevFS.
|
||||
Map<String, dynamic> result = await callMethod(
|
||||
service,
|
||||
'_createDevFS',
|
||||
args: {'fsName': fsId},
|
||||
);
|
||||
if (result case {'type': 'FileSystem', 'name': fsId, 'uri': String _}) {
|
||||
// Expected
|
||||
} else {
|
||||
invalidResponse(result);
|
||||
}
|
||||
|
||||
// Write the file.
|
||||
result = await callMethod(
|
||||
service,
|
||||
'_writeDevFSFile',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
'path': filePath,
|
||||
'fileContents': fileContents,
|
||||
},
|
||||
);
|
||||
expectSuccess(result);
|
||||
|
||||
// Read the file back.
|
||||
result = await callMethod(
|
||||
service,
|
||||
'_readDevFSFile',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
'path': filePath,
|
||||
},
|
||||
);
|
||||
if (result case {'type': 'FSFile', 'fileContents': final String contents}) {
|
||||
expect(contents, fileContents);
|
||||
} else {
|
||||
invalidResponse(result);
|
||||
}
|
||||
|
||||
// List all the files in the file system.
|
||||
result = await callMethod(
|
||||
service,
|
||||
'_listDevFSFiles',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
},
|
||||
);
|
||||
if (result
|
||||
case {
|
||||
'type': 'FSFileList',
|
||||
'files': [{'name': '/foo/bar?dat'}],
|
||||
}) {
|
||||
// Expected
|
||||
} else {
|
||||
invalidResponse(result);
|
||||
}
|
||||
|
||||
// Delete DevFS.
|
||||
result = await callMethod(
|
||||
service,
|
||||
'_deleteDevFS',
|
||||
args: {
|
||||
'fsName': fsId,
|
||||
},
|
||||
);
|
||||
expectSuccess(result);
|
||||
},
|
||||
];
|
||||
|
||||
void main(List<String> args) => runVMTests(
|
||||
args,
|
||||
tests,
|
||||
'dev_fs_weird_char_test.dart',
|
||||
);
|
||||
Reference in New Issue
Block a user