mirror of
https://github.com/dart-lang/sdk
synced 2024-11-02 10:49:00 +00:00
[dds] Serve DevTools index page for extension-less requests to support UrlPathStrategy
Change-Id: I780e16b391dda6159c99b4844f6663dad02a98af Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/239082 Reviewed-by: Ben Konyi <bkonyi@google.com> Reviewed-by: Kenzie Davisson <kenzieschmoll@google.com> Commit-Queue: Ben Konyi <bkonyi@google.com>
This commit is contained in:
parent
731ef4f5c9
commit
db0d9b1852
3 changed files with 217 additions and 8 deletions
|
@ -3,8 +3,11 @@
|
|||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:devtools_shared/devtools_server.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_static/shelf_static.dart';
|
||||
import 'package:sse/server/sse_handler.dart';
|
||||
|
@ -29,21 +32,51 @@ FutureOr<Handler> defaultHandler({
|
|||
ClientManager? clientManager,
|
||||
Handler? notFoundHandler,
|
||||
}) {
|
||||
// Serves the web assets for DevTools.
|
||||
final devtoolsAssetHandler = createStaticHandler(
|
||||
// When served through DDS, the app root is /devtools/.
|
||||
// This variable is used in base href and must start and end with `/`.
|
||||
var appRoot = dds != null ? '/devtools/' : '/';
|
||||
if (dds?.authCodesEnabled ?? false) {
|
||||
appRoot = '/${dds!.authCode}$appRoot';
|
||||
}
|
||||
|
||||
const defaultDocument = 'index.html';
|
||||
final indexFile = File(path.join(buildDir, defaultDocument));
|
||||
|
||||
// Serves the static web assets for DevTools.
|
||||
final devtoolsStaticAssetHandler = createStaticHandler(
|
||||
buildDir,
|
||||
defaultDocument: 'index.html',
|
||||
defaultDocument: defaultDocument,
|
||||
);
|
||||
|
||||
/// A wrapper around [devtoolsStaticAssetHandler] that handles serving
|
||||
/// index.html up for / and non-file requests like /memory, /inspector, etc.
|
||||
/// with the correct base href for the DevTools root.
|
||||
final devtoolsAssetHandler = (Request request) {
|
||||
// To avoid hard-coding a set of page names here (or needing access to one
|
||||
// from DevTools, assume any single-segment path with no extension is a
|
||||
// DevTools page that needs to serve up index.html).
|
||||
final pathSegments = request.url.pathSegments;
|
||||
final isValidRootPage = pathSegments.isEmpty ||
|
||||
(pathSegments.length == 1 && !pathSegments[0].contains('.'));
|
||||
if (isValidRootPage) {
|
||||
return _serveStaticFile(
|
||||
request,
|
||||
indexFile,
|
||||
'text/html',
|
||||
baseHref: appRoot,
|
||||
);
|
||||
}
|
||||
|
||||
return devtoolsStaticAssetHandler(request);
|
||||
};
|
||||
|
||||
// Support DevTools client-server interface via SSE.
|
||||
// Note: the handler path needs to match the full *original* path, not the
|
||||
// current request URL (we remove '/devtools' in the initial router but we
|
||||
// need to include it here).
|
||||
final devToolsSseHandlerPath = dds != null ? '/devtools/api/sse' : '/api/sse';
|
||||
final devToolsSseHandlerPath = '${appRoot}api/sse';
|
||||
final devToolsApiHandler = SseHandler(
|
||||
(dds?.authCodesEnabled ?? false)
|
||||
? Uri.parse('/${dds!.authCode}$devToolsSseHandlerPath')
|
||||
: Uri.parse(devToolsSseHandlerPath),
|
||||
Uri.parse(devToolsSseHandlerPath),
|
||||
keepAlive: sseKeepAlive,
|
||||
);
|
||||
|
||||
|
@ -78,7 +111,7 @@ FutureOr<Handler> defaultHandler({
|
|||
return ServerApi.handle(request);
|
||||
};
|
||||
|
||||
return (request) {
|
||||
return (Request request) {
|
||||
if (notFoundHandler != null) {
|
||||
final pathSegments = request.url.pathSegments;
|
||||
if (pathSegments.isEmpty || pathSegments.first != 'devtools') {
|
||||
|
@ -90,3 +123,30 @@ FutureOr<Handler> defaultHandler({
|
|||
return devtoolsHandler(request);
|
||||
};
|
||||
}
|
||||
|
||||
/// Serves [file] for all requests.
|
||||
///
|
||||
/// If [baseHref] is provided, any existing `<base href="">` tag will be
|
||||
/// rewritten with this path.
|
||||
Future<Response> _serveStaticFile(
|
||||
Request request,
|
||||
File file,
|
||||
String contentType, {
|
||||
String? baseHref,
|
||||
}) async {
|
||||
final headers = {HttpHeaders.contentTypeHeader: contentType};
|
||||
var contents = file.readAsStringSync();
|
||||
|
||||
if (baseHref != null) {
|
||||
assert(baseHref.startsWith('/'));
|
||||
assert(baseHref.endsWith('/'));
|
||||
// Replace the base href to match where the app is being served from.
|
||||
final baseHrefPattern = RegExp(r'<base href="\/"\s?\/?>');
|
||||
contents = contents.replaceFirst(
|
||||
baseHrefPattern,
|
||||
'<base href="${htmlEscape.convert(baseHref)}">',
|
||||
);
|
||||
}
|
||||
|
||||
return Response.ok(contents, headers: headers);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2022 The Chromium Authors. 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:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'utils/server_driver.dart';
|
||||
|
||||
late final DevToolsServerTestController testController;
|
||||
|
||||
void main() {
|
||||
const testScriptContents =
|
||||
'Future<void> main() => Future.delayed(const Duration(minutes: 10));';
|
||||
final tempDir = Directory.systemTemp.createTempSync('devtools_server.');
|
||||
final devToolsBannerRegex =
|
||||
RegExp(r'DevTools[\w\s]+at: (https?:.*\/devtools\/)');
|
||||
|
||||
test('serves index.html contents for /token/devtools/inspector', () async {
|
||||
final testFile = File(path.join(tempDir.path, 'foo.dart'));
|
||||
testFile.writeAsStringSync(testScriptContents);
|
||||
|
||||
final proc = await Process.start(
|
||||
Platform.resolvedExecutable, ['--observe=0', testFile.path]);
|
||||
try {
|
||||
final completer = Completer<String>();
|
||||
proc.stderr
|
||||
.transform(utf8.decoder)
|
||||
.transform(LineSplitter())
|
||||
.listen(print);
|
||||
proc.stdout.transform(utf8.decoder).transform(LineSplitter()).listen(
|
||||
(String line) {
|
||||
print(line);
|
||||
final match = devToolsBannerRegex.firstMatch(line);
|
||||
if (match != null) {
|
||||
completer.complete(match.group(1));
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(
|
||||
'Process ended without emitting DevTools banner');
|
||||
}
|
||||
},
|
||||
onError: (e) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final devToolsUrl = Uri.parse(await completer.future);
|
||||
final httpClient = HttpClient();
|
||||
late HttpClientResponse resp;
|
||||
try {
|
||||
final req = await httpClient.get(
|
||||
devToolsUrl.host, devToolsUrl.port, '${devToolsUrl.path}inspector');
|
||||
resp = await req.close();
|
||||
expect(resp.statusCode, 200);
|
||||
final bodyContent = await resp.transform(utf8.decoder).join();
|
||||
expect(bodyContent, contains('Dart DevTools'));
|
||||
final expectedBaseHref = htmlEscape.convert(devToolsUrl.path);
|
||||
expect(bodyContent, contains('<base href="$expectedBaseHref">'));
|
||||
} finally {
|
||||
httpClient.close();
|
||||
}
|
||||
} finally {
|
||||
proc.kill();
|
||||
}
|
||||
// TODO(dantup): Unskip this test once DevTools has rolled into
|
||||
// the SDK so that contains the (newly-added) base href tag.
|
||||
}, timeout: const Timeout.factor(10), skip: true);
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2022 The Chromium Authors. 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 'dart:io';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'utils/server_driver.dart';
|
||||
|
||||
late final DevToolsServerTestController testController;
|
||||
|
||||
void main() {
|
||||
testController = DevToolsServerTestController();
|
||||
|
||||
setUp(() async {
|
||||
await testController.setUp();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await testController.tearDown();
|
||||
});
|
||||
|
||||
test('serves index.html contents for /inspector', () async {
|
||||
final server = await DevToolsServerDriver.create();
|
||||
final httpClient = HttpClient();
|
||||
late HttpClientResponse resp;
|
||||
try {
|
||||
final startedEvent = (await server.stdout.firstWhere(
|
||||
(map) => map!['event'] == 'server.started',
|
||||
))!;
|
||||
final host = startedEvent['params']['host'];
|
||||
final port = startedEvent['params']['port'];
|
||||
|
||||
final req = await httpClient.get(host, port, '/inspector');
|
||||
resp = await req.close();
|
||||
expect(resp.statusCode, 200);
|
||||
final bodyContent = await resp.transform(utf8.decoder).join();
|
||||
expect(bodyContent, contains('Dart DevTools'));
|
||||
final expectedBaseHref = htmlEscape.convert('/');
|
||||
expect(bodyContent, contains('<base href="$expectedBaseHref">'));
|
||||
} finally {
|
||||
httpClient.close();
|
||||
server.kill();
|
||||
}
|
||||
// TODO(dantup): Unskip this test once DevTools has rolled into
|
||||
// the SDK so that contains the (newly-added) base href tag.
|
||||
}, timeout: const Timeout.factor(10), skip: true);
|
||||
|
||||
test('serves 404 contents for requests that are not pages', () async {
|
||||
final server = await DevToolsServerDriver.create();
|
||||
final httpClient = HttpClient();
|
||||
late HttpClientResponse resp;
|
||||
try {
|
||||
final startedEvent = (await server.stdout.firstWhere(
|
||||
(map) => map!['event'] == 'server.started',
|
||||
))!;
|
||||
final host = startedEvent['params']['host'];
|
||||
final port = startedEvent['params']['port'];
|
||||
|
||||
// The index page is only served up for extension-less requests.
|
||||
final req = await httpClient.get(host, port, '/inspector.html');
|
||||
resp = await req.close();
|
||||
expect(resp.statusCode, 404);
|
||||
} finally {
|
||||
httpClient.close();
|
||||
await resp.drain();
|
||||
server.kill();
|
||||
}
|
||||
}, timeout: const Timeout.factor(10));
|
||||
}
|
Loading…
Reference in a new issue