blob: 08033d85c07b72d20796c73dad249c57e6b6e84e [file] [log] [blame]
// Copyright (c) 2023, 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 'package:clock/clock.dart';
import 'package:file/file.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;
import 'constants.dart';
import 'initializer.dart';
/// The regex pattern used to parse the disable analytics line
const String telemetryFlagPattern = r'^reporting=([0|1]) *$';
/// The regex pattern used to parse the tools info
/// from the configuration file
///
/// Example:
/// flutter_tools=2022-10-26,1
const String toolPattern =
r'^([A-Za-z0-9]+[A-Za-z0-9_]*)=([0-9]{4}-[0-9]{2}-[0-9]{2}),([0-9]+)$';
class ConfigHandler {
/// Regex pattern implementation for matching a line in the config file
///
/// Example:
/// flutter-tools=2022-10-26,1
static RegExp telemetryFlagRegex =
RegExp(telemetryFlagPattern, multiLine: true);
static RegExp toolRegex = RegExp(toolPattern, multiLine: true);
/// Get a string representation of the current date in the following format
/// yyyy-MM-dd (2023-01-09)
static String get dateStamp {
return DateFormat('yyyy-MM-dd').format(clock.now());
}
final FileSystem fs;
final Directory homeDirectory;
final Initializer initializer;
final File configFile;
final Map<String, ToolInfo> parsedTools = <String, ToolInfo>{};
late DateTime configFileLastModified;
/// Reporting enabled unless specified by user
bool _telemetryEnabled = true;
ConfigHandler({
required this.fs,
required this.homeDirectory,
required this.initializer,
}) : configFile = fs.file(p.join(
homeDirectory.path,
kDartToolDirectoryName,
kConfigFileName,
)) {
// Get the last time the file was updated and check this
// datestamp whenever the client asks for the telemetry enabled boolean
configFileLastModified = configFile.lastModifiedSync();
// Call the method to parse the contents of the config file when
// this class is initialized
parseConfig();
}
/// Returns the telemetry state from the config file
///
/// Method will reparse the config file if it detects that the
/// last modified datetime is different from what was parsed when
/// the class was initialized
bool get telemetryEnabled {
if (configFileLastModified.isBefore(configFile.lastModifiedSync())) {
parseConfig();
configFileLastModified = configFile.lastModifiedSync();
}
return _telemetryEnabled;
}
/// Responsibe for the creation of the configuration line
/// for the tool being passed in by the user and adding a
/// [ToolInfo] object
void addTool({required String tool}) {
// Create the new instance of [ToolInfo] to be added
// to the [parsedTools] map
parsedTools[tool] = ToolInfo(lastRun: clock.now(), versionNumber: 1);
// New string to be appended to the bottom of the configuration file
// with a newline character for new tools to be added
String newTool = '$tool=$dateStamp,1\n';
if (!configFile.readAsStringSync().endsWith('\n')) {
newTool = '\n$newTool';
}
configFile.writeAsStringSync(newTool, mode: FileMode.append);
configFileLastModified = configFile.lastModifiedSync();
}
/// Will increment the version number and update the date
/// in the config file for the provided tool name while
/// also incrementing the version number in [ToolInfo]
void incrementToolVersion({required String tool}) {
if (!parsedTools.containsKey(tool)) {
return;
}
// Read in the config file contents and use a regex pattern to
// match the line for the current tool (ie. flutter-tools=2023-01-05,1)
final String configString = configFile.readAsStringSync();
final String pattern = '^($tool)=([0-9]{4}-[0-9]{2}-[0-9]{2}),([0-9]+)\$';
final RegExp regex = RegExp(pattern, multiLine: true);
final Iterable<RegExpMatch> matches = regex.allMatches(configString);
// If there isn't exactly one match for the given tool, that suggests the
// file has been altered and needs to be reset
if (matches.length != 1) {
resetConfig();
return;
}
final RegExpMatch match = matches.first;
// Extract the groups from the regex match to prep for parsing
final int newVersionNumber = int.parse(match.group(3) as String) + 1;
// Construct the new tool line for the config line and replace it
// in the original config string to prep for writing back out
final String newToolString = '$tool=$dateStamp,$newVersionNumber';
final String newConfigString =
configString.replaceAll(regex, newToolString);
configFile.writeAsStringSync(newConfigString);
final ToolInfo? toolInfo = parsedTools[tool];
if (toolInfo == null) {
return;
}
// Update the [ToolInfo] object for the current tool
toolInfo.lastRun = clock.now();
toolInfo.versionNumber = newVersionNumber;
}
/// Method responsible for reading in the config file stored on
/// user's machine and parsing out the following: all the tools that
/// have been logged in the file, the dates they were last run, and
/// determining if telemetry is enabled by parsing the file
void parseConfig() {
// Begin with the assumption that telemetry is always enabled
_telemetryEnabled = true;
// Read the configuration file as a string and run the two regex patterns
// on it to get information around which tools have been parsed and whether
// or not telemetry has been disabled by the user
final String configString = configFile.readAsStringSync();
// Collect the tools logged in the configuration file
toolRegex.allMatches(configString).forEach((RegExpMatch element) {
// Extract the information relevant for the [ToolInfo] class
final String tool = element.group(1) as String;
final DateTime lastRun = DateTime.parse(element.group(2) as String);
final int versionNumber = int.parse(element.group(3) as String);
// Initialize an instance of the [ToolInfo] class to store
// in the [parsedTools] map object
parsedTools[tool] = ToolInfo(
lastRun: lastRun,
versionNumber: versionNumber,
);
});
// Check for lines signaling that the user has disabled analytics,
// if multiple lines are found, the more conservative value will be used
telemetryFlagRegex.allMatches(configString).forEach((RegExpMatch element) {
// Conditional for recording telemetry as being disabled
if (element.group(1) == '0') {
_telemetryEnabled = false;
}
});
}
/// This will reset the configuration file and clear the
/// [parsedTools] map and trigger parsing the config again
void resetConfig() {
initializer.run(forceReset: true);
parsedTools.clear();
parseConfig();
}
/// Disables the reporting capabilities if false is passed
Future<void> setTelemetry(bool reportingBool) async {
final String flag = reportingBool ? '1' : '0';
final String configString = await configFile.readAsString();
final Iterable<RegExpMatch> matches =
telemetryFlagRegex.allMatches(configString);
// If there isn't exactly one match for the reporting flag, that suggests the
// file has been altered and needs to be reset
if (matches.length != 1) {
resetConfig();
return;
}
final String newTelemetryString = 'reporting=$flag';
final String newConfigString =
configString.replaceAll(telemetryFlagRegex, newTelemetryString);
await configFile.writeAsString(newConfigString);
configFileLastModified = configFile.lastModifiedSync();
_telemetryEnabled = reportingBool;
}
}
class ToolInfo {
DateTime lastRun;
int versionNumber;
ToolInfo({
required this.lastRun,
required this.versionNumber,
});
@override
String toString() {
return json.encode(<String, Object?>{
'lastRun': DateFormat('yyyy-MM-dd').format(lastRun),
'versionNumber': versionNumber,
});
}
}