Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add extensionActivationState endpoint to the devtools server #6111

Merged
merged 7 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Future<DevToolsJsonFile?> requestTestAppSizeFile(String path) async {
}

Future<List<DevToolsExtensionConfig>> refreshAvailableExtensions(
String? rootPath,
String rootPath,
) async {
return [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,26 +339,30 @@ DevToolsJsonFile _devToolsJsonFileFromResponse(
);
}

/// Makes a request to the server to refresh the list of available extensions,
/// serve their assets on the server, and return the list of available
/// extensions here.
Future<List<DevToolsExtensionConfig>> refreshAvailableExtensions(
String? rootPath,
String rootPath,
) async {
if (isDevToolsServerAvailable) {
final uri = Uri(
path: apiServeAvailableExtensions,
queryParameters: {extensionRootPathPropertyName: rootPath},
path: ExtensionsApi.apiServeAvailableExtensions,
queryParameters: {ExtensionsApi.extensionRootPathPropertyName: rootPath},
);
final resp = await request(uri.toString());
if (resp?.status == HttpStatus.ok) {
final parsedResult = json.decode(resp!.responseText!);
final extensionsAsJson =
(parsedResult[extensionsResultPropertyName]! as List<Object?>)
(parsedResult[ExtensionsApi.extensionsResultPropertyName]!
as List<Object?>)
.whereNotNull()
.cast<Map<String, Object?>>();
return extensionsAsJson
.map((p) => DevToolsExtensionConfig.parse(p))
.toList();
} else {
logWarning(resp, apiServeAvailableExtensions);
logWarning(resp, ExtensionsApi.apiServeAvailableExtensions);
return [];
}
}
Expand Down
10 changes: 0 additions & 10 deletions packages/devtools_extensions/.metadata

This file was deleted.

50 changes: 32 additions & 18 deletions packages/devtools_shared/lib/src/devtools_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,15 @@ const surveyActionTakenPropertyName = 'surveyActionTaken';
const apiGetSurveyShownCount = '${apiPrefix}getSurveyShownCount';

/// Increments the surveyShownCount of the of the activeSurvey (apiSetActiveSurvey).
const apiIncrementSurveyShownCount =
'${apiPrefix}incrementSurveyShownCount';
const apiIncrementSurveyShownCount = '${apiPrefix}incrementSurveyShownCount';

const lastReleaseNotesVersionPropertyName = 'lastReleaseNotesVersion';

/// Returns the last DevTools version for which we have shown release notes.
const apiGetLastReleaseNotesVersion =
'${apiPrefix}getLastReleaseNotesVersion';
const apiGetLastReleaseNotesVersion = '${apiPrefix}getLastReleaseNotesVersion';

/// Sets the last DevTools version for which we have shown release notes.
const apiSetLastReleaseNotesVersion =
'${apiPrefix}setLastReleaseNotesVersion';
const apiSetLastReleaseNotesVersion = '${apiPrefix}setLastReleaseNotesVersion';

/// Returns the base app size file, if present.
const apiGetBaseAppSizeFile = '${apiPrefix}getBaseAppSizeFile';
Expand All @@ -65,15 +62,32 @@ const baseAppSizeFilePropertyName = 'appSizeBase';

const testAppSizeFilePropertyName = 'appSizeTest';

/// Serves any available extensions and returns a list of their configurations
/// to DevTools.
const apiServeAvailableExtensions =
'${apiPrefix}serveAvailableExtensions';

/// The property name for the query parameter passed along with
/// [apiServeAvailableExtensions] requests to the server.
const extensionRootPathPropertyName = 'rootPath';

/// The property name for the response that the server sends back upon
/// receiving a [apiServeAvailableExtensions] request.
const extensionsResultPropertyName = 'extensions';
abstract class ExtensionsApi {
/// Serves any available extensions and returns a list of their configurations
/// to DevTools.
static const apiServeAvailableExtensions =
'${apiPrefix}serveAvailableExtensions';

/// The property name for the query parameter passed along with
/// extension-related requests to the server that describes the package root
/// for the app whose extensions are being queried.
static const extensionRootPathPropertyName = 'rootPath';

/// The property name for the response that the server sends back upon
/// receiving a [apiServeAvailableExtensions] request.
static const extensionsResultPropertyName = 'extensions';

/// Returns and optionally sets the activation state for a DevTools extension.
static const apiExtensionActivationState =
'${apiPrefix}extensionActivationState';

/// The property name for the query parameter passed along with
/// [apiExtensionActivationState] requests to the server that describes the
/// name of the extension whose state is being queried.
static const extensionNamePropertyName = 'name';

/// The property name for the query parameter that is optionally passed along
/// with [apiExtensionActivationState] requests to the server to set the
/// activation state for the extension.
static const activationStatePropertyName = 'activate';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. 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:io';

import 'package:collection/collection.dart';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';
import 'package:yaml_edit/yaml_edit.dart';

import 'extension_model.dart';

/// Manages the `devtools_options.yaml` file and allows read / write access.
class DevToolsOptions {
static const optionsFileName = 'devtools_options.yaml';

static const _extensionsKey = 'extensions';

static const _defaultOptions = '''
$_extensionsKey:
''';

/// Returns the current activation state for [extensionName] in the
/// 'devtools_options.yaml' file at [rootUri].
///
/// If the 'devtools_options.yaml' file does not exist, it will be created
/// with an empty set of extensions.
ExtensionActivationState lookupExtensionActivationState({
required Uri rootUri,
required String extensionName,
}) {
final options = _optionsAsMap(rootUri: rootUri);
if (options == null) return ExtensionActivationState.error;

final extensions =
(options[_extensionsKey] as List?)?.cast<Map<String, Object?>>();
if (extensions == null) return ExtensionActivationState.none;

for (final e in extensions) {
// Each entry should only have one key / value pair (e.g. '- foo: true').
assert(e.keys.length == 1);

if (e.keys.first == extensionName) {
return _extensionStateForValue(e[extensionName]);
}
}
return ExtensionActivationState.none;
}

/// Sets the activation state for [extensionName] in the
/// 'devtools_options.yaml' file at [rootUri].
///
/// If the 'devtools_options.yaml' file does not exist, it will be created.
ExtensionActivationState setExtensionActivationState({
required Uri rootUri,
required String extensionName,
required bool activate,
}) {
final options = _optionsAsMap(rootUri: rootUri);
if (options == null) return ExtensionActivationState.error;

var extensions =
(options[_extensionsKey] as List?)?.cast<Map<String, Object?>>();
if (extensions == null) {
options[_extensionsKey] = <Map<String, Object?>>[];
extensions = options[_extensionsKey] as List<Map<String, Object?>>;
}

// Write the new activation state to the map.
final extension = extensions.firstWhereOrNull(
kenzieschmoll marked this conversation as resolved.
Show resolved Hide resolved
(e) => e.keys.first == extensionName,
);
if (extension == null) {
extensions.add({extensionName: activate});
} else {
extension[extensionName] = activate;
}

_writeToOptionsFile(rootUri: rootUri, options: options);

// Lookup the activation state from the file we just wrote to to ensure that
// are not returning an out of sync result.
return lookupExtensionActivationState(
rootUri: rootUri,
extensionName: extensionName,
);
}

/// Returns the content of the `devtools_options.yaml` file at [rootUri] as a
/// Map.
Map<String, Object?>? _optionsAsMap({required Uri rootUri}) {
final optionsFile = _lookupOptionsFile(rootUri);
if (optionsFile == null) return null;
final yamlMap = loadYaml(optionsFile.readAsStringSync()) as YamlMap;
return yamlMap.toDartMap();
}

/// Writes the `devtools_options.yaml` file at [rootUri] with the value of
/// [options] as YAML.
///
/// Any existing content in `devtools_options.yaml` will be overwritten.
void _writeToOptionsFile({
required Uri rootUri,
required Map<String, Object?> options,
}) {
final yamlEditor = YamlEditor('');
yamlEditor.update([], options);
_lookupOptionsFile(rootUri)?.writeAsStringSync(yamlEditor.toString());
}

/// Returns the `devtools_options.yaml` file in the [rootUri] directory.
///
/// Returns null if the directory at [rootUri] does not exist. Otherwise, if
/// the `devtools_options.yaml` does not already exist, it will be created
/// and written with [_defaultOptions], and then returned.
File? _lookupOptionsFile(Uri rootUri) {
final rootDir = Directory.fromUri(rootUri);
if (!rootDir.existsSync()) {
print('Directory does not exist at path: ${rootUri.toString()}');
return null;
}

final optionsFile = File(path.join(rootDir.path, optionsFileName));
if (!optionsFile.existsSync()) {
optionsFile
..createSync()
..writeAsStringSync(_defaultOptions);
}
return optionsFile;
}

ExtensionActivationState _extensionStateForValue(Object? value) {
switch (value) {
case true:
return ExtensionActivationState.enabled;
case false:
return ExtensionActivationState.disabled;
default:
return ExtensionActivationState.none;
}
}
}

extension YamlExtension on YamlMap {
Map<String, Object?> toDartMap() {
final map = <String, Object?>{};
for (final entry in nodes.entries) {
map[entry.key.toString()] = entry.value.convertToDartType();
}
return map;
}
}

extension YamlListExtension on YamlList {
List<Object?> toDartList() {
final list = <Object>[];
for (final e in nodes) {
final element = e.convertToDartType();
if (element != null) list.add(element);
}
return list;
}
}

extension YamlNodeExtension on YamlNode {
Object? convertToDartType() {
return switch (this) {
YamlMap() => (this as YamlMap).toDartMap(),
YamlList() => (this as YamlList).toDartList(),
_ => value,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:collection/collection.dart';

/// Describes an extension that can be dynamically loaded into a custom screen
/// in DevTools.
class DevToolsExtensionConfig {
Expand Down Expand Up @@ -109,3 +111,25 @@ class DevToolsExtensionConfig {
materialIconCodePointKey: materialIconCodePoint,
};
}

enum ExtensionActivationState {
/// The extension has been enabled manually by the user.
enabled,

/// The extension has been disabled manually by the user.
disabled,

/// The extension has been neither enabled nor disabled by the user.
none,

/// Something went wrong with reading or writing the activation state.
///
/// We should ignore extensions with this activation state.
error;

static ExtensionActivationState from(String? value) {
return ExtensionActivationState.values
.firstWhereOrNull((e) => e.name == value) ??
ExtensionActivationState.none;
}
}
Loading