use code from package:devtools_shared

Change-Id: Ie16400c7c216fb165fb5c65a0150e37b5512ef69
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/210161
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Devon Carew <devoncarew@google.com>
This commit is contained in:
Devon Carew 2021-08-13 21:54:01 +00:00 committed by commit-bot@chromium.org
parent 16ff4aec0e
commit 3365b77ac2
6 changed files with 2 additions and 541 deletions

View file

@ -4,12 +4,11 @@
import 'dart:async';
import 'package:devtools_shared/devtools_server.dart';
import 'package:json_rpc_2/src/server.dart' as json_rpc;
import 'package:sse/src/server/sse_handler.dart';
import 'package:stream_channel/stream_channel.dart';
import 'server_api.dart';
class LoggingMiddlewareSink<S> implements StreamSink<S> {
LoggingMiddlewareSink(this.sink);

View file

@ -5,13 +5,13 @@
import 'dart:async';
import 'package:dds/src/constants.dart';
import 'package:devtools_shared/devtools_server.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_static/shelf_static.dart';
import 'package:sse/server/sse_handler.dart';
import '../dds_impl.dart';
import 'devtools_client.dart';
import 'server_api.dart';
/// Returns a [Handler] which handles serving DevTools and the DevTools server
/// API under $DDS_URI/devtools/.

View file

@ -1,82 +0,0 @@
// Copyright 2021 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.
// TODO(bkonyi): remove once package:devtools_server_api is available
// See https://github.com/flutter/devtools/issues/2958.
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'usage.dart';
class LocalFileSystem {
static String _userHomeDir() {
final String envKey =
Platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
final String? value = Platform.environment[envKey];
return value == null ? '.' : value;
}
/// Returns the path to the DevTools storage directory.
static String devToolsDir() {
return path.join(_userHomeDir(), '.flutter-devtools');
}
/// Moves the .devtools file to ~/.flutter-devtools/.devtools if the .devtools file
/// exists in the user's home directory.
static void maybeMoveLegacyDevToolsStore() {
final file = File(path.join(_userHomeDir(), DevToolsUsage.storeName));
if (file.existsSync()) {
ensureDevToolsDirectory();
file.copySync(path.join(devToolsDir(), DevToolsUsage.storeName));
file.deleteSync();
}
}
/// Creates the ~/.flutter-devtools directory if it does not already exist.
static void ensureDevToolsDirectory() {
Directory('${LocalFileSystem.devToolsDir()}').createSync();
}
/// Returns a DevTools file from the given path.
///
/// Only files within ~/.flutter-devtools/ can be accessed.
static File? devToolsFileFromPath(String pathFromDevToolsDir) {
if (pathFromDevToolsDir.contains('..')) {
// The passed in path should not be able to walk up the directory tree
// outside of the ~/.flutter-devtools/ directory.
return null;
}
ensureDevToolsDirectory();
final file = File(path.join(devToolsDir(), pathFromDevToolsDir));
if (!file.existsSync()) {
return null;
}
return file;
}
/// Returns a DevTools file from the given path as encoded json.
///
/// Only files within ~/.flutter-devtools/ can be accessed.
static String? devToolsFileAsJson(String pathFromDevToolsDir) {
final file = devToolsFileFromPath(pathFromDevToolsDir);
if (file == null) return null;
final fileName = path.basename(file.path);
if (!fileName.endsWith('.json')) return null;
final content = file.readAsStringSync();
final json = jsonDecode(content);
json['lastModifiedTime'] = file.lastModifiedSync().toString();
return jsonEncode(json);
}
/// Whether the flutter store file exists.
static bool flutterStoreExists() {
final flutterStore = File('${_userHomeDir()}/.flutter');
return flutterStore.existsSync();
}
}

View file

@ -1,228 +0,0 @@
// Copyright 2021 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.
// TODO(bkonyi): remove once package:devtools_server_api is available
// See https://github.com/flutter/devtools/issues/2958.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:devtools_shared/devtools_shared.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'file_system.dart';
import 'usage.dart';
/// The DevTools server API.
///
/// This defines endpoints that serve all requests that come in over api/.
class ServerApi {
static const errorNoActiveSurvey = 'ERROR: setActiveSurvey not called.';
/// 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 apiSetActiveSurvey:
// Assume failure.
bool result = false;
// Set the active survey used to store subsequent apiGetSurveyActionTaken,
// apiSetSurveyActionTaken, apiGetSurveyShownCount, and
// apiIncrementSurveyShownCount calls.
final queryParams = request.requestedUri.queryParameters;
if (queryParams.keys.length == 1 &&
queryParams.containsKey(activeSurveyName)) {
final String theSurveyName = queryParams[activeSurveyName]!;
// Set the current activeSurvey.
_devToolsUsage.activeSurvey = theSurveyName;
result = true;
}
return api.getCompleted(request, json.encode(result));
case apiGetSurveyActionTaken:
// Request setActiveSurvey has not been requested.
if (_devToolsUsage.activeSurvey == null) {
return api.badRequest('$errorNoActiveSurvey '
'- $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:
// Request setActiveSurvey has not been requested.
if (_devToolsUsage.activeSurvey == null) {
return api.badRequest('$errorNoActiveSurvey '
'- $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:
// Request setActiveSurvey has not been requested.
if (_devToolsUsage.activeSurvey == null) {
return api.badRequest('$errorNoActiveSurvey '
'- $apiGetSurveyShownCount');
}
// SurveyShownCount how many times have we asked to take survey.
return api.getCompleted(
request,
json.encode(_devToolsUsage.surveyShownCount),
);
case apiIncrementSurveyShownCount:
// Request setActiveSurvey has not been requested.
if (_devToolsUsage.activeSurvey == null) {
return api.badRequest('$errorNoActiveSurvey '
'- $apiIncrementSurveyShownCount');
}
// Increment the SurveyShownCount, we've asked about the survey.
_devToolsUsage.incrementSurveyShownCount();
return api.getCompleted(
request,
json.encode(_devToolsUsage.surveyShownCount),
);
case apiGetBaseAppSizeFile:
final queryParams = request.requestedUri.queryParameters;
if (queryParams.containsKey(baseAppSizeFilePropertyName)) {
final filePath = queryParams[baseAppSizeFilePropertyName]!;
final fileJson = LocalFileSystem.devToolsFileAsJson(filePath);
if (fileJson == null) {
return api.badRequest('No JSON file available at $filePath.');
}
return api.getCompleted(request, fileJson);
}
return api.badRequest('Request for base app size file does not '
'contain a query parameter with the expected key: '
'$baseAppSizeFilePropertyName');
case apiGetTestAppSizeFile:
final queryParams = request.requestedUri.queryParameters;
if (queryParams.containsKey(testAppSizeFilePropertyName)) {
final filePath = queryParams[testAppSizeFilePropertyName]!;
final fileJson = LocalFileSystem.devToolsFileAsJson(filePath);
if (fileJson == null) {
return api.badRequest('No JSON file available at $filePath.');
}
return api.getCompleted(request, fileJson);
}
return api.badRequest('Request for test app size file does not '
'contain a query parameter with the expected key: '
'$testAppSizeFilePropertyName');
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();
static DevToolsUsage get devToolsPreferences => _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 encountered a request problem e.g.,
/// setActiveSurvey not called.
///
/// This is a 400 Bad Request response.
FutureOr<shelf.Response> badRequest([String? logError]) {
if (logError != null) print(logError);
return shelf.Response(HttpStatus.badRequest);
}
/// 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(HttpStatus.noContent);
}

View file

@ -1,227 +0,0 @@
// Copyright 2021 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.
// TODO(bkonyi): remove once package:devtools_server_api is available
// See https://github.com/flutter/devtools/issues/2958.
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:usage/usage_io.dart';
import 'file_system.dart';
/// 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, '');
}
late Analytics _analytics;
/// Does the .flutter store exist?
static bool get doesStoreExist {
return LocalFileSystem.flutterStoreExists();
}
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/.devtools).
class DevToolsUsage {
/// Create a new Usage instance; [versionOverride] and [configDirOverride] are
/// used for testing.
DevToolsUsage({
String? versionOverride,
String? configDirOverride,
}) {
LocalFileSystem.maybeMoveLegacyDevToolsStore();
properties = IOPersistentProperties(
storeName,
documentDirPath: LocalFileSystem.devToolsDir(),
);
}
static const storeName = '.devtools';
/// The activeSurvey is the property name of a top-level property
/// existing or created in the file ~/.devtools
/// If the property doesn't exist it is created with default survey values:
///
/// properties[activeSurvey]['surveyActionTaken'] = false;
/// properties[activeSurvey]['surveyShownCount'] = 0;
///
/// It is a requirement that the API apiSetActiveSurvey must be called before
/// calling any survey method on DevToolsUsage (addSurvey, rewriteActiveSurvey,
/// surveyShownCount, incrementSurveyShownCount, or surveyActionTaken).
String? _activeSurvey;
late IOPersistentProperties properties;
static const _surveyActionTaken = 'surveyActionTaken';
static const _surveyShownCount = 'surveyShownCount';
void reset() {
properties.remove('firstRun');
properties['enabled'] = 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'];
}
bool surveyNameExists(String? surveyName) => properties[surveyName] != null;
void _addSurvey(String? surveyName) {
assert(activeSurvey == surveyName);
rewriteActiveSurvey(false, 0);
}
String? get activeSurvey => _activeSurvey;
set activeSurvey(String? surveyName) {
_activeSurvey = surveyName;
if (!surveyNameExists(activeSurvey)) {
// Create the survey if property is non-existent in ~/.devtools
_addSurvey(activeSurvey);
}
}
/// Need to rewrite the entire survey structure for property to be persisted.
void rewriteActiveSurvey(bool? actionTaken, int? shownCount) {
properties[activeSurvey] = {
_surveyActionTaken: actionTaken,
_surveyShownCount: shownCount,
};
}
int? get surveyShownCount {
final prop = properties[activeSurvey];
if (prop[_surveyShownCount] == null) {
rewriteActiveSurvey(prop[_surveyActionTaken], 0);
}
return properties[activeSurvey][_surveyShownCount];
}
void incrementSurveyShownCount() {
surveyShownCount; // Ensure surveyShownCount has been initialized.
final prop = properties[activeSurvey];
rewriteActiveSurvey(prop[_surveyActionTaken], prop[_surveyShownCount] + 1);
}
bool get surveyActionTaken {
return properties[activeSurvey][_surveyActionTaken] == true;
}
set surveyActionTaken(bool? value) {
final prop = properties[activeSurvey];
rewriteActiveSurvey(value, prop[_surveyShownCount]);
}
}
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 ??= LocalFileSystem.devToolsDir();
_file = File(path.join(documentDirPath, fileName));
if (!_file.existsSync()) {
_file.createSync(recursive: true);
}
syncSettings();
}
IOPersistentProperties.fromFile(File file) : super(path.basename(file.path)) {
_file = file;
if (!_file.existsSync()) {
_file.createSync(recursive: true);
}
syncSettings();
}
late 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);
}
}

View file

@ -25,7 +25,6 @@ dependencies:
shelf_web_socket: ^1.0.0
sse: ^4.0.0
stream_channel: ^2.0.0
usage: ^4.0.0
vm_service: ^7.0.0
web_socket_channel: ^2.0.0