Devon Carew | 67046f9 | 2016-02-20 22:00:11 -0800 | [diff] [blame] | 1 | // Copyright 2016 The Chromium 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'; |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 6 | import 'dart:math' show Random, max; |
Devon Carew | 4e10bf5 | 2016-02-26 17:53:41 -0800 | [diff] [blame] | 7 | |
Devon Carew | 20e83e3 | 2017-04-11 07:57:18 -0700 | [diff] [blame] | 8 | import 'package:intl/intl.dart'; |
Devon Carew | 4e10bf5 | 2016-02-26 17:53:41 -0800 | [diff] [blame] | 9 | |
Jonah Williams | 91fd89e | 2019-01-25 16:16:26 -0800 | [diff] [blame] | 10 | import '../convert.dart'; |
Devon Carew | a7c3bf4 | 2017-08-01 15:29:54 -0700 | [diff] [blame] | 11 | import '../globals.dart'; |
xster | 66ed8de | 2017-04-27 15:28:15 -0700 | [diff] [blame] | 12 | import 'context.dart'; |
Todd Volkert | 8bb2703 | 2017-01-06 16:51:44 -0800 | [diff] [blame] | 13 | import 'file_system.dart'; |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 14 | import 'io.dart' as io; |
Todd Volkert | 417c2f2 | 2017-01-25 16:06:41 -0800 | [diff] [blame] | 15 | import 'platform.dart'; |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 16 | import 'terminal.dart'; |
Todd Volkert | 8bb2703 | 2017-01-06 16:51:44 -0800 | [diff] [blame] | 17 | |
Alexandre Ardhuin | eda03e2 | 2018-08-02 12:02:32 +0200 | [diff] [blame] | 18 | const BotDetector _kBotDetector = BotDetector(); |
jcollins-g | ca67701 | 2018-02-21 09:54:07 -0800 | [diff] [blame] | 19 | |
| 20 | class BotDetector { |
| 21 | const BotDetector(); |
| 22 | |
| 23 | bool get isRunningOnBot { |
Todd Volkert | 1b3fc53 | 2019-06-10 16:32:08 -0700 | [diff] [blame] | 24 | if ( |
| 25 | // Explicitly stated to not be a bot. |
| 26 | platform.environment['BOT'] == 'false' |
| 27 | |
| 28 | // Set by the IDEs to the IDE name, so a strong signal that this is not a bot. |
| 29 | || platform.environment.containsKey('FLUTTER_HOST') |
| 30 | ) { |
| 31 | return false; |
| 32 | } |
| 33 | |
| 34 | return platform.environment['BOT'] == 'true' |
| 35 | |
Greg Spencer | e60087a | 2018-08-07 13:41:33 -0700 | [diff] [blame] | 36 | // https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables |
Ian Hickson | d1cc8b6 | 2018-06-14 12:30:20 -0700 | [diff] [blame] | 37 | || platform.environment['TRAVIS'] == 'true' |
| 38 | || platform.environment['CONTINUOUS_INTEGRATION'] == 'true' |
| 39 | || platform.environment.containsKey('CI') // Travis and AppVeyor |
jcollins-g | ca67701 | 2018-02-21 09:54:07 -0800 | [diff] [blame] | 40 | |
Greg Spencer | e60087a | 2018-08-07 13:41:33 -0700 | [diff] [blame] | 41 | // https://www.appveyor.com/docs/environment-variables/ |
Ian Hickson | d1cc8b6 | 2018-06-14 12:30:20 -0700 | [diff] [blame] | 42 | || platform.environment.containsKey('APPVEYOR') |
jcollins-g | ca67701 | 2018-02-21 09:54:07 -0800 | [diff] [blame] | 43 | |
Greg Spencer | e60087a | 2018-08-07 13:41:33 -0700 | [diff] [blame] | 44 | // https://cirrus-ci.org/guide/writing-tasks/#environment-variables |
| 45 | || platform.environment.containsKey('CIRRUS_CI') |
| 46 | |
| 47 | // https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html |
Zachary Anderson | fa65ddf | 2019-07-16 09:48:49 -0700 | [diff] [blame^] | 48 | || (platform.environment.containsKey('AWS_REGION') && |
| 49 | platform.environment.containsKey('CODEBUILD_INITIATOR')) |
jcollins-g | ca67701 | 2018-02-21 09:54:07 -0800 | [diff] [blame] | 50 | |
Greg Spencer | e60087a | 2018-08-07 13:41:33 -0700 | [diff] [blame] | 51 | // https://wiki.jenkins.io/display/JENKINS/Building+a+software+project#Buildingasoftwareproject-belowJenkinsSetEnvironmentVariables |
Ian Hickson | d1cc8b6 | 2018-06-14 12:30:20 -0700 | [diff] [blame] | 52 | || platform.environment.containsKey('JENKINS_URL') |
jcollins-g | ca67701 | 2018-02-21 09:54:07 -0800 | [diff] [blame] | 53 | |
Greg Spencer | e60087a | 2018-08-07 13:41:33 -0700 | [diff] [blame] | 54 | // Properties on Flutter's Chrome Infra bots. |
Ian Hickson | d1cc8b6 | 2018-06-14 12:30:20 -0700 | [diff] [blame] | 55 | || platform.environment['CHROME_HEADLESS'] == '1' |
Todd Volkert | 1b3fc53 | 2019-06-10 16:32:08 -0700 | [diff] [blame] | 56 | || platform.environment.containsKey('BUILDBOT_BUILDERNAME') |
| 57 | || platform.environment.containsKey('SWARMING_TASK_ID'); |
jcollins-g | ca67701 | 2018-02-21 09:54:07 -0800 | [diff] [blame] | 58 | } |
| 59 | } |
| 60 | |
Devon Carew | bd564a0 | 2016-05-05 19:51:22 -0700 | [diff] [blame] | 61 | bool get isRunningOnBot { |
Jonah Williams | 0acd3e6 | 2019-04-25 15:51:08 -0700 | [diff] [blame] | 62 | final BotDetector botDetector = context.get<BotDetector>() ?? _kBotDetector; |
Todd Volkert | 37ffb2d | 2018-02-21 15:01:46 -0800 | [diff] [blame] | 63 | return botDetector.isRunningOnBot; |
Devon Carew | bd564a0 | 2016-05-05 19:51:22 -0700 | [diff] [blame] | 64 | } |
| 65 | |
Devon Carew | 4c56919 | 2016-03-02 11:22:19 -0800 | [diff] [blame] | 66 | /// Convert `foo_bar` to `fooBar`. |
| 67 | String camelCase(String str) { |
| 68 | int index = str.indexOf('_'); |
| 69 | while (index != -1 && index < str.length - 2) { |
| 70 | str = str.substring(0, index) + |
| 71 | str.substring(index + 1, index + 2).toUpperCase() + |
| 72 | str.substring(index + 2); |
| 73 | index = str.indexOf('_'); |
| 74 | } |
| 75 | return str; |
| 76 | } |
| 77 | |
Alexandre Ardhuin | d927c93 | 2018-09-12 08:29:29 +0200 | [diff] [blame] | 78 | final RegExp _upperRegex = RegExp(r'[A-Z]'); |
Stanislav Baranov | 393f927 | 2018-08-16 08:43:41 -0700 | [diff] [blame] | 79 | |
| 80 | /// Convert `fooBar` to `foo_bar`. |
Alexandre Ardhuin | 5169ab5 | 2019-02-21 09:27:07 +0100 | [diff] [blame] | 81 | String snakeCase(String str, [ String sep = '_' ]) { |
Stanislav Baranov | 393f927 | 2018-08-16 08:43:41 -0700 | [diff] [blame] | 82 | return str.replaceAllMapped(_upperRegex, |
| 83 | (Match m) => '${m.start == 0 ? '' : sep}${m[0].toLowerCase()}'); |
| 84 | } |
| 85 | |
Devon Carew | 9cfa966 | 2016-05-26 09:14:51 -0700 | [diff] [blame] | 86 | String toTitleCase(String str) { |
| 87 | if (str.isEmpty) |
| 88 | return str; |
| 89 | return str.substring(0, 1).toUpperCase() + str.substring(1); |
| 90 | } |
| 91 | |
Devon Carew | 4c56919 | 2016-03-02 11:22:19 -0800 | [diff] [blame] | 92 | /// Return the plural of the given word (`cat(s)`). |
| 93 | String pluralize(String word, int count) => count == 1 ? word : word + 's'; |
| 94 | |
Devon Carew | f132aca | 2016-04-15 21:08:03 -0700 | [diff] [blame] | 95 | /// Return the name of an enum item. |
| 96 | String getEnumName(dynamic enumItem) { |
Chris Bracken | 7a09316 | 2017-03-03 17:50:46 -0800 | [diff] [blame] | 97 | final String name = '$enumItem'; |
| 98 | final int index = name.indexOf('.'); |
Devon Carew | f132aca | 2016-04-15 21:08:03 -0700 | [diff] [blame] | 99 | return index == -1 ? name : name.substring(index + 1); |
| 100 | } |
| 101 | |
Devon Carew | 15b9e1d | 2016-03-25 16:04:22 -0700 | [diff] [blame] | 102 | File getUniqueFile(Directory dir, String baseName, String ext) { |
Chris Bracken | 7a09316 | 2017-03-03 17:50:46 -0800 | [diff] [blame] | 103 | final FileSystem fs = dir.fileSystem; |
Devon Carew | 15b9e1d | 2016-03-25 16:04:22 -0700 | [diff] [blame] | 104 | int i = 1; |
| 105 | |
| 106 | while (true) { |
Chris Bracken | 7a09316 | 2017-03-03 17:50:46 -0800 | [diff] [blame] | 107 | final String name = '${baseName}_${i.toString().padLeft(2, '0')}.$ext'; |
| 108 | final File file = fs.file(fs.path.join(dir.path, name)); |
Devon Carew | 15b9e1d | 2016-03-25 16:04:22 -0700 | [diff] [blame] | 109 | if (!file.existsSync()) |
| 110 | return file; |
| 111 | i++; |
| 112 | } |
| 113 | } |
| 114 | |
Devon Carew | 40c0d6e | 2016-05-12 18:15:23 -0700 | [diff] [blame] | 115 | String toPrettyJson(Object jsonable) { |
Alexandre Ardhuin | 2888139 | 2017-02-20 23:07:16 +0100 | [diff] [blame] | 116 | return const JsonEncoder.withIndent(' ').convert(jsonable) + '\n'; |
Devon Carew | 40c0d6e | 2016-05-12 18:15:23 -0700 | [diff] [blame] | 117 | } |
| 118 | |
Devon Carew | 7c47837 | 2016-05-29 15:07:41 -0700 | [diff] [blame] | 119 | /// Return a String - with units - for the size in MB of the given number of bytes. |
| 120 | String getSizeAsMB(int bytesLength) { |
| 121 | return '${(bytesLength / (1024 * 1024)).toStringAsFixed(1)}MB'; |
| 122 | } |
| 123 | |
Alexandre Ardhuin | d927c93 | 2018-09-12 08:29:29 +0200 | [diff] [blame] | 124 | final NumberFormat kSecondsFormat = NumberFormat('0.0'); |
| 125 | final NumberFormat kMillisecondsFormat = NumberFormat.decimalPattern(); |
Devon Carew | 20e83e3 | 2017-04-11 07:57:18 -0700 | [diff] [blame] | 126 | |
| 127 | String getElapsedAsSeconds(Duration duration) { |
Jason Simmons | 466d154 | 2018-03-12 11:06:32 -0700 | [diff] [blame] | 128 | final double seconds = duration.inMilliseconds / Duration.millisecondsPerSecond; |
Devon Carew | 20e83e3 | 2017-04-11 07:57:18 -0700 | [diff] [blame] | 129 | return '${kSecondsFormat.format(seconds)}s'; |
| 130 | } |
| 131 | |
| 132 | String getElapsedAsMilliseconds(Duration duration) { |
| 133 | return '${kMillisecondsFormat.format(duration.inMilliseconds)}ms'; |
| 134 | } |
John McCutchan | b314fa5 | 2016-08-10 14:02:44 -0700 | [diff] [blame] | 135 | |
Devon Carew | 3ba1713 | 2016-06-07 12:13:35 -0700 | [diff] [blame] | 136 | /// Return a relative path if [fullPath] is contained by the cwd, else return an |
| 137 | /// absolute path. |
| 138 | String getDisplayPath(String fullPath) { |
Chris Bracken | 7a09316 | 2017-03-03 17:50:46 -0800 | [diff] [blame] | 139 | final String cwd = fs.currentDirectory.path + fs.path.separator; |
Alexandre Ardhuin | c02b6a8 | 2018-02-02 23:27:29 +0100 | [diff] [blame] | 140 | return fullPath.startsWith(cwd) ? fullPath.substring(cwd.length) : fullPath; |
Devon Carew | 3ba1713 | 2016-06-07 12:13:35 -0700 | [diff] [blame] | 141 | } |
| 142 | |
Devon Carew | 67046f9 | 2016-02-20 22:00:11 -0800 | [diff] [blame] | 143 | /// A class to maintain a list of items, fire events when items are added or |
| 144 | /// removed, and calculate a diff of changes when a new list of items is |
| 145 | /// available. |
| 146 | class ItemListNotifier<T> { |
| 147 | ItemListNotifier() { |
Phil Quitslund | 802eca2 | 2019-03-06 11:05:16 -0800 | [diff] [blame] | 148 | _items = <T>{}; |
Devon Carew | 67046f9 | 2016-02-20 22:00:11 -0800 | [diff] [blame] | 149 | } |
| 150 | |
| 151 | ItemListNotifier.from(List<T> items) { |
Alexandre Ardhuin | d927c93 | 2018-09-12 08:29:29 +0200 | [diff] [blame] | 152 | _items = Set<T>.from(items); |
Devon Carew | 67046f9 | 2016-02-20 22:00:11 -0800 | [diff] [blame] | 153 | } |
| 154 | |
| 155 | Set<T> _items; |
| 156 | |
Alexandre Ardhuin | d927c93 | 2018-09-12 08:29:29 +0200 | [diff] [blame] | 157 | final StreamController<T> _addedController = StreamController<T>.broadcast(); |
| 158 | final StreamController<T> _removedController = StreamController<T>.broadcast(); |
Devon Carew | 67046f9 | 2016-02-20 22:00:11 -0800 | [diff] [blame] | 159 | |
| 160 | Stream<T> get onAdded => _addedController.stream; |
| 161 | Stream<T> get onRemoved => _removedController.stream; |
| 162 | |
| 163 | List<T> get items => _items.toList(); |
| 164 | |
| 165 | void updateWithNewList(List<T> updatedList) { |
Alexandre Ardhuin | d927c93 | 2018-09-12 08:29:29 +0200 | [diff] [blame] | 166 | final Set<T> updatedSet = Set<T>.from(updatedList); |
Devon Carew | 67046f9 | 2016-02-20 22:00:11 -0800 | [diff] [blame] | 167 | |
Chris Bracken | 7a09316 | 2017-03-03 17:50:46 -0800 | [diff] [blame] | 168 | final Set<T> addedItems = updatedSet.difference(_items); |
| 169 | final Set<T> removedItems = _items.difference(updatedSet); |
Devon Carew | 67046f9 | 2016-02-20 22:00:11 -0800 | [diff] [blame] | 170 | |
| 171 | _items = updatedSet; |
| 172 | |
Alexandre Ardhuin | 2836600 | 2017-10-25 08:25:44 +0200 | [diff] [blame] | 173 | addedItems.forEach(_addedController.add); |
| 174 | removedItems.forEach(_removedController.add); |
Devon Carew | 67046f9 | 2016-02-20 22:00:11 -0800 | [diff] [blame] | 175 | } |
| 176 | |
| 177 | /// Close the streams. |
| 178 | void dispose() { |
| 179 | _addedController.close(); |
| 180 | _removedController.close(); |
| 181 | } |
| 182 | } |
Devon Carew | 57b76a0 | 2016-07-19 20:00:02 -0700 | [diff] [blame] | 183 | |
| 184 | class SettingsFile { |
Jakob Andersen | 6b54137 | 2017-05-05 14:53:51 +0200 | [diff] [blame] | 185 | SettingsFile(); |
| 186 | |
Devon Carew | 57b76a0 | 2016-07-19 20:00:02 -0700 | [diff] [blame] | 187 | SettingsFile.parse(String contents) { |
| 188 | for (String line in contents.split('\n')) { |
| 189 | line = line.trim(); |
| 190 | if (line.startsWith('#') || line.isEmpty) |
| 191 | continue; |
Chris Bracken | 7a09316 | 2017-03-03 17:50:46 -0800 | [diff] [blame] | 192 | final int index = line.indexOf('='); |
Devon Carew | 57b76a0 | 2016-07-19 20:00:02 -0700 | [diff] [blame] | 193 | if (index != -1) |
| 194 | values[line.substring(0, index)] = line.substring(index + 1); |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | factory SettingsFile.parseFromFile(File file) { |
Alexandre Ardhuin | d927c93 | 2018-09-12 08:29:29 +0200 | [diff] [blame] | 199 | return SettingsFile.parse(file.readAsStringSync()); |
Devon Carew | 57b76a0 | 2016-07-19 20:00:02 -0700 | [diff] [blame] | 200 | } |
| 201 | |
| 202 | final Map<String, String> values = <String, String>{}; |
| 203 | |
| 204 | void writeContents(File file) { |
Stanislav Baranov | f6c1476 | 2018-12-18 10:39:27 -0800 | [diff] [blame] | 205 | file.parent.createSync(recursive: true); |
Alexandre Ardhuin | f62afdc | 2018-10-01 21:29:08 +0200 | [diff] [blame] | 206 | file.writeAsStringSync(values.keys.map<String>((String key) { |
Devon Carew | 57b76a0 | 2016-07-19 20:00:02 -0700 | [diff] [blame] | 207 | return '$key=${values[key]}'; |
| 208 | }).join('\n')); |
| 209 | } |
| 210 | } |
Devon Carew | d9bbd2f | 2016-09-28 11:18:05 -0700 | [diff] [blame] | 211 | |
| 212 | /// A UUID generator. This will generate unique IDs in the format: |
| 213 | /// |
| 214 | /// f47ac10b-58cc-4372-a567-0e02b2c3d479 |
| 215 | /// |
Greg Spencer | 0259be9 | 2017-11-17 10:05:21 -0800 | [diff] [blame] | 216 | /// The generated UUIDs are 128 bit numbers encoded in a specific string format. |
Devon Carew | d9bbd2f | 2016-09-28 11:18:05 -0700 | [diff] [blame] | 217 | /// |
| 218 | /// For more information, see |
| 219 | /// http://en.wikipedia.org/wiki/Universally_unique_identifier. |
| 220 | class Uuid { |
Alexandre Ardhuin | d927c93 | 2018-09-12 08:29:29 +0200 | [diff] [blame] | 221 | final Random _random = Random(); |
Devon Carew | d9bbd2f | 2016-09-28 11:18:05 -0700 | [diff] [blame] | 222 | |
Greg Spencer | 0259be9 | 2017-11-17 10:05:21 -0800 | [diff] [blame] | 223 | /// Generate a version 4 (random) UUID. This is a UUID scheme that only uses |
| 224 | /// random numbers as the source of the generated UUID. |
Devon Carew | d9bbd2f | 2016-09-28 11:18:05 -0700 | [diff] [blame] | 225 | String generateV4() { |
| 226 | // Generate xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx / 8-4-4-4-12. |
Chris Bracken | 7a09316 | 2017-03-03 17:50:46 -0800 | [diff] [blame] | 227 | final int special = 8 + _random.nextInt(4); |
Devon Carew | d9bbd2f | 2016-09-28 11:18:05 -0700 | [diff] [blame] | 228 | |
| 229 | return |
| 230 | '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}-' |
| 231 | '${_bitsDigits(16, 4)}-' |
| 232 | '4${_bitsDigits(12, 3)}-' |
Alexandre Ardhuin | c02b6a8 | 2018-02-02 23:27:29 +0100 | [diff] [blame] | 233 | '${_printDigits(special, 1)}${_bitsDigits(12, 3)}-' |
Devon Carew | d9bbd2f | 2016-09-28 11:18:05 -0700 | [diff] [blame] | 234 | '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}'; |
| 235 | } |
| 236 | |
| 237 | String _bitsDigits(int bitCount, int digitCount) => |
| 238 | _printDigits(_generateBits(bitCount), digitCount); |
| 239 | |
| 240 | int _generateBits(int bitCount) => _random.nextInt(1 << bitCount); |
| 241 | |
| 242 | String _printDigits(int value, int count) => |
| 243 | value.toRadixString(16).padLeft(count, '0'); |
| 244 | } |
xster | 66ed8de | 2017-04-27 15:28:15 -0700 | [diff] [blame] | 245 | |
Devon Carew | 9d9836f | 2018-07-09 12:22:46 -0700 | [diff] [blame] | 246 | /// Given a data structure which is a Map of String to dynamic values, return |
| 247 | /// the same structure (`Map<String, dynamic>`) with the correct runtime types. |
| 248 | Map<String, dynamic> castStringKeyedMap(dynamic untyped) { |
| 249 | final Map<dynamic, dynamic> map = untyped; |
| 250 | return map.cast<String, dynamic>(); |
| 251 | } |
| 252 | |
Alexandre Ardhuin | 2d3ff10 | 2018-10-05 07:54:56 +0200 | [diff] [blame] | 253 | typedef AsyncCallback = Future<void> Function(); |
Devon Carew | a7c3bf4 | 2017-08-01 15:29:54 -0700 | [diff] [blame] | 254 | |
| 255 | /// A [Timer] inspired class that: |
| 256 | /// - has a different initial value for the first callback delay |
| 257 | /// - waits for a callback to be complete before it starts the next timer |
| 258 | class Poller { |
Alexandre Ardhuin | 09276be | 2018-06-05 08:50:40 +0200 | [diff] [blame] | 259 | Poller(this.callback, this.pollingInterval, { this.initialDelay = Duration.zero }) { |
Alexandre Ardhuin | 2d3ff10 | 2018-10-05 07:54:56 +0200 | [diff] [blame] | 260 | Future<void>.delayed(initialDelay, _handleCallback); |
Devon Carew | a7c3bf4 | 2017-08-01 15:29:54 -0700 | [diff] [blame] | 261 | } |
| 262 | |
| 263 | final AsyncCallback callback; |
| 264 | final Duration initialDelay; |
| 265 | final Duration pollingInterval; |
| 266 | |
Chris Bracken | 156b422 | 2019-05-24 19:13:02 -0700 | [diff] [blame] | 267 | bool _canceled = false; |
Devon Carew | a7c3bf4 | 2017-08-01 15:29:54 -0700 | [diff] [blame] | 268 | Timer _timer; |
| 269 | |
Alexandre Ardhuin | 2d3ff10 | 2018-10-05 07:54:56 +0200 | [diff] [blame] | 270 | Future<void> _handleCallback() async { |
Chris Bracken | 156b422 | 2019-05-24 19:13:02 -0700 | [diff] [blame] | 271 | if (_canceled) |
Devon Carew | a7c3bf4 | 2017-08-01 15:29:54 -0700 | [diff] [blame] | 272 | return; |
| 273 | |
| 274 | try { |
| 275 | await callback(); |
| 276 | } catch (error) { |
| 277 | printTrace('Error from poller: $error'); |
| 278 | } |
| 279 | |
Chris Bracken | 156b422 | 2019-05-24 19:13:02 -0700 | [diff] [blame] | 280 | if (!_canceled) |
Alexandre Ardhuin | d927c93 | 2018-09-12 08:29:29 +0200 | [diff] [blame] | 281 | _timer = Timer(pollingInterval, _handleCallback); |
Devon Carew | a7c3bf4 | 2017-08-01 15:29:54 -0700 | [diff] [blame] | 282 | } |
| 283 | |
| 284 | /// Cancels the poller. |
| 285 | void cancel() { |
Chris Bracken | 156b422 | 2019-05-24 19:13:02 -0700 | [diff] [blame] | 286 | _canceled = true; |
Devon Carew | a7c3bf4 | 2017-08-01 15:29:54 -0700 | [diff] [blame] | 287 | _timer?.cancel(); |
| 288 | _timer = null; |
| 289 | } |
| 290 | } |
xster | 987b205 | 2017-09-20 16:25:16 -0700 | [diff] [blame] | 291 | |
| 292 | /// Returns a [Future] that completes when all given [Future]s complete. |
| 293 | /// |
| 294 | /// Uses [Future.wait] but removes null elements from the provided |
| 295 | /// `futures` iterable first. |
| 296 | /// |
| 297 | /// The returned [Future<List>] will be shorter than the given `futures` if |
| 298 | /// it contains nulls. |
| 299 | Future<List<T>> waitGroup<T>(Iterable<Future<T>> futures) { |
| 300 | return Future.wait<T>(futures.where((Future<T> future) => future != null)); |
| 301 | } |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 302 | /// The terminal width used by the [wrapText] function if there is no terminal |
| 303 | /// attached to [io.Stdio], --wrap is on, and --wrap-columns was not specified. |
| 304 | const int kDefaultTerminalColumns = 100; |
| 305 | |
| 306 | /// Smallest column that will be used for text wrapping. If the requested column |
| 307 | /// width is smaller than this, then this is what will be used. |
| 308 | const int kMinColumnWidth = 10; |
| 309 | |
| 310 | /// Wraps a block of text into lines no longer than [columnWidth]. |
| 311 | /// |
| 312 | /// Tries to split at whitespace, but if that's not good enough to keep it |
Greg Spencer | 4559ae1 | 2018-10-30 16:00:50 -0700 | [diff] [blame] | 313 | /// under the limit, then it splits in the middle of a word. If [columnWidth] is |
| 314 | /// smaller than 10 columns, will wrap at 10 columns. |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 315 | /// |
| 316 | /// Preserves indentation (leading whitespace) for each line (delimited by '\n') |
| 317 | /// in the input, and will indent wrapped lines that same amount, adding |
| 318 | /// [indent] spaces in addition to any existing indent. |
| 319 | /// |
| 320 | /// If [hangingIndent] is supplied, then that many additional spaces will be |
| 321 | /// added to each line, except for the first line. The [hangingIndent] is added |
| 322 | /// to the specified [indent], if any. This is useful for wrapping |
| 323 | /// text with a heading prefix (e.g. "Usage: "): |
| 324 | /// |
| 325 | /// ```dart |
| 326 | /// String prefix = "Usage: "; |
| 327 | /// print(prefix + wrapText(invocation, indent: 2, hangingIndent: prefix.length, columnWidth: 40)); |
| 328 | /// ``` |
| 329 | /// |
| 330 | /// yields: |
| 331 | /// ``` |
| 332 | /// Usage: app main_command <subcommand> |
| 333 | /// [arguments] |
| 334 | /// ``` |
| 335 | /// |
| 336 | /// If [columnWidth] is not specified, then the column width will be the |
| 337 | /// [outputPreferences.wrapColumn], which is set with the --wrap-column option. |
| 338 | /// |
| 339 | /// If [outputPreferences.wrapText] is false, then the text will be returned |
Greg Spencer | 4559ae1 | 2018-10-30 16:00:50 -0700 | [diff] [blame] | 340 | /// unchanged. If [shouldWrap] is specified, then it overrides the |
| 341 | /// [outputPreferences.wrapText] setting. |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 342 | /// |
| 343 | /// The [indent] and [hangingIndent] must be smaller than [columnWidth] when |
| 344 | /// added together. |
Alexandre Ardhuin | 5169ab5 | 2019-02-21 09:27:07 +0100 | [diff] [blame] | 345 | String wrapText(String text, { int columnWidth, int hangingIndent, int indent, bool shouldWrap }) { |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 346 | if (text == null || text.isEmpty) { |
| 347 | return ''; |
| 348 | } |
| 349 | indent ??= 0; |
| 350 | columnWidth ??= outputPreferences.wrapColumn; |
| 351 | columnWidth -= indent; |
| 352 | assert(columnWidth >= 0); |
| 353 | |
| 354 | hangingIndent ??= 0; |
| 355 | final List<String> splitText = text.split('\n'); |
| 356 | final List<String> result = <String>[]; |
| 357 | for (String line in splitText) { |
| 358 | String trimmedText = line.trimLeft(); |
| 359 | final String leadingWhitespace = line.substring(0, line.length - trimmedText.length); |
| 360 | List<String> notIndented; |
| 361 | if (hangingIndent != 0) { |
| 362 | // When we have a hanging indent, we want to wrap the first line at one |
| 363 | // width, and the rest at another (offset by hangingIndent), so we wrap |
| 364 | // them twice and recombine. |
| 365 | final List<String> firstLineWrap = _wrapTextAsLines( |
| 366 | trimmedText, |
| 367 | columnWidth: columnWidth - leadingWhitespace.length, |
Greg Spencer | 4559ae1 | 2018-10-30 16:00:50 -0700 | [diff] [blame] | 368 | shouldWrap: shouldWrap, |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 369 | ); |
| 370 | notIndented = <String>[firstLineWrap.removeAt(0)]; |
| 371 | trimmedText = trimmedText.substring(notIndented[0].length).trimLeft(); |
| 372 | if (firstLineWrap.isNotEmpty) { |
| 373 | notIndented.addAll(_wrapTextAsLines( |
| 374 | trimmedText, |
| 375 | columnWidth: columnWidth - leadingWhitespace.length - hangingIndent, |
Greg Spencer | 4559ae1 | 2018-10-30 16:00:50 -0700 | [diff] [blame] | 376 | shouldWrap: shouldWrap, |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 377 | )); |
| 378 | } |
| 379 | } else { |
| 380 | notIndented = _wrapTextAsLines( |
| 381 | trimmedText, |
| 382 | columnWidth: columnWidth - leadingWhitespace.length, |
Greg Spencer | 4559ae1 | 2018-10-30 16:00:50 -0700 | [diff] [blame] | 383 | shouldWrap: shouldWrap, |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 384 | ); |
| 385 | } |
| 386 | String hangingIndentString; |
| 387 | final String indentString = ' ' * indent; |
| 388 | result.addAll(notIndented.map( |
| 389 | (String line) { |
| 390 | // Don't return any lines with just whitespace on them. |
| 391 | if (line.isEmpty) { |
| 392 | return ''; |
| 393 | } |
| 394 | final String result = '$indentString${hangingIndentString ?? ''}$leadingWhitespace$line'; |
| 395 | hangingIndentString ??= ' ' * hangingIndent; |
| 396 | return result; |
| 397 | }, |
| 398 | )); |
| 399 | } |
| 400 | return result.join('\n'); |
| 401 | } |
| 402 | |
Danny Tuppeny | aa83f77 | 2018-10-24 07:21:36 +0100 | [diff] [blame] | 403 | void writePidFile(String pidFile) { |
| 404 | if (pidFile != null) { |
| 405 | // Write our pid to the file. |
| 406 | fs.file(pidFile).writeAsStringSync(io.pid.toString()); |
| 407 | } |
| 408 | } |
| 409 | |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 410 | // Used to represent a run of ANSI control sequences next to a visible |
| 411 | // character. |
| 412 | class _AnsiRun { |
| 413 | _AnsiRun(this.original, this.character); |
| 414 | |
| 415 | String original; |
| 416 | String character; |
| 417 | } |
| 418 | |
| 419 | /// Wraps a block of text into lines no longer than [columnWidth], starting at the |
| 420 | /// [start] column, and returning the result as a list of strings. |
| 421 | /// |
| 422 | /// Tries to split at whitespace, but if that's not good enough to keep it |
| 423 | /// under the limit, then splits in the middle of a word. Preserves embedded |
| 424 | /// newlines, but not indentation (it trims whitespace from each line). |
| 425 | /// |
| 426 | /// If [columnWidth] is not specified, then the column width will be the width of the |
| 427 | /// terminal window by default. If the stdout is not a terminal window, then the |
| 428 | /// default will be [outputPreferences.wrapColumn]. |
| 429 | /// |
| 430 | /// If [outputPreferences.wrapText] is false, then the text will be returned |
Greg Spencer | 4559ae1 | 2018-10-30 16:00:50 -0700 | [diff] [blame] | 431 | /// simply split at the newlines, but not wrapped. If [shouldWrap] is specified, |
| 432 | /// then it overrides the [outputPreferences.wrapText] setting. |
Alexandre Ardhuin | 5169ab5 | 2019-02-21 09:27:07 +0100 | [diff] [blame] | 433 | List<String> _wrapTextAsLines(String text, { int start = 0, int columnWidth, bool shouldWrap }) { |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 434 | if (text == null || text.isEmpty) { |
| 435 | return <String>['']; |
| 436 | } |
| 437 | assert(columnWidth != null); |
| 438 | assert(columnWidth >= 0); |
| 439 | assert(start >= 0); |
Greg Spencer | 4559ae1 | 2018-10-30 16:00:50 -0700 | [diff] [blame] | 440 | shouldWrap ??= outputPreferences.wrapText; |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 441 | |
| 442 | /// Returns true if the code unit at [index] in [text] is a whitespace |
| 443 | /// character. |
| 444 | /// |
| 445 | /// Based on: https://en.wikipedia.org/wiki/Whitespace_character#Unicode |
| 446 | bool isWhitespace(_AnsiRun run) { |
| 447 | final int rune = run.character.isNotEmpty ? run.character.codeUnitAt(0) : 0x0; |
| 448 | return rune >= 0x0009 && rune <= 0x000D || |
| 449 | rune == 0x0020 || |
| 450 | rune == 0x0085 || |
| 451 | rune == 0x1680 || |
| 452 | rune == 0x180E || |
| 453 | rune >= 0x2000 && rune <= 0x200A || |
| 454 | rune == 0x2028 || |
| 455 | rune == 0x2029 || |
| 456 | rune == 0x202F || |
| 457 | rune == 0x205F || |
| 458 | rune == 0x3000 || |
| 459 | rune == 0xFEFF; |
| 460 | } |
| 461 | |
| 462 | // Splits a string so that the resulting list has the same number of elements |
| 463 | // as there are visible characters in the string, but elements may include one |
| 464 | // or more adjacent ANSI sequences. Joining the list elements again will |
| 465 | // reconstitute the original string. This is useful for manipulating "visible" |
| 466 | // characters in the presence of ANSI control codes. |
| 467 | List<_AnsiRun> splitWithCodes(String input) { |
| 468 | final RegExp characterOrCode = RegExp('(\u001b\[[0-9;]*m|.)', multiLine: true); |
| 469 | List<_AnsiRun> result = <_AnsiRun>[]; |
| 470 | final StringBuffer current = StringBuffer(); |
| 471 | for (Match match in characterOrCode.allMatches(input)) { |
| 472 | current.write(match[0]); |
| 473 | if (match[0].length < 4) { |
| 474 | // This is a regular character, write it out. |
| 475 | result.add(_AnsiRun(current.toString(), match[0])); |
| 476 | current.clear(); |
| 477 | } |
| 478 | } |
| 479 | // If there's something accumulated, then it must be an ANSI sequence, so |
| 480 | // add it to the end of the last entry so that we don't lose it. |
| 481 | if (current.isNotEmpty) { |
| 482 | if (result.isNotEmpty) { |
| 483 | result.last.original += current.toString(); |
| 484 | } else { |
| 485 | // If there is nothing in the string besides control codes, then just |
| 486 | // return them as the only entry. |
| 487 | result = <_AnsiRun>[_AnsiRun(current.toString(), '')]; |
| 488 | } |
| 489 | } |
| 490 | return result; |
| 491 | } |
| 492 | |
Alexandre Ardhuin | 5169ab5 | 2019-02-21 09:27:07 +0100 | [diff] [blame] | 493 | String joinRun(List<_AnsiRun> list, int start, [ int end ]) { |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 494 | return list.sublist(start, end).map<String>((_AnsiRun run) => run.original).join().trim(); |
| 495 | } |
| 496 | |
| 497 | final List<String> result = <String>[]; |
| 498 | final int effectiveLength = max(columnWidth - start, kMinColumnWidth); |
| 499 | for (String line in text.split('\n')) { |
| 500 | // If the line is short enough, even with ANSI codes, then we can just add |
| 501 | // add it and move on. |
Greg Spencer | 4559ae1 | 2018-10-30 16:00:50 -0700 | [diff] [blame] | 502 | if (line.length <= effectiveLength || !shouldWrap) { |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 503 | result.add(line); |
| 504 | continue; |
| 505 | } |
| 506 | final List<_AnsiRun> splitLine = splitWithCodes(line); |
| 507 | if (splitLine.length <= effectiveLength) { |
| 508 | result.add(line); |
| 509 | continue; |
| 510 | } |
| 511 | |
| 512 | int currentLineStart = 0; |
| 513 | int lastWhitespace; |
| 514 | // Find the start of the current line. |
| 515 | for (int index = 0; index < splitLine.length; ++index) { |
| 516 | if (splitLine[index].character.isNotEmpty && isWhitespace(splitLine[index])) { |
| 517 | lastWhitespace = index; |
| 518 | } |
| 519 | |
| 520 | if (index - currentLineStart >= effectiveLength) { |
| 521 | // Back up to the last whitespace, unless there wasn't any, in which |
| 522 | // case we just split where we are. |
| 523 | if (lastWhitespace != null) { |
| 524 | index = lastWhitespace; |
| 525 | } |
| 526 | |
| 527 | result.add(joinRun(splitLine, currentLineStart, index)); |
| 528 | |
| 529 | // Skip any intervening whitespace. |
Jonah Williams | d80999d | 2018-11-01 20:04:52 -0700 | [diff] [blame] | 530 | while (index < splitLine.length && isWhitespace(splitLine[index])) { |
Greg Spencer | 081d2a7 | 2018-10-10 18:17:56 -0700 | [diff] [blame] | 531 | index++; |
| 532 | } |
| 533 | |
| 534 | currentLineStart = index; |
| 535 | lastWhitespace = null; |
| 536 | } |
| 537 | } |
| 538 | result.add(joinRun(splitLine, currentLineStart)); |
| 539 | } |
| 540 | return result; |
| 541 | } |