daemon.dart 13.6 KB
Newer Older
Devon Carew's avatar
Devon Carew committed
1 2 3 4 5 6 7 8
// 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:convert';
import 'dart:io';

9
import '../android/android_device.dart';
10
import '../application_package.dart';
11
import '../base/context.dart';
12
import '../base/logger.dart';
13
import '../build_info.dart';
14
import '../device.dart';
15
import '../globals.dart';
16 17
import '../ios/devices.dart';
import '../ios/simulators.dart';
18
import '../runner/flutter_command.dart';
19
import 'run.dart';
Devon Carew's avatar
Devon Carew committed
20

21
const String protocolVersion = '0.1.0';
22

Devon Carew's avatar
Devon Carew committed
23 24 25 26 27 28 29
/// A server process command. This command will start up a long-lived server.
/// It reads JSON-RPC based commands from stdin, executes them, and returns
/// JSON-RPC based responses and events to stdout.
///
/// It can be shutdown with a `daemon.shutdown` command (or by killing the
/// process).
class DaemonCommand extends FlutterCommand {
30
  DaemonCommand({ this.hidden: false });
31

32
  @override
Devon Carew's avatar
Devon Carew committed
33
  final String name = 'daemon';
34 35

  @override
36
  final String description = 'Run a persistent, JSON-RPC based server to communicate with devices.';
Devon Carew's avatar
Devon Carew committed
37

38
  @override
39 40
  bool get requiresProjectRoot => false;

41
  @override
42
  final bool hidden;
43

44
  @override
45 46 47
  Future<int> runInProject() {
    printStatus('Starting device daemon...');

48 49 50
    AppContext appContext = new AppContext();
    NotifyingLogger notifyingLogger = new NotifyingLogger();
    appContext[Logger] = notifyingLogger;
Devon Carew's avatar
Devon Carew committed
51

52
    return appContext.runInZone(() {
53 54 55 56 57 58 59 60
      Stream<Map<String, dynamic>> commandStream = stdin
        .transform(UTF8.decoder)
        .transform(const LineSplitter())
        .where((String line) => line.startsWith('[{') && line.endsWith('}]'))
        .map((String line) {
          line = line.substring(1, line.length - 1);
          return JSON.decode(line);
        });
Devon Carew's avatar
Devon Carew committed
61

Ian Hickson's avatar
Ian Hickson committed
62
      Daemon daemon = new Daemon(commandStream, (Map<String, dynamic> command) {
63
        stdout.writeln('[${JSON.encode(command, toEncodable: _jsonEncodeObject)}]');
64
      }, daemonCommand: this, notifyingLogger: notifyingLogger);
Devon Carew's avatar
Devon Carew committed
65

66
      return daemon.onExit;
67
    }, onError: _handleError);
Devon Carew's avatar
Devon Carew committed
68
  }
69 70 71 72 73 74

  dynamic _jsonEncodeObject(dynamic object) {
    if (object is Device)
      return _deviceToMap(object);
    return object;
  }
75 76 77 78

  void _handleError(dynamic error) {
    printError('Error from flutter daemon: $error');
  }
Devon Carew's avatar
Devon Carew committed
79 80
}

81
typedef void DispatchComand(Map<String, dynamic> command);
Devon Carew's avatar
Devon Carew committed
82 83 84 85

typedef Future<dynamic> CommandHandler(dynamic args);

class Daemon {
Ian Hickson's avatar
Ian Hickson committed
86
  Daemon(Stream<Map<String, dynamic>> commandStream, this.sendCommand, {
87
    this.daemonCommand,
88
    this.notifyingLogger
89
  }) {
Devon Carew's avatar
Devon Carew committed
90
    // Set up domains.
91 92 93
    _registerDomain(daemonDomain = new DaemonDomain(this));
    _registerDomain(appDomain = new AppDomain(this));
    _registerDomain(deviceDomain = new DeviceDomain(this));
Devon Carew's avatar
Devon Carew committed
94 95 96

    // Start listening.
    commandStream.listen(
Ian Hickson's avatar
Ian Hickson committed
97
      (Map<String, dynamic> request) => _handleRequest(request),
Devon Carew's avatar
Devon Carew committed
98 99 100 101
      onDone: () => _onExitCompleter.complete(0)
    );
  }

102 103 104 105
  DaemonDomain daemonDomain;
  AppDomain appDomain;
  DeviceDomain deviceDomain;

106 107
  final DispatchComand sendCommand;
  final DaemonCommand daemonCommand;
108
  final NotifyingLogger notifyingLogger;
109 110 111 112

  final Completer<int> _onExitCompleter = new Completer<int>();
  final Map<String, Domain> _domainMap = <String, Domain>{};

Devon Carew's avatar
Devon Carew committed
113
  void _registerDomain(Domain domain) {
114
    _domainMap[domain.name] = domain;
Devon Carew's avatar
Devon Carew committed
115 116 117 118
  }

  Future<int> get onExit => _onExitCompleter.future;

Ian Hickson's avatar
Ian Hickson committed
119
  void _handleRequest(Map<String, dynamic> request) {
120 121 122 123
    // {id, method, params}

    // [id] is an opaque type to us.
    dynamic id = request['id'];
Devon Carew's avatar
Devon Carew committed
124 125

    if (id == null) {
126
      stderr.writeln('no id for request: $request');
Devon Carew's avatar
Devon Carew committed
127 128 129 130
      return;
    }

    try {
131 132 133
      String method = request['method'];
      if (method.indexOf('.') == -1)
        throw 'method not understood: $method';
Devon Carew's avatar
Devon Carew committed
134

135 136 137 138
      String prefix = method.substring(0, method.indexOf('.'));
      String name = method.substring(method.indexOf('.') + 1);
      if (_domainMap[prefix] == null)
        throw 'no domain for method: $method';
Devon Carew's avatar
Devon Carew committed
139

140
      _domainMap[prefix].handleCommand(name, id, request['params']);
141
    } catch (error) {
142
      _send(<String, dynamic>{'id': id, 'error': _toJsonable(error)});
Devon Carew's avatar
Devon Carew committed
143 144 145
    }
  }

Ian Hickson's avatar
Ian Hickson committed
146
  void _send(Map<String, dynamic> map) => sendCommand(map);
Devon Carew's avatar
Devon Carew committed
147 148

  void shutdown() {
149
    _domainMap.values.forEach((Domain domain) => domain.dispose());
Devon Carew's avatar
Devon Carew committed
150 151 152 153 154 155
    if (!_onExitCompleter.isCompleted)
      _onExitCompleter.complete(0);
  }
}

abstract class Domain {
156 157
  Domain(this.daemon, this.name);

Devon Carew's avatar
Devon Carew committed
158 159
  final Daemon daemon;
  final String name;
Ian Hickson's avatar
Ian Hickson committed
160
  final Map<String, CommandHandler> _handlers = <String, CommandHandler>{};
Devon Carew's avatar
Devon Carew committed
161 162 163 164 165

  void registerHandler(String name, CommandHandler handler) {
    _handlers[name] = handler;
  }

166 167
  FlutterCommand get command => daemon.daemonCommand;

168
  @override
Devon Carew's avatar
Devon Carew committed
169 170
  String toString() => name;

171
  void handleCommand(String command, dynamic id, dynamic args) {
Ian Hickson's avatar
Ian Hickson committed
172
    new Future<dynamic>.sync(() {
173 174 175
      if (_handlers.containsKey(command))
        return _handlers[command](args);
      throw 'command not understood: $name.$command';
Ian Hickson's avatar
Ian Hickson committed
176
    }).then((dynamic result) {
Devon Carew's avatar
Devon Carew committed
177
      if (result == null) {
Ian Hickson's avatar
Ian Hickson committed
178
        _send(<String, dynamic>{'id': id});
Devon Carew's avatar
Devon Carew committed
179
      } else {
Ian Hickson's avatar
Ian Hickson committed
180
        _send(<String, dynamic>{'id': id, 'result': _toJsonable(result)});
Devon Carew's avatar
Devon Carew committed
181
      }
Ian Hickson's avatar
Ian Hickson committed
182 183
    }).catchError((dynamic error, dynamic trace) {
      _send(<String, dynamic>{'id': id, 'error': _toJsonable(error)});
Devon Carew's avatar
Devon Carew committed
184 185 186
    });
  }

187
  void sendEvent(String name, [dynamic args]) {
188
    Map<String, dynamic> map = <String, dynamic>{ 'event': name };
189 190 191 192 193
    if (args != null)
      map['params'] = _toJsonable(args);
    _send(map);
  }

Ian Hickson's avatar
Ian Hickson committed
194
  void _send(Map<String, dynamic> map) => daemon._send(map);
195 196

  void dispose() { }
Devon Carew's avatar
Devon Carew committed
197 198 199
}

/// This domain responds to methods like [version] and [shutdown].
200 201
///
/// This domain fires the `daemon.logMessage` event.
Devon Carew's avatar
Devon Carew committed
202 203 204 205
class DaemonDomain extends Domain {
  DaemonDomain(Daemon daemon) : super(daemon, 'daemon') {
    registerHandler('version', version);
    registerHandler('shutdown', shutdown);
206

207
    _subscription = daemon.notifyingLogger.onMessage.listen((LogMessage message) {
208
      if (message.stackTrace != null) {
209
        sendEvent('daemon.logMessage', <String, dynamic>{
210 211 212
          'level': message.level,
          'message': message.message,
          'stackTrace': message.stackTrace.toString()
213 214
        });
      } else {
215
        sendEvent('daemon.logMessage', <String, dynamic>{
216 217
          'level': message.level,
          'message': message.message
218 219 220
        });
      }
    });
Devon Carew's avatar
Devon Carew committed
221 222
  }

223
  StreamSubscription<LogMessage> _subscription;
224

225
  Future<String> version(dynamic args) {
Ian Hickson's avatar
Ian Hickson committed
226
    return new Future<String>.value(protocolVersion);
Devon Carew's avatar
Devon Carew committed
227 228
  }

Ian Hickson's avatar
Ian Hickson committed
229
  Future<Null> shutdown(dynamic args) {
Devon Carew's avatar
Devon Carew committed
230
    Timer.run(() => daemon.shutdown());
Ian Hickson's avatar
Ian Hickson committed
231
    return new Future<Null>.value();
Devon Carew's avatar
Devon Carew committed
232
  }
233

234
  @override
235 236 237
  void dispose() {
    _subscription?.cancel();
  }
Devon Carew's avatar
Devon Carew committed
238 239
}

240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
/// Return the device matching the deviceId field in the args.
Future<Device> _getDevice(Daemon daemon, Map<String, dynamic> args) async {
  if (args == null || args['deviceId'] is! String)
    throw 'deviceId is required';

  List<Device> devices = await daemon.deviceDomain.getDevices();
  Device device = devices.firstWhere(
    (Device device) => device.id == args['deviceId'],
    orElse: () => null
  );

  if (device == null)
    throw "device '${args['deviceId']}' not found";

  return device;
}

257
/// This domain responds to methods like [start] and [stop].
Devon Carew's avatar
Devon Carew committed
258 259 260 261 262 263
///
/// It'll be extended to fire events for when applications start, stop, and
/// log data.
class AppDomain extends Domain {
  AppDomain(Daemon daemon) : super(daemon, 'app') {
    registerHandler('start', start);
264
    registerHandler('stop', stop);
265
    registerHandler('discover', discover);
Devon Carew's avatar
Devon Carew committed
266 267
  }

268
  Future<dynamic> start(Map<String, dynamic> args) async {
269
    Device device = await _getDevice(daemon, args);
270

271
    if (args['projectDirectory'] is! String)
272
      throw "projectDirectory is required";
273 274
    String projectDirectory = args['projectDirectory'];
    if (!FileSystemEntity.isDirectorySync(projectDirectory))
275
      throw "'$projectDirectory' does not exist";
276

277
    // We change the current working directory for the duration of the `start` command.
278 279 280 281 282 283
    // TODO(devoncarew): Make flutter_tools work better with commands run from any directory.
    Directory cwd = Directory.current;
    Directory.current = new Directory(projectDirectory);

    try {
      int result = await startApp(
284
        device,
285
        stop: true,
286
        target: args['target'],
Devon Carew's avatar
Devon Carew committed
287
        route: args['route']
288 289 290 291 292 293 294
      );

      if (result != 0)
        throw 'Error starting app: $result';
    } finally {
      Directory.current = cwd;
    }
295

296
    return null;
Devon Carew's avatar
Devon Carew committed
297 298
  }

299 300
  Future<bool> stop(Map<String, dynamic> args) async {
    Device device = await _getDevice(daemon, args);
301 302

    if (args['projectDirectory'] is! String)
303
      throw "projectDirectory is required";
304 305
    String projectDirectory = args['projectDirectory'];
    if (!FileSystemEntity.isDirectorySync(projectDirectory))
306
      throw "'$projectDirectory' does not exist";
307 308 309 310 311 312 313 314 315 316 317 318

    Directory cwd = Directory.current;
    Directory.current = new Directory(projectDirectory);

    try {
      ApplicationPackage app = command.applicationPackages.getPackageForPlatform(device.platform);
      return device.stopApp(app);
    } finally {
      Directory.current = cwd;
    }
  }

319 320 321 322 323 324
  Future<List<Map<String, dynamic>>> discover(Map<String, dynamic> args) async {
    Device device = await _getDevice(daemon, args);
    List<DiscoveredApp> apps = await device.discoverApps();
    return apps.map((DiscoveredApp app) =>
      <String, dynamic>{'id': app.id, 'observatoryDevicePort': app.observatoryPort}
    ).toList();
Devon Carew's avatar
Devon Carew committed
325 326 327
  }
}

328 329
/// This domain lets callers list and monitor connected devices.
///
330 331
/// It exports a `getDevices()` call, as well as firing `device.added` and
/// `device.removed` events.
332 333 334
class DeviceDomain extends Domain {
  DeviceDomain(Daemon daemon) : super(daemon, 'device') {
    registerHandler('getDevices', getDevices);
335 336
    registerHandler('enable', enable);
    registerHandler('disable', disable);
337 338
    registerHandler('forward', forward);
    registerHandler('unforward', unforward);
339

340 341 342
    PollingDeviceDiscovery deviceDiscovery = new AndroidDevices();
    if (deviceDiscovery.supportsPlatform)
      _discoverers.add(deviceDiscovery);
343

344 345 346 347 348 349 350 351 352 353
    deviceDiscovery = new IOSDevices();
    if (deviceDiscovery.supportsPlatform)
      _discoverers.add(deviceDiscovery);

    deviceDiscovery = new IOSSimulators();
    if (deviceDiscovery.supportsPlatform)
      _discoverers.add(deviceDiscovery);

    for (PollingDeviceDiscovery discoverer in _discoverers) {
      discoverer.onAdded.listen((Device device) {
354 355
        sendEvent('device.added', _deviceToMap(device));
      });
356
      discoverer.onRemoved.listen((Device device) {
357 358 359
        sendEvent('device.removed', _deviceToMap(device));
      });
    }
360 361
  }

362
  List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
363

364
  Future<List<Device>> getDevices([dynamic args]) {
365 366 367
    List<Device> devices = _discoverers.expand((PollingDeviceDiscovery discoverer) {
      return discoverer.devices;
    }).toList();
Ian Hickson's avatar
Ian Hickson committed
368
    return new Future<List<Device>>.value(devices);
369 370
  }

371
  /// Enable device events.
Ian Hickson's avatar
Ian Hickson committed
372
  Future<Null> enable(dynamic args) {
373 374
    for (PollingDeviceDiscovery discoverer in _discoverers) {
      discoverer.startPolling();
375
    }
Ian Hickson's avatar
Ian Hickson committed
376
    return new Future<Null>.value();
377 378
  }

379
  /// Disable device events.
Ian Hickson's avatar
Ian Hickson committed
380
  Future<Null> disable(dynamic args) {
381 382
    for (PollingDeviceDiscovery discoverer in _discoverers) {
      discoverer.stopPolling();
383
    }
Ian Hickson's avatar
Ian Hickson committed
384
    return new Future<Null>.value();
385 386
  }

387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
  /// Forward a host port to a device port.
  Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async {
    Device device = await _getDevice(daemon, args);

    if (args['devicePort'] is! int)
      throw 'devicePort is required';
    int devicePort = args['devicePort'];

    int hostPort = args['hostPort'];

    hostPort = await device.portForwarder.forward(devicePort, hostPort: hostPort);

    return <String, dynamic>{'hostPort': hostPort};
  }

  /// Removes a forwarded port.
  Future<Null> unforward(Map<String, dynamic> args) async {
    Device device = await _getDevice(daemon, args);

    if (args['devicePort'] is! int)
      throw 'devicePort is required';
    int devicePort = args['devicePort'];

    if (args['hostPort'] is! int)
      throw 'hostPort is required';
    int hostPort = args['hostPort'];

    device.portForwarder.unforward(new ForwardedPort(hostPort, devicePort));
  }

417
  @override
418
  void dispose() {
419 420
    for (PollingDeviceDiscovery discoverer in _discoverers) {
      discoverer.dispose();
421 422 423 424
    }
  }
}

425 426
Map<String, String> _deviceToMap(Device device) {
  return <String, String>{
427
    'id': device.id,
428
    'name': device.name,
429
    'platform': getNameForTargetPlatform(device.platform)
430 431 432
  };
}

Devon Carew's avatar
Devon Carew committed
433
dynamic _toJsonable(dynamic obj) {
Ian Hickson's avatar
Ian Hickson committed
434
  if (obj is String || obj is int || obj is bool || obj is Map<dynamic, dynamic> || obj is List<dynamic> || obj == null)
Devon Carew's avatar
Devon Carew committed
435
    return obj;
436 437
  if (obj is Device)
    return obj;
Hixie's avatar
Hixie committed
438
  return '$obj';
Devon Carew's avatar
Devon Carew committed
439
}
440

441
class NotifyingLogger extends Logger {
442 443 444 445
  StreamController<LogMessage> _messageController = new StreamController<LogMessage>.broadcast();

  Stream<LogMessage> get onMessage => _messageController.stream;

446
  @override
447 448 449 450
  void printError(String message, [StackTrace stackTrace]) {
    _messageController.add(new LogMessage('error', message, stackTrace));
  }

451
  @override
452
  void printStatus(String message, { bool emphasis: false }) {
453 454 455
    _messageController.add(new LogMessage('status', message));
  }

456
  @override
457
  void printTrace(String message) {
458
    // This is a lot of traffic to send over the wire.
459
  }
460 461 462 463 464 465

  @override
  Status startProgress(String message) {
    printStatus(message);
    return new Status();
  }
466 467 468 469 470 471 472 473 474
}

class LogMessage {
  final String level;
  final String message;
  final StackTrace stackTrace;

  LogMessage(this.level, this.message, [this.stackTrace]);
}