Skip to content

Commit f8e8a8d

Browse files
authored
[ Widget Preview ] Add --machine mode (flutter#173654)
Currently only outputs a single event with information about where the widget preview environment is served from: `[{"event":"widget_preview.started","params":{"url":"http://localhost:61383"}}]` Fixes flutter#173545
1 parent 9e99953 commit f8e8a8d

File tree

3 files changed

+276
-7
lines changed

3 files changed

+276
-7
lines changed

packages/flutter_tools/lib/executable.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,7 @@ Future<void> main(List<String> args) async {
8888
final bool daemon = args.contains('daemon');
8989
final bool runMachine =
9090
(args.contains('--machine') && args.contains('run')) ||
91-
(args.contains('--machine') && args.contains('attach')) ||
92-
// `flutter widget-preview start` starts an application that requires a logger
93-
// to be setup for machine mode.
94-
(args.contains('widget-preview') && args.contains('start'));
91+
(args.contains('--machine') && args.contains('attach'));
9592

9693
// Cache.flutterRoot must be set early because other features use it (e.g.
9794
// enginePath's initializer uses it). This can only work with the real

packages/flutter_tools/lib/src/commands/widget_preview.dart

Lines changed: 152 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ import '../base/logger.dart';
1616
import '../base/os.dart';
1717
import '../base/platform.dart';
1818
import '../base/process.dart';
19+
import '../base/terminal.dart';
1920
import '../build_info.dart';
2021
import '../bundle.dart' as bundle;
2122
import '../cache.dart';
23+
import '../convert.dart';
2224
import '../device.dart';
2325
import '../globals.dart' as globals;
2426
import '../isolated/resident_web_runner.dart';
@@ -116,7 +118,7 @@ abstract base class WidgetPreviewSubCommandBase extends FlutterCommand {
116118
final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with CreateBase {
117119
WidgetPreviewStartCommand({
118120
this.verbose = false,
119-
required this.logger,
121+
required Logger logger,
120122
required this.fs,
121123
required this.projectFactory,
122124
required this.cache,
@@ -126,11 +128,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
126128
required this.processManager,
127129
required this.artifacts,
128130
@visibleForTesting WidgetPreviewDtdServices? dtdServicesOverride,
129-
}) {
131+
}) : logger = WidgetPreviewMachineAwareLogger(logger) {
130132
if (dtdServicesOverride != null) {
131133
_dtdService = dtdServicesOverride;
132134
}
133135
addPubOptions();
136+
addMachineOutputFlag(verboseHelp: verbose);
134137
argParser
135138
..addFlag(
136139
kWebServer,
@@ -189,7 +192,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
189192
final FileSystem fs;
190193

191194
@override
192-
final Logger logger;
195+
final WidgetPreviewMachineAwareLogger logger;
193196

194197
@override
195198
final FlutterProjectFactory projectFactory;
@@ -255,6 +258,9 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
255258
? fs.directory(customPreviewScaffoldOutput)
256259
: rootProject.widgetPreviewScaffold;
257260

261+
final bool machine = boolArg(FlutterGlobalOptions.kMachineFlag);
262+
logger.machine = machine;
263+
258264
// Check to see if a preview scaffold has already been generated. If not,
259265
// generate one.
260266
final bool generateScaffoldProject =
@@ -444,6 +450,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
444450
);
445451
unawaited(_widgetPreviewApp!.run(appStartedCompleter: appStarted));
446452
await appStarted.future;
453+
logger.sendEvent('started', {'url': flutterDevice.devFS!.baseUri.toString()});
447454
}
448455
} on Exception catch (error) {
449456
throwToolExit(error.toString());
@@ -491,3 +498,145 @@ final class WidgetPreviewCleanCommand extends WidgetPreviewSubCommandBase {
491498
return FlutterCommandResult.success();
492499
}
493500
}
501+
502+
/// A custom logger for the widget-preview commands that disables non-event output to stdio when
503+
/// machine mode is enabled.
504+
final class WidgetPreviewMachineAwareLogger extends DelegatingLogger {
505+
WidgetPreviewMachineAwareLogger(super.delegate);
506+
507+
var machine = false;
508+
509+
@override
510+
void printError(
511+
String message, {
512+
StackTrace? stackTrace,
513+
bool? emphasis,
514+
TerminalColor? color,
515+
int? indent,
516+
int? hangingIndent,
517+
bool? wrap,
518+
}) {
519+
if (machine) {
520+
return;
521+
}
522+
super.printError(
523+
message,
524+
stackTrace: stackTrace,
525+
emphasis: emphasis,
526+
color: color,
527+
indent: indent,
528+
hangingIndent: hangingIndent,
529+
wrap: wrap,
530+
);
531+
}
532+
533+
@override
534+
void printWarning(
535+
String message, {
536+
bool? emphasis,
537+
TerminalColor? color,
538+
int? indent,
539+
int? hangingIndent,
540+
bool? wrap,
541+
bool fatal = true,
542+
}) {
543+
if (machine) {
544+
return;
545+
}
546+
super.printWarning(
547+
message,
548+
emphasis: emphasis,
549+
color: color,
550+
indent: indent,
551+
hangingIndent: hangingIndent,
552+
wrap: wrap,
553+
fatal: fatal,
554+
);
555+
}
556+
557+
@override
558+
void printStatus(
559+
String message, {
560+
bool? emphasis,
561+
TerminalColor? color,
562+
bool? newline,
563+
int? indent,
564+
int? hangingIndent,
565+
bool? wrap,
566+
}) {
567+
if (machine) {
568+
return;
569+
}
570+
super.printStatus(
571+
message,
572+
emphasis: emphasis,
573+
color: color,
574+
newline: newline,
575+
indent: indent,
576+
hangingIndent: hangingIndent,
577+
wrap: wrap,
578+
);
579+
}
580+
581+
@override
582+
void printBox(String message, {String? title}) {
583+
if (machine) {
584+
return;
585+
}
586+
super.printBox(message, title: title);
587+
}
588+
589+
@override
590+
void printTrace(String message) {
591+
if (machine) {
592+
return;
593+
}
594+
super.printTrace(message);
595+
}
596+
597+
@override
598+
void sendEvent(String name, [Map<String, dynamic>? args]) {
599+
if (!machine) {
600+
return;
601+
}
602+
super.printStatus(
603+
json.encode([
604+
{'event': 'widget_preview.$name', 'params': ?args},
605+
]),
606+
);
607+
}
608+
609+
@override
610+
Status startProgress(
611+
String message, {
612+
String? progressId,
613+
int progressIndicatorPadding = kDefaultStatusPadding,
614+
}) {
615+
if (machine) {
616+
return SilentStatus(stopwatch: Stopwatch());
617+
}
618+
return super.startProgress(
619+
message,
620+
progressId: progressId,
621+
progressIndicatorPadding: progressIndicatorPadding,
622+
);
623+
}
624+
625+
@override
626+
Status startSpinner({
627+
VoidCallback? onFinish,
628+
Duration? timeout,
629+
SlowWarningCallback? slowWarningCallback,
630+
TerminalColor? warningColor,
631+
}) {
632+
if (machine) {
633+
return SilentStatus(stopwatch: Stopwatch());
634+
}
635+
return super.startSpinner(
636+
onFinish: onFinish,
637+
timeout: timeout,
638+
slowWarningCallback: slowWarningCallback,
639+
warningColor: warningColor,
640+
);
641+
}
642+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:convert';
7+
8+
import 'package:file/file.dart';
9+
import 'package:flutter_tools/src/base/io.dart';
10+
import 'package:flutter_tools/src/commands/widget_preview.dart';
11+
import 'package:flutter_tools/src/widget_preview/dtd_services.dart';
12+
import 'package:http/http.dart';
13+
import 'package:process/process.dart';
14+
15+
import '../src/common.dart';
16+
import 'test_data/basic_project.dart';
17+
import 'test_utils.dart';
18+
19+
typedef ExpectedEvent = ({String event, FutureOr<void> Function(Map<String, Object?>)? validator});
20+
21+
final launchEvents = <ExpectedEvent>[
22+
(
23+
event: 'widget_preview.started',
24+
validator: (Map<String, Object?> params) async {
25+
if (params case {'uri': final String uri}) {
26+
try {
27+
final Response response = await get(Uri.parse(uri));
28+
expect(response.statusCode, HttpStatus.ok, reason: 'Failed to retrieve widget previewer');
29+
} catch (e) {
30+
fail('Failed to access widget previewer: $e');
31+
}
32+
}
33+
},
34+
),
35+
];
36+
37+
void main() {
38+
late Directory tempDir;
39+
Process? process;
40+
DtdLauncher? dtdLauncher;
41+
final project = BasicProject();
42+
const ProcessManager processManager = LocalProcessManager();
43+
44+
setUp(() async {
45+
tempDir = createResolvedTempDirectorySync('widget_preview_test.');
46+
await project.setUpIn(tempDir);
47+
});
48+
49+
tearDown(() async {
50+
process?.kill();
51+
process = null;
52+
await dtdLauncher?.dispose();
53+
dtdLauncher = null;
54+
tryToDelete(tempDir);
55+
});
56+
57+
Future<void> runWidgetPreviewMachineMode({
58+
required List<ExpectedEvent> expectedEvents,
59+
bool useWebServer = false,
60+
}) async {
61+
expect(expectedEvents, isNotEmpty);
62+
process = await processManager.start(<String>[
63+
flutterBin,
64+
'widget-preview',
65+
'start',
66+
'--machine',
67+
'--${WidgetPreviewStartCommand.kHeadless}',
68+
if (useWebServer) '--${WidgetPreviewStartCommand.kWebServer}',
69+
], workingDirectory: tempDir.path);
70+
71+
final completer = Completer<void>();
72+
var nextExpectationIndex = 0;
73+
process!.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((
74+
String message,
75+
) async {
76+
printOnFailure('STDOUT: $message');
77+
if (completer.isCompleted) {
78+
return;
79+
}
80+
try {
81+
final Object? event = json.decode(message);
82+
if (event case [final Map<String, Object?> eventObject]) {
83+
final ExpectedEvent expectation = expectedEvents[nextExpectationIndex];
84+
if (expectation.event == eventObject['event']) {
85+
await expectation.validator?.call(eventObject);
86+
++nextExpectationIndex;
87+
}
88+
}
89+
if (nextExpectationIndex == expectedEvents.length) {
90+
completer.complete();
91+
}
92+
} on FormatException {
93+
// Do nothing.
94+
}
95+
});
96+
97+
process!.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((String msg) {
98+
printOnFailure('STDERR: $msg');
99+
});
100+
101+
unawaited(
102+
process!.exitCode.then((int exitCode) {
103+
if (completer.isCompleted) {
104+
return;
105+
}
106+
completer.completeError(
107+
TestFailure('The widget previewer exited unexpectedly (exit code: $exitCode)'),
108+
);
109+
}),
110+
);
111+
await completer.future;
112+
}
113+
114+
group('flutter widget-preview start --machine', () {
115+
testWithoutContext('launches in browser', () async {
116+
await runWidgetPreviewMachineMode(expectedEvents: launchEvents);
117+
});
118+
119+
testWithoutContext('launches web server', () async {
120+
await runWidgetPreviewMachineMode(expectedEvents: launchEvents, useWebServer: true);
121+
});
122+
});
123+
}

0 commit comments

Comments
 (0)