-
Notifications
You must be signed in to change notification settings - Fork 399
Expand file tree
/
Copy pathextension_service.dart
More file actions
358 lines (316 loc) · 13.1 KB
/
Copy pathextension_service.dart
File metadata and controls
358 lines (316 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
// Copyright 2023 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_shared/devtools_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import '../shared/globals.dart';
import '../shared/server/server.dart' as server;
import 'extension_service_helpers.dart';
final _log = Logger('ExtensionService');
// TODO(https://github.com/flutter/devtools/issues/7594): detect extensions from
// globally activated pub packages.
/// Data pattern containing a [List] of available extensions and a [List] of
/// visible extensions.
typedef DevToolsExtensionsGroup = ({
// All the DevTools extensions, runtime and static, that are available for
// the connected application, regardless of whether they have been enabled
// or disabled by the user.
//
// This set of extensions will include one version of a DevTools extension
// per package.
List<DevToolsExtensionConfig> availableExtensions,
// DevTools extensions that are visible in their own DevTools screen (i.e.
// extensions that have not been manually disabled by the user).
List<DevToolsExtensionConfig> visibleExtensions,
});
class ExtensionService extends DisposableController
with AutoDisposeControllerMixin {
ExtensionService({this.fixedAppRoot, this.ignoreServiceConnection = false});
/// The fixed (unchanging) root file:// URI for the application this
/// [ExtensionService] will manage DevTools extensions for.
///
/// When null, [_appRoot] will be calculated from the
/// `serviceConnection.serviceManager`'s currently connected app.
final Uri? fixedAppRoot;
/// Whether to ignore the VM service connection for the context of this
/// service.
final bool ignoreServiceConnection;
/// The root file:// URI for the Dart / Flutter application this
/// [ExtensionService] will manage DevTools extensions for.
Uri? _appRoot;
/// A listenable for the current set of DevTools extensions.
///
/// The [DevToolsExtensionsGroup] contains both the List of available
/// extensions and the List of visible extensions. These values are updated
/// in tandem in the common case, so storing them as a group saves listeners
/// from having to listen to two separate notifiers.
ValueListenable<DevToolsExtensionsGroup> get currentExtensions =>
_currentExtensions;
final _currentExtensions = ValueNotifier<DevToolsExtensionsGroup>((
availableExtensions: <DevToolsExtensionConfig>[],
visibleExtensions: <DevToolsExtensionConfig>[],
));
/// All the DevTools extensions, runtime and static, that are available for
/// the connected application, regardless of whether they have been enabled or
/// disabled by the user.
///
/// This set of extensions will include one version of a DevTools extension
/// per package.
@visibleForTesting
List<DevToolsExtensionConfig> get availableExtensions =>
_currentExtensions.value.availableExtensions;
/// DevTools extensions that are visible in their own DevTools screen (i.e.
/// extensions that have not been manually disabled by the user).
List<DevToolsExtensionConfig> get visibleExtensions =>
_currentExtensions.value.visibleExtensions;
/// DevTools extensions available in the user's project that do not require a
/// running application.
///
/// The user's project roots are detected from the Dart Tooling Daemon.
/// Extensions are then derived from the `package_config.json` files contained
/// in each of these project roots.
///
/// Any static extensions that match a detected runtime extension will be
/// ignored to prevent duplicates.
@visibleForTesting
var staticExtensions = <DevToolsExtensionConfig>[];
/// DevTools extensions available for the connected VM service.
///
/// These extensions are derived from the `package_config.json` file contained
/// in the package root of the main isolate's root library.
@visibleForTesting
var runtimeExtensions = <DevToolsExtensionConfig>[];
/// The set of extensions that have been ignored due to being a duplicate of
/// some kind.
///
/// An extension may be a duplicate if it was detected in both the set of
/// runtime and static extensions, or if it is an older version of an existing
/// extension.
///
/// Ignored extensions will not be shown to the user, but their enablement
/// states will still be updated for changes to their matching extension's
/// state (the matching extension that is not ignored).
final _ignoredStaticExtensionsByHashCode = <int>{};
/// Returns the [ValueListenable] that stores the [ExtensionEnabledState] for
/// the DevTools Extension with [extensionName].
ValueListenable<ExtensionEnabledState> enabledStateListenable(
String extensionName,
) {
return _extensionEnabledStates.putIfAbsent(
extensionName.toLowerCase(),
() => ValueNotifier<ExtensionEnabledState>(ExtensionEnabledState.none),
);
}
/// Whether extensions are actively being refreshed by the DevTools server.
ValueListenable<bool> get refreshInProgress => _refreshInProgress;
final _refreshInProgress = ValueNotifier(false);
final _extensionEnabledStates =
<String, ValueNotifier<ExtensionEnabledState>>{};
Future<void> initialize() async {
await _refresh();
cancelListeners();
// We only need to add VM service manager related listeners when we are
// interacting with the currently connected app (i.e. when
// [fixedAppRootUri] is null).
if (fixedAppRoot == null && !ignoreServiceConnection) {
addAutoDisposeListener(
serviceConnection.serviceManager.connectedState,
() async {
await _refresh();
},
);
// TODO(https://github.com/flutter/flutter/issues/134470): refresh on
// hot reload and hot restart events instead.
addAutoDisposeListener(
serviceConnection.serviceManager.isolateManager.mainIsolate,
() async {
await _refresh();
},
);
}
addAutoDisposeListener(
preferences.devToolsExtensions.showOnlyEnabledExtensions,
() async {
await _refreshExtensionEnabledStates(
availableExtensions: _currentExtensions.value.availableExtensions,
);
},
);
// TODO(kenz): we should also refresh the available extensions on some event
// from the analysis server that is watching the
// .dart_tool/package_config.json file for changes.
}
Future<void> _refresh() async {
_log.fine('refreshing the ExtensionService');
_reset();
_appRoot = null;
if (fixedAppRoot != null) {
_appRoot = fixedAppRoot;
} else if (!ignoreServiceConnection &&
serviceConnection.serviceManager.connectedState.value.connected &&
serviceConnection.serviceManager.isolateManager.mainIsolate.value !=
null) {
_appRoot = await serviceConnection.serviceManager.connectedAppPackageRoot(
dtdManager,
);
}
// TODO(kenz): gracefully handle app connections / disconnects when there
// are already static extensions in use. The current code resets everything
// when connection states change or hot restarts occur.
_refreshInProgress.value = true;
final allExtensions = await server.refreshAvailableExtensions(_appRoot);
runtimeExtensions = allExtensions
.where((e) => !e.detectedFromStaticContext)
.toList();
staticExtensions = allExtensions
.where((e) => e.detectedFromStaticContext)
.toList();
// TODO(kenz): consider handling duplicates in a way that gives the user a
// choice of which version they want to use.
_deduplicateStaticExtensions();
_deduplicateStaticExtensionsWithRuntimeExtensions();
final available = [
...runtimeExtensions,
...staticExtensions.where((ext) => !isExtensionIgnored(ext)),
]..sort();
await _refreshExtensionEnabledStates(availableExtensions: available);
_refreshInProgress.value = false;
}
/// De-duplicates static extensions from other static extensions by ignoring
/// all that are not the latest version when there are duplicates.
void _deduplicateStaticExtensions() {
deduplicateExtensionsAndTakeLatest(
staticExtensions,
onSetIgnored: setExtensionIgnored,
logger: _log,
extensionType: 'static',
);
}
// De-duplicates unignored static extensions from runtime extensions by
// ignoring the static extension when there is a duplicate.
void _deduplicateStaticExtensionsWithRuntimeExtensions() {
if (runtimeExtensions.isEmpty) return;
for (final staticExtension in staticExtensions.where(
(ext) => !isExtensionIgnored(ext),
)) {
// TODO(kenz): do we need to match on something other than name? Names
// _should_ be unique since they match a pub package name, but this may
// not always be true for extensions that are not published on pub or
// extensions that do not follow best practices for naming.
final isRuntimeDuplicate = runtimeExtensions.any(
(ext) => ext.name == staticExtension.name,
);
if (isRuntimeDuplicate) {
_log.fine(
'ignoring duplicate static extension ${staticExtension.identifier} '
'at ${staticExtension.devtoolsOptionsUri} in favor of a matching '
'runtime extension.',
);
setExtensionIgnored(staticExtension, ignore: true);
}
}
}
Future<void> _refreshExtensionEnabledStates({
required List<DevToolsExtensionConfig> availableExtensions,
}) async {
final onlyIncludeEnabled =
preferences.devToolsExtensions.showOnlyEnabledExtensions.value;
final visible = <DevToolsExtensionConfig>[];
for (final extension in availableExtensions) {
final stateFromOptionsFile = await server.extensionEnabledState(
devtoolsOptionsFileUri: extension.devtoolsOptionsUri,
extensionName: extension.name,
);
final stateNotifier = _extensionEnabledStates.putIfAbsent(
extension.name,
() => ValueNotifier<ExtensionEnabledState>(stateFromOptionsFile),
);
stateNotifier.value = stateFromOptionsFile;
final shouldIncludeInVisible = onlyIncludeEnabled
? stateFromOptionsFile == ExtensionEnabledState.enabled
: stateFromOptionsFile != ExtensionEnabledState.disabled;
if (shouldIncludeInVisible) {
visible.add(extension);
}
}
_log.fine(
'visible extensions after refreshing - ${visible.map((e) => e.name).toList()}',
);
// It is not necessary to sort [visible] because [availableExtensions] is
// already sorted.
_currentExtensions.value = (
availableExtensions: availableExtensions,
visibleExtensions: visible,
);
}
/// Sets the enabled state for [extension] and any currently ignored
/// duplicates of [extension].
Future<void> setExtensionEnabledState(
DevToolsExtensionConfig extension, {
required bool enable,
}) async {
// Set the enabled state for all matching extensions, even if some are
// marked as ignored due to being a duplicate. This ensures that
// devtools_options.yaml files are kept in sync across the project.
final allMatchingExtensions = [
...runtimeExtensions,
...staticExtensions,
].where((e) => e.name == extension.name);
await [
for (final ext in allMatchingExtensions)
server.extensionEnabledState(
devtoolsOptionsFileUri: ext.devtoolsOptionsUri,
extensionName: ext.name,
enable: enable,
),
].wait;
await _refreshExtensionEnabledStates(
availableExtensions: _currentExtensions.value.availableExtensions,
);
}
/// Marks this extension configuration as ignored or unignored based on the
/// value of [ignore].
///
/// An extension may be ignored if it is a duplicate or if it is an older
/// version of an existing extension, for example.
@visibleForTesting
void setExtensionIgnored(
DevToolsExtensionConfig ext, {
required bool ignore,
}) {
ignore
? _ignoredStaticExtensionsByHashCode.add(identityHashCode(ext))
: _ignoredStaticExtensionsByHashCode.remove(identityHashCode(ext));
}
/// Whether this extension configuration should be ignored.
///
/// An extension may be ignored if it is a duplicate or if it is an older
/// version of an existing extension, for example.
bool isExtensionIgnored(DevToolsExtensionConfig ext) {
return _ignoredStaticExtensionsByHashCode.contains(identityHashCode(ext));
}
void _reset() {
_log.fine('resetting the ExtensionService');
_appRoot = null;
runtimeExtensions.clear();
staticExtensions.clear();
_ignoredStaticExtensionsByHashCode.clear();
_currentExtensions.value = (
availableExtensions: <DevToolsExtensionConfig>[],
visibleExtensions: <DevToolsExtensionConfig>[],
);
_extensionEnabledStates.clear();
_refreshInProgress.value = false;
}
@override
void dispose() {
for (final notifier in _extensionEnabledStates.values) {
notifier.dispose();
}
_currentExtensions.dispose();
_refreshInProgress.dispose();
super.dispose();
}
}