1 | | | import 'dart:async'; |
2 | | | import 'dart:isolate'; |
3 | | |
|
4 | | | import '../cancellation_token.dart'; |
5 | | | import '../channel.dart' show Channel, WorkerChannel; |
6 | | | import '../squadron.dart'; |
7 | | | import '../squadron_exception.dart'; |
8 | | | import '../worker_exception.dart'; |
9 | | | import '../worker_request.dart'; |
10 | | | import '../worker_response.dart'; |
11 | | |
|
12 | | | class _SendPort { |
13 | | | /// [SendPort] to communicate with the [Isolate] if the channel is owned by the worker owner. |
14 | | | /// Otherwise, [SendPort] to return values to the client. |
15 | | | SendPort? _sendPort; |
16 | | |
|
17 | | | /// [Channel] serialization in Native world returns the [SendPort]. |
18 | | 2 | dynamic serialize() => _sendPort; |
19 | | |
|
20 | | 1 | void _postRequest(WorkerRequest req) { |
21 | | 1 | final message = req.serialize(); |
22 | | | try { |
23 | | 2 | _sendPort!.send(message); |
24 | | | } catch (ex) { |
25 | | 2 | Squadron.severe('failed to post request $message: error $ex'); |
26 | | | rethrow; |
27 | | | } |
28 | | | } |
29 | | |
|
30 | | 1 | void _postResponse(WorkerResponse res) { |
31 | | 1 | final message = res.serialize(); |
32 | | | try { |
33 | | 2 | _sendPort!.send(message); |
34 | | | } catch (ex) { |
35 | | 2 | Squadron.severe('failed to post response $message: error $ex'); |
36 | | | rethrow; |
37 | | | } |
38 | | | } |
39 | | | } |
40 | | |
|
41 | | | /// [Channel] implementation for the Native world. |
42 | | | class _VmChannel extends _SendPort implements Channel { |
43 | | 1 | _VmChannel._(); |
44 | | |
|
45 | | | /// [Channel] sharing in Native world returns the same instance. |
46 | | 1 | @override |
47 | | | Channel share() => this; |
48 | | |
|
49 | | | /// Sends a termination [WorkerRequest] to the [Isolate] and clears the [SendPort]. |
50 | | 1 | @override |
51 | | | FutureOr close() { |
52 | | 1 | if (_sendPort != null) { |
53 | | 2 | _postRequest(WorkerRequest.stop()); |
54 | | 1 | _sendPort = null; |
55 | | | } |
56 | | | } |
57 | | |
|
58 | | | /// Creates a [web.MessageChannel] and a [WorkerRequest] and sends it to the [web.Worker]. |
59 | | | /// This method expects a single value from the [web.Worker]. |
60 | | 1 | @override |
61 | | | void notifyCancellation(CancellationToken token) { |
62 | | 1 | if (token.cancelled) { |
63 | | 2 | _postRequest(WorkerRequest.cancel(token)); |
64 | | | } |
65 | | | } |
66 | | |
|
67 | | | /// creates a [ReceivePort] and a [WorkerRequest] and sends it to the [Isolate] |
68 | | | /// this method expects a single value from the [Isolate] |
69 | | | @override |
70 | | 1 | Future<T> sendRequest<T>(int command, List args, |
71 | | | {CancellationToken? token}) async { |
72 | | 1 | final receiver = ReceivePort(); |
73 | | 3 | _postRequest(WorkerRequest(receiver.sendPort, command, args, token)); |
74 | | 3 | final res = WorkerResponse.deserialize(await receiver.first); |
75 | | 1 | return res.result as T; |
76 | | | } |
77 | | |
|
78 | | | /// Creates a [ReceivePort] and a [WorkerRequest] and sends it to the [Isolate]. |
79 | | | /// This method expects a stream of values from the [Isolate]. |
80 | | | /// The [Isolate] must send a [WorkerResponse.endOfStream] to close the [Stream]. |
81 | | 1 | @override |
82 | | | Stream<T> sendStreamingRequest<T>(int command, List args, |
83 | | | {CancellationToken? token}) { |
84 | | 1 | final controller = StreamController<T>(); |
85 | | 1 | final receiver = ReceivePort(); |
86 | | 2 | receiver.listen((message) { |
87 | | 1 | final res = WorkerResponse.deserialize(message); |
88 | | 1 | if (res.endOfStream) { |
89 | | 1 | controller.close(); |
90 | | 1 | receiver.close(); |
91 | | 1 | } else if (res.hasError) { |
92 | | 4 | controller.addError(res.error!, res.error!.stackTrace); |
93 | | 1 | controller.close(); |
94 | | 1 | receiver.close(); |
95 | | | } else { |
96 | | 2 | controller.add(res.result); |
97 | | | } |
98 | | | }); |
99 | | 3 | _postRequest(WorkerRequest(receiver.sendPort, command, args, token)); |
100 | | 1 | return controller.stream; |
101 | | | } |
102 | | | } |
103 | | |
|
104 | | | /// [WorkerChannel] implementation for the native world. |
105 | | | class _VmWorkerChannel extends _SendPort implements WorkerChannel { |
106 | | 1 | _VmWorkerChannel._(); |
107 | | |
|
108 | | | /// Sends the [SendPort] to communicate with the [Isolate]. |
109 | | | /// This method must be called by the [Isolate] upon startup. |
110 | | 1 | @override |
111 | | | void connect(Object channelInfo) { |
112 | | 1 | if (channelInfo is ReceivePort) { |
113 | | 2 | reply(channelInfo.sendPort); |
114 | | | } else { |
115 | | 0 | throw WorkerException( |
116 | | 0 | 'invalid channelInfo ${channelInfo.runtimeType}: ReceivePort expected'); |
117 | | | } |
118 | | | } |
119 | | |
|
120 | | | /// Sends a [WorkerResponse] with the specified data to the worker client. |
121 | | | /// This method must be called from the [Isolate] only. |
122 | | 1 | @override |
123 | | 2 | void reply(dynamic data) => _postResponse(WorkerResponse(data)); |
124 | | |
|
125 | | | /// Sends a [WorkerResponse.closeStream] to the worker client. |
126 | | | /// This method must be called from the [Isolate] only. |
127 | | 1 | @override |
128 | | 1 | void closeStream() => _postResponse(WorkerResponse.closeStream); |
129 | | |
|
130 | | | /// Sends the [WorkerException] to the worker client. |
131 | | | /// This method must be called from the [Isolate] only. |
132 | | 1 | @override |
133 | | | void error(SquadronException error) { |
134 | | 2 | Squadron.finer('replying with error: $error'); |
135 | | 2 | _postResponse(WorkerResponse.withError(error)); |
136 | | | } |
137 | | | } |
138 | | |
|
139 | | | /// Stub implementations. |
140 | | |
|
141 | | | int _counter = 0; |
142 | | 1 | String _getId() { |
143 | | 1 | _counter++; |
144 | | 2 | return '${Squadron.id}.$_counter'; |
145 | | | } |
146 | | |
|
147 | | | /// Starts an [Isolate] using the [entryPoint] and sends a start [WorkerRequest] with [startArguments]. |
148 | | | /// The future completes after the [Isolate]'s main program has provided the [SendPort] via [_VmWorkerChannel.connect]. |
149 | | 1 | Future<Channel> openChannel(dynamic entryPoint, List startArguments) async { |
150 | | 1 | final completer = Completer<Channel>(); |
151 | | 1 | final channel = _VmChannel._(); |
152 | | 1 | final receiver = ReceivePort(); |
153 | | 1 | Isolate.spawn( |
154 | | | entryPoint, |
155 | | 3 | WorkerRequest.start(receiver.sendPort, _getId(), startArguments) |
156 | | 1 | .serialize(), |
157 | | | paused: true) |
158 | | 2 | .then((isolate) { |
159 | | 1 | final exitPort = ReceivePort(); |
160 | | 2 | exitPort.listen((message) { |
161 | | 1 | channel.close(); |
162 | | | }); |
163 | | 2 | isolate.addOnExitListener(exitPort.sendPort); |
164 | | 1 | final errorPort = ReceivePort(); |
165 | | 2 | errorPort.listen((message) { |
166 | | 1 | dynamic error = message[0]; |
167 | | 1 | if (error is String) { |
168 | | 1 | error = SquadronException.fromString(error); |
169 | | | } |
170 | | 1 | if (error is! SquadronException) { |
171 | | 0 | error = SquadronException.from( |
172 | | 0 | error: message[0] ?? 'unspecified error', |
173 | | 0 | stackTrace: SquadronException.loadStackTrace(message[1])); |
174 | | | } |
175 | | 1 | if (!completer.isCompleted) { |
176 | | 1 | completer.completeError(error); |
177 | | | } else { |
178 | | 0 | Squadron.warning('unhandled error $error'); |
179 | | | } |
180 | | | }); |
181 | | 2 | isolate.addErrorListener(errorPort.sendPort); |
182 | | 2 | isolate.resume(isolate.pauseCapability!); |
183 | | 3 | Squadron.config('created Isolate #${isolate.hashCode}'); |
184 | | 3 | receiver.first.then((message) { |
185 | | 1 | final response = WorkerResponse.deserialize(message); |
186 | | 1 | if (!completer.isCompleted) { |
187 | | 1 | if (response.hasError) { |
188 | | 1 | isolate.kill(priority: Isolate.immediate); |
189 | | 1 | Squadron.severe( |
190 | | 3 | 'connection to Isolate #${isolate.hashCode} failed: ${response.error}'); |
191 | | 4 | completer.completeError(response.error!, response.error!.stackTrace); |
192 | | | } else { |
193 | | 2 | channel._sendPort = response.result; |
194 | | 3 | Squadron.config('connected to Isolate #${isolate.hashCode}'); |
195 | | 1 | completer.complete(channel); |
196 | | | } |
197 | | | } |
198 | | | }); |
199 | | 1 | }).catchError((error, stackTrace) { |
200 | | 0 | completer.completeError(error, stackTrace); |
201 | | | }); |
202 | | 1 | return completer.future; |
203 | | | } |
204 | | |
|
205 | | | /// Creates a [_VmChannel] from a [SendPort]. |
206 | | 1 | Channel? deserializeChannel(dynamic channelInfo) => |
207 | | 2 | (channelInfo == null) ? null : (_VmChannel._().._sendPort = channelInfo); |
208 | | |
|
209 | | | /// Creates a [_VmWorkerChannel] from a [SendPort]. |
210 | | 1 | WorkerChannel? deserializeWorkerChannel(dynamic channelInfo) => |
211 | | | (channelInfo == null) |
212 | | | ? null |
213 | | 2 | : (_VmWorkerChannel._().._sendPort = channelInfo); |