From 62bd083e14d25ee80ed7e0fb515eec5f8214adf5 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 30 Sep 2015 18:13:11 -0400 Subject: [PATCH] Implement working UpdateTask in Dart Dart code now supports the full flow that the C++ code used to: version check, download, replace app bundle. Bonus: the Dart code is much easier to follow, thanks to async/await! This is part 2 of a 3-part change. The first part added new mojom interfaces, PathService and UpdateService, to the sky_services package. --- .../src/flutter/sky/packages/updater/BUILD.gn | 2 + .../sky/packages/updater/lib/main.dart | 97 ++++++++++++++++++- .../packages/updater/lib/pipe_to_file.dart | 64 ++++++++++++ .../sky/packages/updater/lib/version.dart | 32 ++++++ .../flutter/sky/packages/updater/pubspec.yaml | 11 ++- 5 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 engine/src/flutter/sky/packages/updater/lib/pipe_to_file.dart create mode 100644 engine/src/flutter/sky/packages/updater/lib/version.dart diff --git a/engine/src/flutter/sky/packages/updater/BUILD.gn b/engine/src/flutter/sky/packages/updater/BUILD.gn index a900d09137a..65185a67b66 100644 --- a/engine/src/flutter/sky/packages/updater/BUILD.gn +++ b/engine/src/flutter/sky/packages/updater/BUILD.gn @@ -25,6 +25,8 @@ action("updater") { ] deps = [ + "//sky/services/activity:interfaces", + "//sky/services/updater:interfaces", "//sky/tools/sky_snapshot($host_toolchain)", ] } diff --git a/engine/src/flutter/sky/packages/updater/lib/main.dart b/engine/src/flutter/sky/packages/updater/lib/main.dart index 4c71346b862..ea168da2998 100644 --- a/engine/src/flutter/sky/packages/updater/lib/main.dart +++ b/engine/src/flutter/sky/packages/updater/lib/main.dart @@ -2,13 +2,104 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; +import 'dart:io'; + +import 'package:mojo/core.dart'; +import 'package:sky/services.dart'; +import 'package:sky_services/updater/update_service.mojom.dart'; +import 'package:path/path.dart' as path; +import 'package:yaml/yaml.dart' as yaml; + +import 'version.dart'; +import 'pipe_to_file.dart'; + +const String kManifestFile = 'sky.yaml'; +const String kBundleFile = 'app.skyx'; + +UpdateServiceProxy _initUpdateService() { + UpdateServiceProxy updateService = new UpdateServiceProxy.unbound(); + shell.requestService(null, updateService); + return updateService; +} + +final UpdateServiceProxy _updateService = _initUpdateService(); + +String cachedDataDir = null; +Future getDataDir() async { + if (cachedDataDir == null) + cachedDataDir = await getAppDataDir(); + return cachedDataDir; +} + class UpdateTask { UpdateTask() {} - String toString() => "UpdateTask()"; + run() async { + try { + await _runImpl(); + } catch(e) { + print('Update failed: $e'); + } finally { + _updateService.ptr.notifyUpdateCheckComplete(); + } + } + + _runImpl() async { + _dataDir = await getDataDir(); + + await _readLocalManifest(); + yaml.YamlMap remoteManifest = await _fetchManifest(); + if (!_shouldUpdate(remoteManifest)) { + print('Update skipped. No new version.'); + return; + } + MojoResult result = await _fetchBundle(); + if (!result.isOk) { + print('Update failed while fetching new skyx bundle.'); + return; + } + await _replaceBundle(); + print('Update success.'); + } + + yaml.YamlMap _currentManifest; + String _dataDir; + String _tempPath; + + _readLocalManifest() async { + String manifestPath = path.join(_dataDir, kManifestFile); + String manifestData = await new File(manifestPath).readAsString(); + _currentManifest = yaml.loadYaml(manifestData, sourceUrl: manifestPath); + } + + Future _fetchManifest() async { + String manifestUrl = _currentManifest['update_url'] + '/' + kManifestFile; + String manifestData = await fetchString(manifestUrl); + return yaml.loadYaml(manifestData, sourceUrl: manifestUrl); + } + + bool _shouldUpdate(yaml.YamlMap remoteManifest) { + Version currentVersion = new Version(_currentManifest['version']); + Version remoteVersion = new Version(remoteManifest['version']); + return (currentVersion < remoteVersion); + } + + Future _fetchBundle() async { + // TODO(mpcomplete): Use the cache dir. We need an equivalent of mkstemp(). + _tempPath = path.join(_dataDir, 'tmp.skyx'); + String bundleUrl = _currentManifest['update_url'] + '/' + kBundleFile; + UrlResponse response = await fetchUrl(bundleUrl); + return PipeToFile.copyToFile(response.body, _tempPath); + } + + _replaceBundle() async { + String bundlePath = path.join(_dataDir, kBundleFile); + await new File(_tempPath).rename(bundlePath); + } } void main() { - var x = new UpdateTask(); - print("Success: $x"); + var task = new UpdateTask(); + task.run(); } diff --git a/engine/src/flutter/sky/packages/updater/lib/pipe_to_file.dart b/engine/src/flutter/sky/packages/updater/lib/pipe_to_file.dart new file mode 100644 index 00000000000..f6a62d0a0b7 --- /dev/null +++ b/engine/src/flutter/sky/packages/updater/lib/pipe_to_file.dart @@ -0,0 +1,64 @@ +// Copyright 2015 The Chromium Authors. 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:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:mojo/core.dart'; + +// Helper class to drain the contents of a mojo data pipe to a file. +class PipeToFile { + MojoDataPipeConsumer _consumer; + MojoEventStream _eventStream; + IOSink _outputStream; + + PipeToFile(this._consumer, String outputPath) { + _eventStream = new MojoEventStream(_consumer.handle); + _outputStream = new File(outputPath).openWrite(); + } + + Future _doRead() async { + ByteData thisRead = _consumer.beginRead(); + if (thisRead == null) { + throw 'Data pipe beginRead failed: ${_consumer.status}'; + } + // TODO(mpcomplete): Should I worry about the _eventStream listen callback + // being invoked again before this completes? + await _outputStream.add(thisRead.buffer.asUint8List()); + return _consumer.endRead(thisRead.lengthInBytes); + } + + Future drain() async { + var completer = new Completer(); + // TODO(mpcomplete): Is it legit to pass an async callback to listen? + _eventStream.listen((List event) async { + var mojoSignals = new MojoHandleSignals(event[1]); + if (mojoSignals.isReadable) { + var result = await _doRead(); + if (!result.isOk) { + _eventStream.close(); + _eventStream = null; + _outputStream.close(); + completer.complete(result); + } else { + _eventStream.enableReadEvents(); + } + } else if (mojoSignals.isPeerClosed) { + _eventStream.close(); + _eventStream = null; + _outputStream.close(); + completer.complete(MojoResult.OK); + } else { + throw 'Unexpected handle event: $mojoSignals'; + } + }); + return completer.future; + } + + static Future copyToFile(MojoDataPipeConsumer consumer, String outputPath) { + var drainer = new PipeToFile(consumer, outputPath); + return drainer.drain(); + } +} diff --git a/engine/src/flutter/sky/packages/updater/lib/version.dart b/engine/src/flutter/sky/packages/updater/lib/version.dart new file mode 100644 index 00000000000..975927736cb --- /dev/null +++ b/engine/src/flutter/sky/packages/updater/lib/version.dart @@ -0,0 +1,32 @@ +// Copyright 2015 The Chromium Authors. 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:math'; + +// This class represents a dot-separated version string. Used for comparing +// versions. +// Usage: assert(new Version('1.1.0') < new Version('1.2.1')); +class Version { + Version(String versionStr) : + _parts = versionStr.split('.').map((val) => int.parse(val)).toList(); + + List _parts; + + bool operator<(Version other) => _compare(other) < 0; + bool operator==(dynamic other) => other is Version && _compare(other) == 0; + bool operator>(Version other) => _compare(other) > 0; + + int _compare(Version other) { + int length = min(_parts.length, other._parts.length); + for (int i = 0; i < length; ++i) { + if (_parts[i] < other._parts[i]) + return -1; + if (_parts[i] > other._parts[i]) + return 1; + } + return _parts.length - other._parts.length; // results in 1.0 < 1.0.0 + } + + int get hashCode => _parts.fold(373, (acc, part) => 37*acc + part); +} diff --git a/engine/src/flutter/sky/packages/updater/pubspec.yaml b/engine/src/flutter/sky/packages/updater/pubspec.yaml index e76531eaaa7..d2b804f0a07 100644 --- a/engine/src/flutter/sky/packages/updater/pubspec.yaml +++ b/engine/src/flutter/sky/packages/updater/pubspec.yaml @@ -1,9 +1,16 @@ -name: sky_updater +name: updater version: 0.0.1 author: Chromium Authors description: The autoupdater for flutter homepage: http://flutter.io dependencies: - mojo: ^0.0.21 + mojo: '>=0.1.0 <0.2.0' + sky: any + sky_services: any + path: any + yaml: any +dependency_overrides: + sky: + path: ../sky environment: sdk: '>=1.12.0 <2.0.0'