// 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:convert/convert.dart'; import 'package:file/file.dart'; import 'initializer.dart'; import 'utils.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: /// ```text /// flutter-tool=2022-10-26,1 /// devtools=2022-11-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. static RegExp telemetryFlagRegex = RegExp( telemetryFlagPattern, multiLine: true, ); static RegExp toolRegex = RegExp(toolPattern, multiLine: true); final Directory homeDirectory; final File configFile; final Map parsedTools = {}; DateTime configFileLastModified; /// Reporting enabled unless specified by user bool _telemetryEnabled = true; ConfigHandler({required this.homeDirectory, required this.configFile}) : 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, required int versionNumber}) { // Create the new instance of [ToolInfo] to be added // to the [parsedTools] map parsedTools[tool] = ToolInfo( lastRun: clock.now(), versionNumber: versionNumber, ); // New string to be appended to the bottom of the configuration file // with a newline character for new tools to be added var newTool = '$tool=$dateStamp,$versionNumber\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, required int newVersionNumber, }) { 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 configString = configFile.readAsStringSync(); final pattern = '^($tool)=([0-9]{4}-[0-9]{2}-[0-9]{2}),([0-9]+)\$'; final regex = RegExp(pattern, multiLine: true); final 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; } // Construct the new tool line for the config line and replace it // in the original config string to prep for writing back out final newToolString = '$tool=$dateStamp,$newVersionNumber'; final newConfigString = configString.replaceAll(regex, newToolString); configFile.writeAsStringSync(newConfigString); final 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 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 tool = element.group(1) as String; final lastRun = DateTime.parse(element.group(2) as String); final 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() { createConfigFile(configFile: configFile, homeDirectory: homeDirectory); parsedTools.clear(); parseConfig(); } /// Disables the reporting capabilities if [reportingBool] is set to `false`. Future setTelemetry(bool reportingBool) async { final flag = reportingBool ? '1' : '0'; final configString = await configFile.readAsString(); final 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 newTelemetryString = 'reporting=$flag'; final 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({ 'lastRun': FixedDateTimeFormatter('YYYY-MM-DD').encode(lastRun), 'versionNumber': versionNumber, }); } }