Skip to content

Commit

Permalink
Add extensionActivationState endpoint to the devtools server (#6111)
Browse files Browse the repository at this point in the history
This endpoint manages reading and writing to/from the `devtools_options.yaml` file. This file will store DevTools extension activation states and will live in a project's root directory (similar to `analysis_options.yaml`).

Example `devtools_options.yaml` file:
```
extensions:
  - foo: true
  - bar: false
```
In this scenario:
- the devtools extension for `package:foo` has been enabled by the user 
- the devtools extension for `package:bar` has been disabled by the user
- the devtools extension for some other `package:baz` has been neither enabled nor disabled (meaning that DevTools should prompt the user to activate).

This API will be consumed by DevTools app in a follow up PR.

work towards #1632
  • Loading branch information
kenzieschmoll authored Jul 28, 2023
1 parent 530dfa1 commit 14443fa
Show file tree
Hide file tree
Showing 10 changed files with 464 additions and 65 deletions.
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';
}
174 changes: 174 additions & 0 deletions packages/devtools_shared/lib/src/extensions/extension_activation.dart
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(
(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,
};
}
}
24 changes: 24 additions & 0 deletions packages/devtools_shared/lib/src/extensions/extension_model.dart
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

0 comments on commit 14443fa

Please sign in to comment.