OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library pub.barback.web_socket_api; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:io'; | |
9 | |
10 import 'package:http_parser/http_parser.dart'; | |
11 import 'package:path/path.dart' as path; | |
12 import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc; | |
13 | |
14 import '../exit_codes.dart' as exit_codes; | |
15 import '../io.dart'; | |
16 import '../log.dart' as log; | |
17 import '../utils.dart'; | |
18 import 'asset_environment.dart'; | |
19 | |
20 /// Implements the [WebSocket] API for communicating with a running pub serve | |
21 /// process, mainly for use by the Editor. | |
22 /// | |
23 /// This is a [JSON-RPC 2.0](http://www.jsonrpc.org/specification) server. Its | |
24 /// methods are described in the method-level documentation below. | |
25 class WebSocketApi { | |
26 final AssetEnvironment _environment; | |
27 final json_rpc.Server _server; | |
28 | |
29 /// Whether the application should exit when this connection closes. | |
30 bool _exitOnClose = false; | |
31 | |
32 WebSocketApi(CompatibleWebSocket socket, this._environment) | |
33 : _server = new json_rpc.Server(socket) { | |
34 _server.registerMethod("urlToAssetId", _urlToAssetId); | |
35 _server.registerMethod("pathToUrls", _pathToUrls); | |
36 _server.registerMethod("serveDirectory", _serveDirectory); | |
37 _server.registerMethod("unserveDirectory", _unserveDirectory); | |
38 | |
39 /// Tells the server to exit as soon as this WebSocket connection is closed. | |
40 /// | |
41 /// This takes no arguments and returns no results. It can safely be called | |
42 /// as a JSON-RPC notification. | |
43 _server.registerMethod("exitOnClose", () { | |
44 _exitOnClose = true; | |
45 }); | |
46 } | |
47 | |
48 /// Listens on the socket. | |
49 /// | |
50 /// Returns a future that completes when the socket has closed. It will | |
51 /// complete with an error if the socket had an error, otherwise it will | |
52 /// complete to `null`. | |
53 Future listen() { | |
54 return _server.listen().then((_) { | |
55 if (!_exitOnClose) return; | |
56 log.message("WebSocket connection closed, terminating."); | |
57 flushThenExit(exit_codes.SUCCESS); | |
58 }); | |
59 } | |
60 | |
61 /// Given a URL to an asset that is served by pub, returns the ID of the | |
62 /// asset that would be accessed by that URL. | |
63 /// | |
64 /// The method name is "urlToAssetId" and it takes a "url" parameter for the | |
65 /// URL being mapped: | |
66 /// | |
67 /// "params": { | |
68 /// "url": "http://localhost:8080/index.html" | |
69 /// } | |
70 /// | |
71 /// If successful, it returns a map containing the asset ID's package and | |
72 /// path: | |
73 /// | |
74 /// "result": { | |
75 /// "package": "myapp", | |
76 /// "path": "web/index.html" | |
77 /// } | |
78 /// | |
79 /// The "path" key in the result is a URL path that's relative to the root | |
80 /// directory of the package identified by "package". The location of this | |
81 /// package may vary depending on which source it was installed from. | |
82 /// | |
83 /// An optional "line" key may be provided whose value must be an integer. If | |
84 /// given, the result will also include a "line" key that maps the line in | |
85 /// the served final file back to the corresponding source line in the asset | |
86 /// that was used to generate that file. | |
87 /// | |
88 /// Examples (where "myapp" is the root package and pub serve is being run | |
89 /// normally with "web" bound to port 8080 and "test" to 8081): | |
90 /// | |
91 /// http://localhost:8080/index.html -> myapp|web/index.html | |
92 /// http://localhost:8081/sub/main.dart -> myapp|test/sub/main.dart | |
93 /// | |
94 /// If the URL is not a domain being served by pub, this returns an error: | |
95 /// | |
96 /// http://localhost:1234/index.html -> NOT_SERVED error | |
97 /// | |
98 /// This does *not* currently support the implicit index.html behavior that | |
99 /// pub serve provides for user-friendliness: | |
100 /// | |
101 /// http://localhost:1234 -> NOT_SERVED error | |
102 /// | |
103 /// This does *not* currently check to ensure the asset actually exists. It | |
104 /// only maps what the corresponding asset *should* be for that URL. | |
105 Future<Map> _urlToAssetId(json_rpc.Parameters params) { | |
106 var url = params["url"].asUri; | |
107 | |
108 // If a line number was given, map it to the output line. | |
109 var line = params["line"].asIntOr(null); | |
110 | |
111 return _environment.getAssetIdForUrl(url).then((id) { | |
112 if (id == null) { | |
113 throw new json_rpc.RpcException( | |
114 _Error.NOT_SERVED, | |
115 '"${url.host}:${url.port}" is not being served by pub.'); | |
116 } | |
117 | |
118 // TODO(rnystrom): When this is hooked up to actually talk to barback to | |
119 // see if assets exist, consider supporting implicit index.html at that | |
120 // point. | |
121 | |
122 var result = { | |
123 "package": id.package, | |
124 "path": id.path | |
125 }; | |
126 | |
127 // Map the line. | |
128 // TODO(rnystrom): Right now, source maps are not supported and it just | |
129 // passes through the original line. This lets the editor start using | |
130 // this API before we've fully implemented it. See #12339 and #16061. | |
131 if (line != null) result["line"] = line; | |
132 | |
133 return result; | |
134 }); | |
135 } | |
136 | |
137 /// Given a path on the filesystem, returns the URLs served by pub that can be | |
138 /// used to access asset found at that path. | |
139 /// | |
140 /// The method name is "pathToUrls" and it takes a "path" key (a native OS | |
141 /// path which may be absolute or relative to the root directory of the | |
142 /// entrypoint package) for the path being mapped: | |
143 /// | |
144 /// "params": { | |
145 /// "path": "web/index.html" | |
146 /// } | |
147 /// | |
148 /// If successful, it returns a map containing the list of URLs that can be | |
149 /// used to access that asset. | |
150 /// | |
151 /// "result": { | |
152 /// "urls": ["http://localhost:8080/index.html"] | |
153 /// } | |
154 /// | |
155 /// The "path" key may refer to a path in another package, either by referring | |
156 /// to its location within the top-level "packages" directory or by referring | |
157 /// to its location on disk. Only the "lib" directory is visible in other | |
158 /// packages: | |
159 /// | |
160 /// "params": { | |
161 /// "path": "packages/http/http.dart" | |
162 /// } | |
163 /// | |
164 /// Assets in the "lib" directory will usually have one URL for each server: | |
165 /// | |
166 /// "result": { | |
167 /// "urls": [ | |
168 /// "http://localhost:8080/packages/http/http.dart", | |
169 /// "http://localhost:8081/packages/http/http.dart" | |
170 /// ] | |
171 /// } | |
172 /// | |
173 /// An optional "line" key may be provided whose value must be an integer. If | |
174 /// given, the result will also include a "line" key that maps the line in | |
175 /// the source file to the corresponding output line in the resulting asset | |
176 /// served at the URL. | |
177 /// | |
178 /// Examples (where "myapp" is the root package and pub serve is being run | |
179 /// normally with "web" bound to port 8080 and "test" to 8081): | |
180 /// | |
181 /// web/index.html -> http://localhost:8080/index.html | |
182 /// test/sub/main.dart -> http://localhost:8081/sub/main.dart | |
183 /// | |
184 /// If the asset is not in a directory being served by pub, returns an error: | |
185 /// | |
186 /// example/index.html -> NOT_SERVED error | |
187 Future<Map> _pathToUrls(json_rpc.Parameters params) { | |
188 var assetPath = params["path"].asString; | |
189 var line = params["line"].asIntOr(null); | |
190 | |
191 return _environment.getUrlsForAssetPath(assetPath).then((urls) { | |
192 if (urls.isEmpty) { | |
193 throw new json_rpc.RpcException( | |
194 _Error.NOT_SERVED, | |
195 'Asset path "$assetPath" is not currently being served.'); | |
196 } | |
197 | |
198 var result = { | |
199 "urls": urls.map((url) => url.toString()).toList() | |
200 }; | |
201 | |
202 // Map the line. | |
203 // TODO(rnystrom): Right now, source maps are not supported and it just | |
204 // passes through the original line. This lets the editor start using | |
205 // this API before we've fully implemented it. See #12339 and #16061. | |
206 if (line != null) result["line"] = line; | |
207 | |
208 return result; | |
209 }); | |
210 } | |
211 | |
212 /// Given a relative directory path within the entrypoint package, binds a | |
213 /// new port to serve from that path and returns its URL. | |
214 /// | |
215 /// The method name is "serveDirectory" and it takes a "path" key (a native | |
216 /// OS path relative to the root of the entrypoint package) for the directory | |
217 /// being served: | |
218 /// | |
219 /// "params": { | |
220 /// "path": "example/awesome" | |
221 /// } | |
222 /// | |
223 /// If successful, it returns a map containing the URL that can be used to | |
224 /// access the directory. | |
225 /// | |
226 /// "result": { | |
227 /// "url": "http://localhost:8083" | |
228 /// } | |
229 /// | |
230 /// If the directory is already being served, returns the previous URL. | |
231 Future<Map> _serveDirectory(json_rpc.Parameters params) { | |
232 var rootDirectory = _validateRelativePath(params, "path"); | |
233 return _environment.serveDirectory(rootDirectory).then((server) { | |
234 return { | |
235 "url": server.url.toString() | |
236 }; | |
237 }).catchError((error) { | |
238 if (error is! OverlappingSourceDirectoryException) throw error; | |
239 | |
240 var dir = pluralize( | |
241 "directory", | |
242 error.overlappingDirectories.length, | |
243 plural: "directories"); | |
244 var overlapping = | |
245 toSentence(error.overlappingDirectories.map((dir) => '"$dir"')); | |
246 print("data: ${error.overlappingDirectories}"); | |
247 throw new json_rpc.RpcException( | |
248 _Error.OVERLAPPING, | |
249 'Path "$rootDirectory" overlaps already served $dir $overlapping.', | |
250 data: { | |
251 "directories": error.overlappingDirectories | |
252 }); | |
253 }); | |
254 } | |
255 | |
256 /// Given a relative directory path within the entrypoint package, unbinds | |
257 /// the server previously bound to that directory and returns its (now | |
258 /// unreachable) URL. | |
259 /// | |
260 /// The method name is "unserveDirectory" and it takes a "path" key (a | |
261 /// native OS path relative to the root of the entrypoint package) for the | |
262 /// directory being unserved: | |
263 /// | |
264 /// "params": { | |
265 /// "path": "example/awesome" | |
266 /// } | |
267 /// | |
268 /// If successful, it returns a map containing the URL that used to be used | |
269 /// to access the directory. | |
270 /// | |
271 /// "result": { | |
272 /// "url": "http://localhost:8083" | |
273 /// } | |
274 /// | |
275 /// If no server is bound to that directory, it returns a `NOT_SERVED` error. | |
276 Future<Map> _unserveDirectory(json_rpc.Parameters params) { | |
277 var rootDirectory = _validateRelativePath(params, "path"); | |
278 return _environment.unserveDirectory(rootDirectory).then((url) { | |
279 if (url == null) { | |
280 throw new json_rpc.RpcException( | |
281 _Error.NOT_SERVED, | |
282 'Directory "$rootDirectory" is not bound to a server.'); | |
283 } | |
284 | |
285 return { | |
286 "url": url.toString() | |
287 }; | |
288 }); | |
289 } | |
290 | |
291 /// Validates that [command] has a field named [key] whose value is a string | |
292 /// containing a relative path that doesn't reach out of the entrypoint | |
293 /// package's root directory. | |
294 /// | |
295 /// Returns the path if found, or throws a [_WebSocketException] if | |
296 /// validation failed. | |
297 String _validateRelativePath(json_rpc.Parameters params, String key) { | |
298 var pathString = params[key].asString; | |
299 | |
300 if (!path.isRelative(pathString)) { | |
301 throw new json_rpc.RpcException.invalidParams( | |
302 '"$key" must be a relative path. Got "$pathString".'); | |
303 } | |
304 | |
305 if (!path.isWithin(".", pathString)) { | |
306 throw new json_rpc.RpcException.invalidParams( | |
307 '"$key" cannot reach out of its containing directory. ' 'Got "$pathStr
ing".'); | |
308 } | |
309 | |
310 return pathString; | |
311 } | |
312 } | |
313 | |
314 /// The pub-specific JSON RPC error codes. | |
315 class _Error { | |
316 /// The specified directory is not being served. | |
317 static const NOT_SERVED = 1; | |
318 | |
319 /// The specified directory overlaps one or more ones already being served. | |
320 static const OVERLAPPING = 2; | |
321 } | |
OLD | NEW |