blob: 1938b236bade05721e9c61607750035606677509 [file]
// Copyright (c) 2025, 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:convert';
import 'dart:io' as io;
import 'package:analysis_server/src/session_logger/log_entry.dart';
import 'package:analysis_server/src/session_logger/log_normalizer.dart';
import 'package:analysis_server/src/session_logger/process_id.dart';
import 'package:language_server_protocol/protocol_special.dart' show Either2;
/// A sink for a session logger that will write entries to a file.
final class SessionLoggerFileSink extends SessionLoggerSink {
/// The sink used to write to the file.
late final io.IOSink _sink;
@override
final LogNormalizer _normalizer;
/// Initializes a newly created sink to write to the file at the given
/// [filePath].
new(String filePath, {required this._normalizer}) {
_sink = io.File(filePath).openWrite();
}
@override
Future<void> close() => _sink.close();
@override
void writeLogEntry(JsonMap entry) {
var jsonString = _normalizer.normalize(entry);
_sink.writeln(jsonString);
}
}
/// A sink for a session logger that will write entries to an in-memory buffer.
///
/// This class has been designed to allow a user to enable the capturing of log
/// entries at an arbitrary time, and then retrieve the captured entries at some
/// future time. To support this, the sink caches entries related to
/// - server initialization,
/// - workspace configuration, and
/// - text documents
/// until capturing is enabled.
final class SessionLoggerInMemorySink extends SessionLoggerSink {
/// The maximum number of entries stored in the [_sessionBuffer].
int maxBufferLength;
/// Whether entries should be captured in the buffer.
bool _capturingEntries = false;
/// A session logger to which requests should be forwarded, or `null` if there
/// is no logger to forward requests to.
final SessionLoggerSink? _nextLogger;
/// The buffer in which initialization related entries are stored.
final List<LogEntry> _initializationBuffer = [];
/// The buffer in which workspace configuration related entries are stored.
///
/// This buffer is cleared every time a new workspace configuration is
/// requested.
final List<LogEntry> _configurationBuffer = [];
/// The buffer in which text document related entries are stored.
final List<LogEntry> _textDocumentBuffer = [];
/// The buffer in which normal entries are stored.
final List<LogEntry> _sessionBuffer = [];
@override
final LogNormalizer _normalizer;
/// Initialize a newly created sink to store up to [maxBufferLength] entries.
new({
required this.maxBufferLength,
required LogNormalizer normalizer,
String? sessionLogFilePath,
}) : _normalizer = normalizer,
_nextLogger = sessionLogFilePath == null
? null
: SessionLoggerFileSink(sessionLogFilePath, normalizer: normalizer);
/// Returns a list of the entries that have been captured.
///
/// The list includes necessary initialization entries that might have
/// occurred before the capture was started.
List<LogEntry> get capturedEntries {
return [
..._initializationBuffer,
..._configurationBuffer,
..._textDocumentBuffer,
..._sessionBuffer,
].map((entry) => LogEntry(_normalize(entry.map))).toList();
}
/// Whether entries are currently being captured in the buffer.
bool get isCapturingEntries => _capturingEntries;
/// Returns the session logger to which requests should be forwarded.
SessionLoggerSink? get nextLogger => _nextLogger;
/// Stops the capturing of entries.
void startCapture() {
_sessionBuffer.clear();
_capturingEntries = true;
}
/// Stops the capturing of entries.
void stopCapture() {
_capturingEntries = false;
}
@override
void writeLogEntry(JsonMap entry) {
_nextLogger?.writeLogEntry(entry);
var logEntry = LogEntry(entry);
if (_isInitializationEntry(logEntry)) {
_initializationBuffer.add(logEntry);
return;
}
if (_capturingEntries) {
if (_sessionBuffer.length > maxBufferLength) {
_sessionBuffer.removeAt(0);
}
_sessionBuffer.add(logEntry);
} else {
if (_isConfigurationEntry(logEntry)) {
_configurationBuffer.add(logEntry);
} else if (_isTextDocumentEntry(logEntry)) {
_textDocumentBuffer.add(logEntry);
}
}
}
/// Returns the request ids of the messages in the given list of [entries].
///
/// This assumes that the entries are all from the same process. If that isn't
/// true, then the returned list may contain duplicate ids.
List<Either2<int, String>> _getRequestIds(List<LogEntry> entries) {
return entries
.where((entry) => entry.isMessage)
.map((entry) => entry.message.id)
.nonNulls
.toList();
}
/// Returns whether the [entry] is a configuration entry.
///
/// A configuration entry is defined as an entry that records the
/// configuration of the workspace.
///
/// Configuration entries are captured even when [_capturingEntries] is
/// `false`.
bool _isConfigurationEntry(LogEntry entry) {
// TODO(brianwilkerson): Make this method support the legacy protocol.
if (entry.isMessage) {
var message = entry.message;
if (message.isWorkspaceConfiguration) {
return true;
}
for (var requestId in _getRequestIds(_configurationBuffer)) {
if (message.isResponseTo(requestId)) {
return true;
}
}
}
return false;
}
/// Returns whether the [entry] is an initialization entry.
///
/// An initialization entry is defined as an entry that records the
/// initialization process.
///
/// Initialization entries are captured even when [_capturingEntries] is
/// `false`.
bool _isInitializationEntry(LogEntry entry) {
// TODO(brianwilkerson): Make this method support the legacy protocol.
if (entry.isCommandLine) return true;
if (entry.isMessage) {
var message = entry.message;
if (message.isInitializeRequest || message.isInitialized) {
return true;
}
for (var requestId in _getRequestIds(_initializationBuffer)) {
if (message.isResponseTo(requestId)) {
return true;
}
}
}
return false;
}
/// Returns whether the [entry] is a text document entry.
///
/// A text document entry is defined as an entry that records an operation
/// on a text document.
///
/// Text document entries are captured even when [_capturingEntries] is
/// `false`.
bool _isTextDocumentEntry(LogEntry entry) {
if (entry.isMessage) {
var message = entry.message;
return message.isDidChange || message.isDidClose || message.isDidOpen;
}
return false;
}
}
/// Used to write information about a session to a log.
sealed class SessionLoggerSink {
/// The normalizer used to normalize paths in log entries.
LogNormalizer get _normalizer;
Future<void> close() async {}
/// Writes the given log [entry] to this sink.
void writeLogEntry(JsonMap entry);
/// Returns the given [entry] after normalizing any paths in it.
JsonMap _normalize(JsonMap entry) {
var normalizedJsonString = _normalizer.normalize(entry);
return json.decode(normalizedJsonString) as JsonMap;
}
}