mirror of
https://github.com/dart-lang/sdk
synced 2024-11-02 10:49:00 +00:00
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:
parent
16ff4aec0e
commit
3365b77ac2
6 changed files with 2 additions and 541 deletions
|
@ -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);
|
||||
|
||||
|
|
|
@ -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/.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue