| // Copyright (c) 2024, 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:io'; |
| import 'dart:isolate'; |
| |
| import 'cli/formatter_options.dart'; |
| import 'dart_formatter.dart'; |
| import 'debug.dart' as debug; |
| import 'exceptions.dart'; |
| import 'profile.dart'; |
| import 'short/style_fix.dart'; |
| import 'source_code.dart'; |
| |
| class Worker { |
| static Future<Worker> start(int id) async { |
| Profile.begin2('start worker', '$id'); |
| |
| var connection = Completer<(ReceivePort, SendPort)>.sync(); |
| |
| // TODO: Docs. |
| var initPort = RawReceivePort(); |
| initPort.handler = (Object? initialMessage) { |
| var requestPort = initialMessage as SendPort; |
| connection.complete(( |
| ReceivePort.fromRawReceivePort(initPort), |
| requestPort, |
| )); |
| }; |
| |
| await Isolate.spawn( |
| _startRemoteIsolate, (workerId: id, sendPort: initPort.sendPort)); |
| |
| var (receivePort, sendPort) = await connection.future; |
| |
| Profile.end2('start worker', '$id'); |
| return Worker._(id, sendPort, receivePort); |
| } |
| |
| // runs in isolate |
| static void _startRemoteIsolate(({int workerId, SendPort sendPort}) message) { |
| if (debug.traceWorkers) { |
| debug.log('_startRemoteIsolate #${message.workerId}'); |
| } |
| |
| var receivePort = ReceivePort(); |
| message.sendPort.send(receivePort.sendPort); |
| _processRequests(message.workerId, receivePort, message.sendPort); |
| } |
| |
| // runs in isolate |
| static void _processRequests( |
| int workerId, ReceivePort receivePort, SendPort sendPort) { |
| receivePort.listen((dynamic message) { |
| switch (message) { |
| case 'quit': |
| if (debug.traceWorkers) { |
| debug.log('Quitting worker #$workerId...'); |
| } |
| |
| receivePort.close(); |
| |
| case _WorkerFormatRequest request: |
| Profile.end2('send format request', request.filePath); |
| if (debug.traceWorkers) { |
| debug.log('Worker #$workerId received request to format ' |
| '${request.filePath}...'); |
| } |
| |
| try { |
| Profile.begin2('format', request.filePath); |
| var response = _processFormatRequest(request); |
| Profile.end2('format', request.filePath); |
| |
| Profile.begin2('send format response', request.filePath); |
| sendPort.send(response); |
| } catch (error) { |
| sendPort.send(RemoteError(error.toString(), '')); |
| } |
| |
| default: |
| throw ArgumentError('Unknown request $message'); |
| } |
| }); |
| } |
| |
| static WorkerFormatResponse _processFormatRequest( |
| _WorkerFormatRequest request) { |
| var source = SourceCode(request.source, uri: request.filePath); |
| |
| var formatter = DartFormatter( |
| indent: request.indent, |
| pageWidth: request.pageWidth, |
| fixes: request.fixes, |
| experimentFlags: request.experimentFlags); |
| try { |
| var output = formatter.formatSource(source); |
| |
| // TODO: Temporary code to replace the actual formatting logic with some |
| // other CPU-intensive task. Comment out the above line and uncomment this |
| // block to try it. |
| /* |
| // Do some dumb slow computation. |
| int fib(int n) { |
| if (n <= 1) return n; |
| return fib(n - 2) + fib(n - 1); |
| } |
| |
| var x = fib(33); |
| if (x == 3) throw '!'; |
| var output = source; |
| */ |
| |
| return ( |
| path: request.filePath, |
| text: output.text, |
| isChanged: source.text != output.text, |
| selectionStart: output.selectionStart, |
| selectionLength: output.selectionLength |
| ); |
| } on FormatterException catch (err) { |
| // TODO: Probably want all error reporting to happen on main isolate. |
| var color = Platform.operatingSystem != 'windows' && |
| stdioType(stderr) == StdioType.terminal; |
| |
| stderr.writeln(err.message(color: color)); |
| } on UnexpectedOutputException catch (err) { |
| // TODO: Probably want all error reporting to happen on main isolate. |
| // TODO: Should show display path. |
| stderr.writeln( |
| '''Hit a bug in the formatter when formatting ${request.filePath}. |
| $err |
| Please report at github.com/dart-lang/dart_style/issues.'''); |
| } catch (err, stack) { |
| // TODO: Probably want all error reporting to happen on main isolate. |
| // TODO: Should show display path. |
| stderr.writeln( |
| '''Hit a bug in the formatter when formatting ${request.filePath}. |
| Please report at github.com/dart-lang/dart_style/issues. |
| $err |
| $stack'''); |
| } |
| |
| // TODO: Temp. |
| return ( |
| path: request.filePath, |
| text: 'ERROR', |
| isChanged: false, |
| selectionStart: null, |
| selectionLength: null |
| ); |
| |
| // } on FormatterException catch (err) { |
| // // TODO: Color? |
| // return (_ErrorType.formatter, err.message()); |
| // } on UnexpectedOutputException catch (err) { |
| // return (_ErrorType.unexpectedOutput, err.toString()); |
| // } catch (err, stack) { |
| // return (_ErrorType.other, '$err\n$stack'); |
| // } |
| } |
| |
| final int _id; |
| |
| final SendPort _requests; |
| final ReceivePort _responses; |
| |
| /// If this worker is currently formatting, this will be the Completer that |
| /// completes with the eventual result. |
| /// |
| /// Otherwise, if the worker is idle, this is `null`. |
| Completer<WorkerFormatResponse>? _pendingRequest; |
| |
| Worker._(this._id, this._requests, this._responses) { |
| _responses.listen(_handleResponse); |
| } |
| |
| Future<WorkerFormatResponse> requestFormat( |
| FormatterOptions options, String filePath, String source) async { |
| // assert(!_isWorking, 'Worker has already quit.'); |
| assert(_pendingRequest == null, '$this is already formatting.'); |
| |
| if (debug.traceWorkers) { |
| debug.log('$this requestFormat($filePath)'); |
| } |
| |
| var completer = Completer<WorkerFormatResponse>(); |
| _pendingRequest = completer; |
| |
| if (debug.traceWorkers) { |
| debug.log('$this init pendingRequest for $filePath'); |
| } |
| |
| Profile.begin2('send format request', filePath); |
| _requests.send(( |
| indent: options.indent, |
| pageWidth: options.pageWidth, |
| fixes: options.fixes, |
| experimentFlags: options.experimentFlags, |
| filePath: filePath, |
| source: source, |
| )); |
| |
| return await completer.future; |
| } |
| |
| // main isolate |
| void _handleResponse(dynamic message) { |
| if (debug.traceWorkers) { |
| debug.log('$this _handleResponse()'); |
| } |
| |
| switch (message) { |
| case RemoteError error: |
| // print('$this _handleResponse error $error'); |
| _pendingRequest!.completeError(error); |
| _pendingRequest = null; |
| // print('$this _handleResponse() error clear pendingRequest'); |
| |
| case WorkerFormatResponse response: |
| Profile.end2('send format response', response.path); |
| |
| // print('$this _handleResponse response ${response.path}'); |
| _pendingRequest!.complete(response); |
| _pendingRequest = null; |
| // print('$this _handleResponse() clear pendingRequest for ' |
| // '${response.path}'); |
| |
| // case _WorkerErrorResponse(type: _ErrorType.formatter): |
| // _currentRequest!.completeError(error); |
| // _currentRequest = null; |
| |
| default: |
| throw ArgumentError('Unknown response $message'); |
| } |
| } |
| |
| void quit() { |
| if (debug.traceWorkers) { |
| debug.log('Quit $this'); |
| } |
| |
| _requests.send('quit'); |
| _responses.close(); |
| } |
| |
| @override |
| String toString() => 'Worker #$_id'; |
| } |
| |
| typedef _WorkerFormatRequest = ({ |
| int indent, |
| int pageWidth, |
| List<StyleFix> fixes, |
| List<String> experimentFlags, |
| String filePath, |
| String source, |
| }); |
| |
| typedef WorkerFormatResponse = ({ |
| // TODO: Just for debug output. |
| String path, |
| String text, |
| bool isChanged, |
| int? selectionStart, |
| int? selectionLength |
| }); |