blob: 80528c85ba6f95dcb5527ca32076057314ae4756 [file]
// Copyright 2019 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 'dart:isolate';
import 'package:path/path.dart' as path;
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf;
import 'package:shelf_static/shelf_static.dart';
import 'package:sse/server/sse_handler.dart';
import 'package:usage/usage_io.dart';
import 'client_manager.dart';
import 'devtools_api.dart';
/// Default [shelf.Handler] for serving DevTools files.
///
/// This serves files out from the build results of running a pub build of the
/// DevTools project.
Future<shelf.Handler> defaultHandler(ClientManager clients) async {
final resourceUri = await Isolate.resolvePackageUri(
Uri(scheme: 'package', path: 'devtools/devtools.dart'));
final packageDir = path.dirname(path.dirname(resourceUri.toFilePath()));
// Default static handler for all non-package requests.
final buildDir = path.join(packageDir, 'build');
final buildHandler = createStaticHandler(
buildDir,
defaultDocument: 'index.html',
);
// The packages folder is renamed in the pub package so this handler serves
// out of the `pack` folder.
final packagesDir = path.join(packageDir, 'build', 'pack');
final packHandler = createStaticHandler(
packagesDir,
defaultDocument: 'index.html',
);
final sseHandler = SseHandler(Uri.parse('/api/sse'))
..connections.rest.listen(clients.acceptClient);
// Make a handler that delegates based on path.
final handler = (shelf.Request request) {
if (request.url.path.startsWith('packages/')) {
// request.change here will strip the `packages` prefix from the path
// so it's relative to packHandler's root.
return packHandler(request.change(path: 'packages'));
}
if (request.url.path.startsWith('api/sse')) {
return sseHandler.handler(request);
}
// The API handler takes all other calls to api/.
if (ServerApi.canHandle(request)) {
return ServerApi.handle(request);
}
return buildHandler(request);
};
return handler;
}
/// The DevTools server API.
///
/// This defines endpoints that serve all requests that come in over api/.
class ServerApi {
/// Determines whether or not [request] is an API call.
static bool canHandle(shelf.Request request) {
return request.url.path.startsWith(apiPrefix);
}
/// Handles all requests.
///
/// To override an API call, pass in a subclass of [ServerApi].
static FutureOr<shelf.Response> handle(
shelf.Request request, [
ServerApi api,
]) {
api ??= ServerApi();
switch (request.url.path) {
// ----- Flutter Tool GA store. -----
case apiGetFlutterGAEnabled:
// Is Analytics collection enabled?
return api.getCompleted(
request,
json.encode(FlutterUsage.doesStoreExist ? _usage.enabled : null),
);
case apiGetFlutterGAClientId:
// Flutter Tool GA clientId - ONLY get Flutter's clientId if enabled is
// true.
return (FlutterUsage.doesStoreExist)
? api.getCompleted(
request,
json.encode(_usage.enabled ? _usage.clientId : null),
)
: api.getCompleted(
request,
json.encode(null),
);
// ----- DevTools GA store. -----
case apiResetDevTools:
_devToolsUsage.reset();
return api.getCompleted(request, json.encode(true));
case apiGetDevToolsFirstRun:
// Has DevTools been run first time? To bring up welcome screen.
return api.getCompleted(
request,
json.encode(_devToolsUsage.isFirstRun),
);
case apiGetDevToolsEnabled:
// Is DevTools Analytics collection enabled?
return api.getCompleted(request, json.encode(_devToolsUsage.enabled));
case apiSetDevToolsEnabled:
// Enable or disable DevTools analytics collection.
final queryParams = request.requestedUri.queryParameters;
if (queryParams.containsKey(devToolsEnabledPropertyName)) {
_devToolsUsage.enabled =
json.decode(queryParams[devToolsEnabledPropertyName]);
}
return api.setCompleted(request, json.encode(_devToolsUsage.enabled));
// ----- DevTools survey store. -----
case apiGetSurveyActionTaken:
// SurveyActionTaken has the survey been acted upon (taken or dismissed)
return api.getCompleted(
request, json.encode(_devToolsUsage.surveyActionTaken));
// TODO(terry): remove the query param logic for this request.
// setSurveyActionTaken should only be called with the value of true, so
// we can remove the extra complexity.
case apiSetSurveyActionTaken:
// Set the SurveyActionTaken.
// Has the survey been taken or dismissed..
final queryParams = request.requestedUri.queryParameters;
if (queryParams.containsKey(surveyActionTakenPropertyName)) {
_devToolsUsage.surveyActionTaken =
json.decode(queryParams[surveyActionTakenPropertyName]);
}
return api.setCompleted(
request,
json.encode(_devToolsUsage.surveyActionTaken),
);
case apiGetSurveyShownCount:
// SurveyShownCount how many times have we asked to take survey.
return api.getCompleted(
request,
json.encode(_devToolsUsage.surveyShownCount),
);
case apiIncrementSurveyShownCount:
// Increment the SurveyShownCount, we've asked about the survey.
_devToolsUsage.incrementSurveyShownCount();
return api.getCompleted(
request,
json.encode(_devToolsUsage.surveyShownCount),
);
default:
return api.notImplemented(request);
}
}
// Accessing Flutter usage file e.g., ~/.flutter.
// NOTE: Only access the file if it exists otherwise Flutter Tool hasn't yet
// been run.
static final FlutterUsage _usage =
FlutterUsage.doesStoreExist ? FlutterUsage() : null;
// Accessing DevTools usage file e.g., ~/.devtools
static final DevToolsUsage _devToolsUsage = DevToolsUsage();
/// Logs a page view in the DevTools server.
///
/// In the open-source version of DevTools, Google Analytics handles this
/// without any need to involve the server.
FutureOr<shelf.Response> logScreenView(shelf.Request request) =>
notImplemented(request);
/// Return the value of the property.
FutureOr<shelf.Response> getCompleted(shelf.Request request, String value) =>
shelf.Response.ok('$value');
/// Return the value of the property after the property value has been set.
FutureOr<shelf.Response> setCompleted(shelf.Request request, String value) =>
shelf.Response.ok('$value');
/// A [shelf.Response] for API calls that have not been implemented in this
/// server.
///
/// This is a no-op 204 No Content response because returning 404 Not Found
/// creates unnecessary noise in the console.
FutureOr<shelf.Response> notImplemented(shelf.Request request) =>
shelf.Response(204);
}
/// Access the file '~/.flutter'.
class FlutterUsage {
/// Create a new Usage instance; [versionOverride] and [configDirOverride] are
/// used for testing.
FlutterUsage({
String settingsName = 'flutter',
String versionOverride,
String configDirOverride,
}) {
_analytics = AnalyticsIO('', settingsName, '', documentDirectory: null);
}
Analytics _analytics;
/// Does the .flutter store exist?
static bool get doesStoreExist {
final flutterStore = File('${DevToolsUsage.userHomeDir()}/.flutter');
return flutterStore.existsSync();
}
bool get isFirstRun => _analytics.firstRun;
bool get enabled => _analytics.enabled;
set enabled(bool value) => _analytics.enabled = value;
String get clientId => _analytics.clientId;
}
// Access the DevTools on disk store (~/.devtools).
class DevToolsUsage {
/// Create a new Usage instance; [versionOverride] and [configDirOverride] are
/// used for testing.
DevToolsUsage({
String settingsName = 'devtools',
String versionOverride,
String configDirOverride,
}) {
properties = IOPersistentProperties(
settingsName,
documentDirPath: userHomeDir(),
);
}
static String userHomeDir() {
final String envKey =
Platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
final String value = Platform.environment[envKey];
return value == null ? '.' : value;
}
IOPersistentProperties properties;
void reset() {
properties.remove('firstRun');
properties['enabled'] = false;
properties['surveyShownCount'] = 0;
properties['surveyActionTaken'] = false;
}
bool get isFirstRun {
properties['firstRun'] = properties['firstRun'] == null;
return properties['firstRun'];
}
bool get enabled {
if (properties['enabled'] == null) {
properties['enabled'] = false;
}
return properties['enabled'];
}
set enabled(bool value) {
properties['enabled'] = value;
return properties['enabled'];
}
int get surveyShownCount {
if (properties['surveyShownCount'] == null) {
properties['surveyShownCount'] = 0;
}
return properties['surveyShownCount'];
}
void incrementSurveyShownCount() {
surveyShownCount; // Ensure surveyShownCount has been initialized.
properties['surveyShownCount'] += 1;
}
bool get surveyActionTaken => properties['surveyActionTaken'] == true;
set surveyActionTaken(bool value) {
properties['surveyActionTaken'] = value;
}
}
abstract class PersistentProperties {
PersistentProperties(this.name);
final String name;
dynamic operator [](String key);
void operator []=(String key, dynamic value);
/// Re-read settings from the backing store.
///
/// May be a no-op on some platforms.
void syncSettings();
}
const JsonEncoder _jsonEncoder = JsonEncoder.withIndent(' ');
class IOPersistentProperties extends PersistentProperties {
IOPersistentProperties(
String name, {
String documentDirPath,
}) : super(name) {
final String fileName = '.${name.replaceAll(' ', '_')}';
documentDirPath ??= DevToolsUsage.userHomeDir();
_file = File(path.join(documentDirPath, fileName));
if (!_file.existsSync()) {
_file.createSync();
}
syncSettings();
}
IOPersistentProperties.fromFile(File file) : super(path.basename(file.path)) {
_file = file;
if (!_file.existsSync()) {
_file.createSync();
}
syncSettings();
}
File _file;
Map _map;
@override
dynamic operator [](String key) => _map[key];
@override
void operator []=(String key, dynamic value) {
if (value == null && !_map.containsKey(key)) return;
if (_map[key] == value) return;
if (value == null) {
_map.remove(key);
} else {
_map[key] = value;
}
try {
_file.writeAsStringSync(_jsonEncoder.convert(_map) + '\n');
} catch (_) {}
}
@override
void syncSettings() {
try {
String contents = _file.readAsStringSync();
if (contents.isEmpty) contents = '{}';
_map = jsonDecode(contents);
} catch (_) {
_map = {};
}
}
void remove(String propertyName) {
_map.remove(propertyName);
}
}