Commit af3a10f4 authored by Matt Perry's avatar Matt Perry

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.
parent 01f7c846
......@@ -37,6 +37,15 @@ UserFeedbackProxy _initUserFeedbackProxy() {
final UserFeedbackProxy _userFeedbackProxy = _initUserFeedbackProxy();
final UserFeedback userFeedback = _userFeedbackProxy.ptr;
PathServiceProxy _initPathServiceProxy() {
PathServiceProxy proxy = new PathServiceProxy.unbound();
shell.requestService(null, proxy);
return proxy;
}
final PathServiceProxy _pathServiceProxy = _initPathServiceProxy();
final PathService pathService = _pathServiceProxy.ptr;
Color _cachedPrimaryColor;
String _cachedLabel;
......@@ -55,5 +64,6 @@ void updateTaskDescription(String label, Color color) {
_activityProxy.ptr.setTaskDescription(description);
}
Future<String> getFilesDir() async => (await _activityProxy.ptr.getFilesDir()).path;
Future<String> getCacheDir() async => (await _activityProxy.ptr.getCacheDir()).path;
Future<String> getAppDataDir() async => (await _pathServiceProxy.ptr.getAppDataDir()).path;
Future<String> getFilesDir() async => (await _pathServiceProxy.ptr.getFilesDir()).path;
Future<String> getCacheDir() async => (await _pathServiceProxy.ptr.getCacheDir()).path;
......@@ -25,6 +25,8 @@ action("updater") {
]
deps = [
"//sky/services/activity:interfaces",
"//sky/services/updater:interfaces",
"//sky/tools/sky_snapshot($host_toolchain)",
]
}
......@@ -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<String> 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<yaml.YamlMap> _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<MojoResult> _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();
}
// 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<MojoResult> _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<MojoResult> drain() async {
var completer = new Completer();
// TODO(mpcomplete): Is it legit to pass an async callback to listen?
_eventStream.listen((List<int> 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<MojoResult> copyToFile(MojoDataPipeConsumer consumer, String outputPath) {
var drainer = new PipeToFile(consumer, outputPath);
return drainer.drain();
}
}
// 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<int> _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);
}
name: sky_updater
name: updater
version: 0.0.1
author: Chromium Authors <sky-dev@googlegroups.com>
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'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment