Skip to content

Commit 4452283

Browse files
committed
fix(logging): combine newline-split stdout messages into one log entry
1 parent 9e3f94d commit 4452283

3 files changed

Lines changed: 79 additions & 8 deletions

File tree

packages/devtools_app/lib/src/screens/logging/logging_controller.dart

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -293,13 +293,13 @@ class LoggingController extends DevToolsScreenController
293293

294294
void _handleConnectionStart(VmServiceWrapper service) {
295295
// Log stdout events.
296-
final stdoutHandler = _StdoutEventHandler(this, 'stdout');
296+
final stdoutHandler = StdoutEventHandler(this, 'stdout');
297297
autoDisposeStreamSubscription(
298298
service.onStdoutEventWithHistorySafe.listen(stdoutHandler.handle),
299299
);
300300

301301
// Log stderr events.
302-
final stderrHandler = _StdoutEventHandler(this, 'stderr', isError: true);
302+
final stderrHandler = StdoutEventHandler(this, 'stderr', isError: true);
303303
autoDisposeStreamSubscription(
304304
service.onStderrEventWithHistorySafe.listen(stderrHandler.handle),
305305
);
@@ -841,12 +841,9 @@ extension type _LogRecord(Map<String, dynamic> json) {
841841
/// stdout message and its newline. Currently, `foo\n` is sent as two VM events;
842842
/// we wait for up to 1ms when we get the `foo` event, to see if the next event
843843
/// is a single newline. If so, we add the newline to the previous log message.
844-
class _StdoutEventHandler {
845-
_StdoutEventHandler(
846-
this.loggingController,
847-
this.name, {
848-
this.isError = false,
849-
});
844+
@visibleForTesting
845+
class StdoutEventHandler {
846+
StdoutEventHandler(this.loggingController, this.name, {this.isError = false});
850847

851848
final LoggingController loggingController;
852849
final String name;
@@ -876,6 +873,28 @@ class _StdoutEventHandler {
876873
return;
877874
}
878875

876+
// If the buffered message ends with a newline, the next message is a
877+
// continuation of the same print statement (e.g. debugPrint('line1\nline2')
878+
// is sent by the VM as two events: 'line1\n' and 'line2'). Combine them
879+
// into a single log entry.
880+
// See: https://github.com/flutter/devtools/issues/9557
881+
if (buffer!.details!.endsWith('\n')) {
882+
final combined = LogData(
883+
buffer!.kind,
884+
buffer!.details! + message,
885+
buffer!.timestamp,
886+
summary: buffer!.summary,
887+
isError: buffer!.isError,
888+
isolateRef: e.isolateRef,
889+
);
890+
buffer = combined;
891+
timer = Timer(const Duration(milliseconds: 1), () {
892+
loggingController.log(buffer!);
893+
buffer = null;
894+
});
895+
return;
896+
}
897+
879898
loggingController.log(buffer!);
880899
buffer = null;
881900
}

packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ TODO: Remove this section if there are not any updates.
4545

4646
- Added support for searching within the log details view (raw text mode). [#9712](https://github.com/flutter/devtools/pull/9712)
4747
![Search in log details](images/log_details_search.png "Searching within the log details view")
48+
- Fixed an issue where log messages containing newline characters were incorrectly split into multiple separate entries in the Logging screen (#9557).
4849

4950
## App size tool updates
5051

packages/devtools_app/test/screens/logging/logging_controller_test.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,4 +381,55 @@ void main() {
381381
},
382382
);
383383
});
384+
385+
group('StdoutEventHandler', () {
386+
late LoggingController controller;
387+
late StdoutEventHandler stdoutHandler;
388+
389+
Event stdoutEvent(String message, {int? timestamp}) {
390+
return Event(
391+
bytes: base64Encode(utf8.encode(message)),
392+
timestamp: timestamp ?? ++timestampCounter,
393+
);
394+
}
395+
396+
setUp(() {
397+
setGlobal(ServiceConnectionManager, FakeServiceConnectionManager());
398+
setGlobal(MessageBus, MessageBus());
399+
setGlobal(PreferencesController, PreferencesController());
400+
401+
controller = LoggingController()..init();
402+
stdoutHandler = StdoutEventHandler(controller, 'stdout');
403+
});
404+
405+
test('combines newline-terminated continuation into one log', () async {
406+
stdoutHandler.handle(stdoutEvent('line1\n'));
407+
stdoutHandler.handle(stdoutEvent('line2'));
408+
409+
await Future<void>.delayed(const Duration(milliseconds: 10));
410+
411+
expect(controller.data, hasLength(1));
412+
expect(controller.data.single.kind, 'stdout');
413+
expect(controller.data.single.details, 'line1\nline2');
414+
});
415+
416+
test('still combines message followed by lone newline', () {
417+
stdoutHandler.handle(stdoutEvent('line1'));
418+
stdoutHandler.handle(stdoutEvent('\n'));
419+
420+
expect(controller.data, hasLength(1));
421+
expect(controller.data.single.details, 'line1\n');
422+
});
423+
424+
test('keeps separate entries for distinct non-newline chunks', () async {
425+
stdoutHandler.handle(stdoutEvent('line1'));
426+
stdoutHandler.handle(stdoutEvent('line2'));
427+
428+
await Future<void>.delayed(const Duration(milliseconds: 10));
429+
430+
expect(controller.data, hasLength(2));
431+
expect(controller.data[0].details, 'line1');
432+
expect(controller.data[1].details, 'line2');
433+
});
434+
});
384435
}

0 commit comments

Comments
 (0)