blob: f42174021920b6d69753843089bca7bd84cf5dd4 [file] [edit]
// 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.
// ignore_for_file: implementation_imports
import 'dart:async';
import 'dart:convert';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:analyzer/file_system/overlay_file_system.dart';
import 'package:async/async.dart';
import 'package:json_rpc_2/json_rpc_2.dart';
import 'package:path/path.dart' as p;
import 'package:stream_channel/stream_channel.dart';
import 'resource_provider/resource_provider_ext.dart';
import 'resource_provider/resource_provider_wrap_cwd.dart';
import 'shared.dart' hide FileSystemException;
import 'tools/hot_reload_compiler.dart' show HotReloadCompiler;
import 'tools/language_server.dart';
import 'tools/pub.dart';
final class Worker {
final _rp = MemoryResourceProvider(context: p.posix);
var _config = DartPadConfig();
int _nextLanguageServerId = 1;
int _nextHotReloadCompilerId = 1;
int _nextWorkspaceId = 1;
Worker._();
static Future<Worker> create(
Stream<List<int>> sdkTarStream, {
String? pubHostedUrl,
}) async {
final w = Worker._();
await w._rp.getFolder('/').extractTarStream(sdkTarStream);
final configFile = w._rp.getFile(DartPadConfig.defaultDartPadConfigPath);
if (configFile.exists) {
try {
w._config = DartPadConfig.fromJson(
jsonDecode(configFile.readAsStringSync()) as Map<String, Object?>,
);
} catch (e) {
// TODO(jonasfj): Find a better way to propogate this error.
// This is only relevant for people making their own
// sdk.tar files. But it'd also make general debugging
// easier. To report it better we might also want to
// report progress updates while loading.
print('Error reading dartpad-config.json: $e');
}
}
w._config = w._config.copyWith(pubHostedUrl: pubHostedUrl);
return w;
}
void connect(StreamChannel<String> channel) {
_Session(channel, this);
}
}
class _Session {
final Worker _worker;
late final Peer _rpc;
final _workspaces = <int, _Workspace>{};
_Session(StreamChannel<String> channel, this._worker) {
_rpc = Peer(channel, onUnhandledError: _onUnhandledError);
_rpc.registerMethod('createWorkspace', _createWorkspace);
_rpc.registerMethod('workspace/dispose', _disposeWorkspace);
_rpc.registerMethod(
'workspace/writeFileFromText',
_forwardToWorkspace((ws) => ws._writeFileFromText),
);
_rpc.registerMethod(
'workspace/writeFileFromBytes',
_forwardToWorkspace((ws) => ws._writeFileFromBytes),
);
_rpc.registerMethod(
'workspace/readFileAsText',
_forwardToWorkspace((ws) => ws._readFileAsText),
);
_rpc.registerMethod(
'workspace/readFileAsBytes',
_forwardToWorkspace((ws) => ws._readFileAsBytes),
);
_rpc.registerMethod(
'workspace/deleteFileSystemEntity',
_forwardToWorkspace((ws) => ws._deleteFileSystemEntity),
);
_rpc.registerMethod(
'workspace/stat',
_forwardToWorkspace((ws) => ws._stat),
);
_rpc.registerMethod(
'workspace/listDirectory',
_forwardToWorkspace((ws) => ws._listDirectory),
);
_rpc.registerMethod(
'workspace/importTarArchive',
_forwardToWorkspace((ws) => ws._importTarArchive),
);
_rpc.registerMethod(
'workspace/exportTarArchive',
_forwardToWorkspace((ws) => ws._exportTarArchive),
);
_rpc.registerMethod(
'workspace/createFolder',
_forwardToWorkspace((ws) => ws._createFolder),
);
_rpc.registerMethod('workspace/pub', _forwardToWorkspace((ws) => ws._pub));
_rpc.registerMethod(
'workspace/startHotReloadCompiler',
_forwardToWorkspace((ws) => ws._startHotReloadCompiler),
);
_rpc.registerMethod(
'workspace/hotReloadCompiler/compile',
_forwardToWorkspace((ws) => ws._hotReloadCompilerCompile),
);
_rpc.registerMethod(
'workspace/hotReloadCompiler/close',
_forwardToWorkspace((ws) => ws._hotReloadCompilerClose),
);
_rpc.registerMethod(
'workspace/startLanguageServer',
_forwardToWorkspace((ws) => ws._startLanguageServer),
);
_rpc.registerMethod(
'workspace/languageServer/message',
_forwardToWorkspace((ws) => ws._languageServerMessage),
);
_rpc.registerMethod(
'workspace/languageServer/stop',
_forwardToWorkspace((ws) => ws._stopLanguageServer),
);
unawaited(() async {
await _rpc.listen();
// Delete all workspaces to cleanup resources
await Future.wait(
_workspaces.values.toList().map((ws) => ws._deleteWorkspace()),
);
}());
}
Object? _createWorkspace(Parameters params) async {
final workspaceId = _worker._nextWorkspaceId++;
final workspaceFolder = '/workspace/pad_$workspaceId';
_worker._rp.getFolder(workspaceFolder).create();
_workspaces[workspaceId] = _Workspace(
_worker,
this,
workspaceId,
workspaceFolder,
resourceProviderWithCurrentWorkingDirectory(_worker._rp, workspaceFolder),
);
return {
'workspaceId': workspaceId,
'workspaceFolder': Uri.directory(workspaceFolder).toString(),
};
}
Object? _disposeWorkspace(Parameters params) async {
final workspace = _workspaces.remove(params['workspaceId'].asNum.toInt());
if (workspace != null) {
await workspace._deleteWorkspace();
}
// Deleting a workspace that doesn't exist is a no-op
// This ensures that deletion is an idempotent operation!
return <String, Object?>{};
}
Object? Function(Parameters) _forwardToWorkspace(
Object? Function(Parameters params) Function(_Workspace ws) resolveHandler,
) {
return (Parameters params) async {
final workspaceId = params['workspaceId'].asNum.toInt();
final workspace = _workspaces[workspaceId];
if (workspace == null) {
throw WorkspaceNotFoundException(
'Invalid "workspaceId", no such workspace exists',
data: {'workspaceId': workspaceId},
);
}
return resolveHandler(workspace)(params);
};
}
void _onUnhandledError(Object? e, Object? st) {
print('Unhandled error not forwarded to the client: $e, $st');
}
}
class _Workspace {
final Worker _worker;
final _Session _session;
final int _workspaceId;
final String _workspaceFolder;
final ResourceProvider _rp;
final _languageServers = <int, LanguageServer>{};
final _hotReloadCompilers = <int, HotReloadCompiler>{};
_Workspace(
this._worker,
this._session,
this._workspaceId,
this._workspaceFolder,
this._rp,
);
String _resolvePath(Uri u) {
final path = _rp.pathContext.fromUri(u);
if (_rp.pathContext.isAbsolute(path)) {
return _rp.pathContext.normalize(path);
}
return _rp.pathContext.normalize(
_rp.pathContext.join(_workspaceFolder, path),
);
}
Object? _writeFileFromText(Parameters params) async {
final path = _resolvePath(params['uri'].asUri);
final text = params['text'].asString;
try {
final file = _rp.getFile(path);
file.parent.createRecursively();
file.writeAsStringSync(text);
} on FileSystemException catch (e) {
throw FileWriteConflictException(
e.message,
data: {'resolvedUri': Uri.file(path).toString()},
);
}
return <String, Object?>{};
}
Object? _writeFileFromBytes(Parameters params) async {
final path = _resolvePath(params['uri'].asUri);
final bytes = base64.decode(params['base64'].asString);
try {
final file = _rp.getFile(path);
file.parent.createRecursively();
file.writeAsBytesSync(bytes);
} on FileSystemException catch (e) {
throw FileWriteConflictException(
e.message,
data: {'resolvedUri': Uri.file(path).toString()},
);
}
return <String, Object?>{};
}
Object? _readFileAsText(Parameters params) async {
final path = _resolvePath(params['uri'].asUri);
try {
return {'text': _rp.getFile(path).readAsStringSync()};
} on FileSystemException catch (e) {
throw FileNotFoundException(
e.message,
data: {'resolvedUri': Uri.file(path).toString()},
);
}
}
Object? _readFileAsBytes(Parameters params) async {
final path = _resolvePath(params['uri'].asUri);
try {
return {'base64': base64.encode(_rp.getFile(path).readAsBytesSync())};
} on FileSystemException catch (e) {
throw FileNotFoundException(
e.message,
data: {'resolvedUri': Uri.file(path).toString()},
);
}
}
Object? _deleteFileSystemEntity(Parameters params) async {
final path = _resolvePath(params['uri'].asUri);
try {
_rp.getResource(path).delete();
} on FileSystemException catch (e) {
throw FileDeletionFailedException(
e.message,
data: {'resolvedUri': Uri.file(path).toString()},
);
}
return <String, Object?>{};
}
Object? _stat(Parameters params) async {
final path = _resolvePath(params['uri'].asUri);
final resource = _rp.getResource(path);
if (!resource.exists) {
throw FileNotFoundException(
'File or directory not found',
data: {'resolvedUri': Uri.file(path).toString()},
);
}
if (resource is File) {
return {'type': 'file', 'size': resource.lengthSync};
} else if (resource is Folder) {
return {'type': 'folder'};
} else {
return {'type': 'other'};
}
}
Object? _createFolder(Parameters params) async {
final path = _resolvePath(params['uri'].asUri);
try {
_rp.getFolder(path).createRecursively();
} on FileSystemException catch (e) {
throw FileWriteConflictException(
e.message,
data: {'resolvedUri': Uri.file(path).toString()},
);
}
return <String, Object?>{};
}
Object? _listDirectory(Parameters params) async {
final path = _resolvePath(params['uri'].asUri);
final recursive = params['recursive'].asBoolOr(false);
final ignoreHidden = params['ignoreHidden'].asBoolOr(false);
try {
final folder = _rp.getFolder(path);
if (!folder.exists) {
throw FileSystemException('Directory not found', path);
}
final entries = <Map<String, String>>[];
void traverse(Folder dir) {
for (final child in dir.getChildren()) {
if (ignoreHidden && child.shortName.startsWith('.')) continue;
final relativePath = _rp.pathContext.relative(child.path, from: path);
if (child is File) {
entries.add({'path': relativePath, 'type': 'file'});
} else if (child is Folder) {
entries.add({'path': relativePath, 'type': 'folder'});
if (recursive) {
traverse(child);
}
}
}
}
traverse(folder);
return {'entries': entries};
} on FileSystemException catch (e) {
throw FileNotFoundException(
e.message,
data: {'resolvedUri': Uri.file(path).toString()},
);
}
}
Object? _importTarArchive(Parameters params) async {
final path = _resolvePath(params['uri'].asUri);
final bytes = base64.decode(params['base64'].asString);
await _rp.getFolder(path).extractTarStream(Stream.value(bytes));
return <String, Object?>{};
}
Object? _exportTarArchive(Parameters params) async {
final path = _resolvePath(params['uri'].asUri);
final folder = _rp.getFolder(path);
if (!folder.exists) {
throw FileNotFoundException(
'Directory not found',
data: {'resolvedUri': Uri.file(path).toString()},
);
}
return {
'base64': base64.encode(await collectBytes(folder.createTarStream())),
};
}
String _findPackageConfigFromEntrypoint(String entrypoint) {
var parent = _rp.getFile(entrypoint).parent;
do {
final pkgConfig = parent
.getChildAssumingFolder('.dart_tool')
.getChildAssumingFile('package_config.json');
if (pkgConfig.exists) {
return pkgConfig.path;
}
parent = parent.parent;
} while (!parent.isRoot);
throw PackageConfigNotFoundException(
'Unable to find `.dart_tool/package_config.json` in any '
'parent directory of `$entrypoint`.',
data: {'entrypoint': entrypoint},
);
}
Object? _pub(Parameters params) async {
final path = _resolvePath(params['uri'].asUri);
final command = params['command'].asString;
final args = params['args'].asListOr(const <String>[]);
if (!supportedPubCommands.contains(command)) {
throw RpcException.invalidParams(
'`command` must be one of: ${supportedPubCommands.join(', ')}',
);
}
if (args.any((a) => a is! String)) {
throw RpcException.invalidParams('args must be a list of strings');
}
final (:log) = await pub(
resourceProvider: _rp,
currentWorkingDirectory: path,
command: command,
args: args.whereType<String>().toList(),
config: _worker._config,
);
return {'log': log};
}
Object? _startHotReloadCompiler(Parameters params) async {
var entrypoint = _resolvePath(params['uri'].asUri);
// Test if the file we're compiling exists.
// Otherwise, we get really ugly errors if there is a bootstrap file in play
if (!_rp.getFile(entrypoint).exists) {
throw CompilationFailedException(
'Compilation entrypoint "$entrypoint" not found',
data: {'entrypoint': entrypoint},
);
}
var rp = _rp;
final bootstrapCodeTemplate = _worker._config.bootstrapCode;
if (bootstrapCodeTemplate != null) {
final originalEntrypoint = entrypoint;
entrypoint = '$originalEntrypoint.virtual-bootstrap-wrapper.dart';
final overlay = rp = OverlayResourceProvider(_rp);
overlay.setOverlay(
entrypoint,
content: bootstrapCodeTemplate.replaceAll(
'{{entrypoint}}',
originalEntrypoint,
),
modificationStamp: 0,
);
}
final id = _worker._nextHotReloadCompilerId++;
_hotReloadCompilers[id] = HotReloadCompiler(
resourceProvider: rp,
packageConfig: _findPackageConfigFromEntrypoint(entrypoint),
targetPath: entrypoint,
config: _worker._config,
);
return {'hotReloadCompilerId': id};
}
HotReloadCompiler _getHotReloadCompiler(Parameters params) {
final id = params['hotReloadCompilerId'].asNum.toInt();
final c = _hotReloadCompilers[id];
if (c == null) {
throw HotReloadCompilerNotFoundException(
'HotReloadCompiler not found, check the "hotReloadCompilerId"',
data: {'workspaceId': _workspaceId, 'hotReloadCompilerId': id},
);
}
return c;
}
Object? _hotReloadCompilerCompile(Parameters params) async {
final c = _getHotReloadCompiler(params);
final (:code, :compiledLibraryUris, :log) = await c.compile();
return {
'code': code,
'compiledLibraryUris': compiledLibraryUris,
'log': log,
};
}
Object? _hotReloadCompilerClose(Parameters params) async {
final id = params['hotReloadCompilerId'].asNum.toInt();
final c = _getHotReloadCompiler(params);
_hotReloadCompilers.remove(id);
await c.close();
return <String, Object?>{};
}
Object? _startLanguageServer(Parameters params) async {
final languageServerId = _worker._nextLanguageServerId++;
final ls = _languageServers[languageServerId] = LanguageServer(
resourceProvider: _rp,
config: _worker._config,
);
ls.messages.listen((m) {
_session._rpc.sendNotification('workspace/languageServer/message', {
'workspaceId': _workspaceId,
'languageServerId': languageServerId,
'message': m,
});
});
unawaited(
ls.closed.whenComplete(() {
_session._rpc.sendNotification('workspace/languageServer/exited', {
'workspaceId': _workspaceId,
'languageServerId': languageServerId,
});
}),
);
return {'languageServerId': languageServerId};
}
Object? _languageServerMessage(Parameters params) async {
final languageServerId = params['languageServerId'].asNum.toInt();
final languageServer = _languageServers[languageServerId];
if (languageServer == null) {
throw LanguageServerNotFoundException(
'Language server not found, check the "languageServerId"',
data: {
'workspaceId': _workspaceId,
'languageServerId': languageServerId,
},
);
}
await languageServer.handle(
params['message'].asMap as Map<String, Object?>,
);
return <String, Object?>{};
}
Object? _stopLanguageServer(Parameters params) async {
final languageServerId = params['languageServerId'].asNum.toInt();
final languageServer = _languageServers.remove(languageServerId);
if (languageServer != null) {
await languageServer.close();
}
return <String, Object?>{};
}
Future<void> _deleteWorkspace() async {
try {
await Future.wait([
..._languageServers.values.map((ls) => ls.close()),
..._hotReloadCompilers.values.map((cs) => cs.close()),
]);
} finally {
_rp.getFolder(_workspaceFolder).delete();
}
}
}