diff --git a/lib/roles/device.js b/lib/roles/device.js index 784fb117..10da16ad 100644 --- a/lib/roles/device.js +++ b/lib/roles/device.js @@ -27,7 +27,7 @@ module.exports = function(options) { .dependency(require('./device/plugins/touch')) .dependency(require('./device/plugins/install')) .dependency(require('./device/plugins/forward')) - .dependency(require('./device/plugins/owner')) + .dependency(require('./device/plugins/group')) .define(function(options, solo) { if (process.send) { // Only if we have a parent process diff --git a/lib/roles/device/plugins/group.js b/lib/roles/device/plugins/group.js new file mode 100644 index 00000000..453d64e5 --- /dev/null +++ b/lib/roles/device/plugins/group.js @@ -0,0 +1,146 @@ +var events = require('events') + +var Promise = require('bluebird') +var syrup = require('syrup') + +var logger = require('../../../util/logger') +var wire = require('../../../wire') +var wireutil = require('../../../wire/util') +var grouputil = require('../../../util/grouputil') +var lifecycle = require('../../../util/lifecycle') + +module.exports = syrup.serial() + .dependency(require('./identity')) + .dependency(require('./input')) + .dependency(require('../support/router')) + .dependency(require('../support/push')) + .dependency(require('../support/sub')) + .dependency(require('../support/channels')) + .define(function(options, identity, input, router, push, sub, channels) { + var log = logger.createLogger('device:plugins:group') + , currentGroup = null + , emitter = new events.EventEmitter() + + function joinGroup(newGroup, timeout) { + if (currentGroup) { + return Promise.reject(new grouputil.AlreadyGroupedError()) + } + + currentGroup = newGroup + + log.info('Now owned by "%s"', currentGroup.email) + log.info('Subscribing to group channel "%s"', currentGroup.group) + + channels.register(currentGroup.group, timeout) + sub.subscribe(currentGroup.group) + + push.send([ + wireutil.global + , wireutil.envelope(new wire.JoinGroupMessage( + options.serial + , currentGroup + )) + ]) + + input.acquireWakeLock() + input.unlock() + + emitter.emit('join', currentGroup) + + return Promise.resolve(currentGroup) + } + + function leaveGroup() { + if (!currentGroup) { + return Promise.reject(new grouputil.NotGroupedError()) + } + + log.info('No longer owned by "%s"', currentGroup.email) + log.info('Unsubscribing from group channel "%s"', currentGroup.group) + + channels.unregister(currentGroup.group) + sub.unsubscribe(currentGroup.group) + + push.send([ + wireutil.global + , wireutil.envelope(new wire.LeaveGroupMessage( + options.serial + , currentGroup + )) + ]) + + input.releaseWakeLock() + input.lock() + + var oldGroup = currentGroup + currentGroup = null + + emitter.emit('leave', oldGroup) + + return Promise.resolve(oldGroup) + } + + channels.on('timeout', function(channel) { + if (currentGroup && channel === currentGroup.group) { + leaveGroup() + } + }) + + router + .on(wire.GroupMessage, function(channel, message) { + var reply = wireutil.reply(options.serial) + grouputil.match(identity, message.requirements) + .then(function() { + return joinGroup(message.owner) + }) + .then(function() { + push.send([ + channel + , reply.okay() + ]) + }) + .catch(grouputil.RequirementMismatchError, function(err) { + push.send([ + channel + , reply.fail(err.message) + ]) + }) + .catch(grouputil.AlreadyGroupedError, function(err) { + push.send([ + channel + , reply.fail(err.message) + ]) + }) + }) + .on(wire.UngroupMessage, function(channel, message) { + var reply = wireutil.reply(options.serial) + grouputil.match(identity, message.requirements) + .then(function() { + return leaveGroup() + }) + .then(function() { + push.send([ + channel + , reply.okay() + ]) + }) + .catch(grouputil.NotGroupedError, function(err) { + push.send([ + channel + , reply.fail(err.message) + ]) + }) + }) + + lifecycle.observe(function() { + if (currentGroup) { + leaveGroup() + return Promise.delay(500) + } + else { + return true + } + }) + + return emitter + }) diff --git a/lib/roles/device/plugins/logcat.js b/lib/roles/device/plugins/logcat.js index d94b683e..3c190c41 100644 --- a/lib/roles/device/plugins/logcat.js +++ b/lib/roles/device/plugins/logcat.js @@ -9,7 +9,7 @@ module.exports = syrup.serial() .dependency(require('../support/adb')) .dependency(require('../support/router')) .dependency(require('../support/push')) - .dependency(require('./owner')) + .dependency(require('./group')) .define(function(options, adb, router, push, owner) { var log = logger.createLogger('device:plugins:logcat') diff --git a/lib/roles/device/plugins/owner.js b/lib/roles/device/plugins/owner.js deleted file mode 100644 index 58b9cc09..00000000 --- a/lib/roles/device/plugins/owner.js +++ /dev/null @@ -1,122 +0,0 @@ -var Promise = require('bluebird') -var syrup = require('syrup') - -var logger = require('../../../util/logger') -var wire = require('../../../wire') -var wireutil = require('../../../wire/util') -var devutil = require('../../../util/devutil') -var lifecycle = require('../../../util/lifecycle') - -module.exports = syrup.serial() - .dependency(require('./identity')) - .dependency(require('./input')) - .dependency(require('../support/router')) - .dependency(require('../support/push')) - .dependency(require('../support/sub')) - .dependency(require('../support/channels')) - .define(function(options, identity, input, router, push, sub, channels) { - var log = logger.createLogger('device:plugins:owner') - var owner = null - - function isGrouped() { - return !!owner - } - - function isOwnedBy(someOwner) { - return owner && owner.group == someOwner.group - } - - function joinGroup(newOwner, timeout) { - log.info('Now owned by "%s"', newOwner.email) - log.info('Subscribing to group channel "%s"', newOwner.group) - channels.register(newOwner.group, timeout) - sub.subscribe(newOwner.group) - push.send([ - wireutil.global - , wireutil.envelope(new wire.JoinGroupMessage( - options.serial - , newOwner - )) - ]) - input.acquireWakeLock() - input.unlock() - owner = newOwner - } - - function leaveGroup() { - log.info('No longer owned by "%s"', owner.email) - log.info('Unsubscribing from group channel "%s"', owner.group) - channels.unregister(owner.group) - sub.unsubscribe(owner.group) - push.send([ - wireutil.global - , wireutil.envelope(new wire.LeaveGroupMessage( - options.serial - , owner - )) - ]) - input.releaseWakeLock() - input.lock() - owner = null - } - - channels.on('timeout', function(channel) { - if (owner && channel === owner.group) { - leaveGroup() - } - }) - - router - .on(wire.GroupMessage, function(channel, message) { - var reply = wireutil.reply(options.serial) - if (devutil.matchesRequirements(identity, message.requirements)) { - if (!isGrouped()) { - joinGroup(message.owner, message.timeout) - push.send([ - channel - , reply.okay() - ]) - } - else if (isOwnedBy(message.owner)) { - push.send([ - channel - , reply.okay() - ]) - } - else { - push.send([ - channel - , reply.fail() - ]) - } - } - }) - .on(wire.UngroupMessage, function(channel, message) { - var reply = wireutil.reply(options.serial) - if (devutil.matchesRequirements(identity, message.requirements)) { - if (isGrouped()) { - leaveGroup() - push.send([ - channel - , reply.okay() - ]) - } - else { - push.send([ - channel - , reply.okay() - ]) - } - } - }) - - lifecycle.observe(function() { - if (isGrouped()) { - leaveGroup() - return Promise.delay(500) - } - else { - return true - } - }) - }) diff --git a/lib/util/devutil.js b/lib/util/devutil.js index 83ce522b..eaf0ef33 100644 --- a/lib/util/devutil.js +++ b/lib/util/devutil.js @@ -2,10 +2,6 @@ var util = require('util') var split = require('split') var Promise = require('bluebird') -var semver = require('semver') -var minimatch = require('minimatch') - -var wire = require('../wire') var devutil = module.exports = Object.create(null) @@ -13,38 +9,6 @@ function closedError(err) { return err.message.indexOf('closed') !== -1 } -devutil.matchesRequirements = function(capabilities, requirements) { - return requirements.every(function(req) { - var capability = capabilities[req.name] - - if (!capability) { - return false - } - - switch (req.type) { - case wire.RequirementType.SEMVER: - if (!semver.satisfies(capability, req.value)) { - return false - } - break - case wire.RequirementType.GLOB: - if (!minimatch(capability, req.value)) { - return false - } - break - case wire.RequirementType.EXACT: - if (capability !== req.value) { - return false - } - break - default: - return false - } - - return true - }) -} - devutil.ensureUnusedPort = function(adb, serial, port) { return adb.openTcp(serial, port) .then(function(conn) { diff --git a/lib/util/grouputil.js b/lib/util/grouputil.js new file mode 100644 index 00000000..5ed40623 --- /dev/null +++ b/lib/util/grouputil.js @@ -0,0 +1,72 @@ +var util = require('util') + +var Promise = require('bluebird') +var semver = require('semver') +var minimatch = require('minimatch') + +var wire = require('../wire') + +function RequirementMismatchError(name) { + Error.call(this) + this.name = 'RequirementMismatchError' + this.message = util.format('Requirement mismatch for "%s"', name) + Error.captureStackTrace(this, RequirementMismatchError) +} + +util.inherits(RequirementMismatchError, Error) + +module.exports.RequirementMismatchError = RequirementMismatchError + +function AlreadyGroupedError(name) { + Error.call(this) + this.name = 'AlreadyGroupedError' + this.message = 'Already a member of another group' + Error.captureStackTrace(this, AlreadyGroupedError) +} + +util.inherits(AlreadyGroupedError, Error) + +module.exports.AlreadyGroupedError = AlreadyGroupedError + +function NotGroupedError() { + Error.call(this) + this.name = 'NotGroupedError' + this.message = 'Not a member of any group' + Error.captureStackTrace(this, NotGroupedError) +} + +util.inherits(NotGroupedError, Error) + +module.exports.NotGroupedError = NotGroupedError + +module.exports.match = Promise.method(function(capabilities, requirements) { + return requirements.every(function(req) { + var capability = capabilities[req.name] + + if (!capability) { + throw new RequirementMismatchError(req.name) + } + + switch (req.type) { + case wire.RequirementType.SEMVER: + if (!semver.satisfies(capability, req.value)) { + throw new RequirementMismatchError(req.name) + } + break + case wire.RequirementType.GLOB: + if (!minimatch(capability, req.value)) { + throw new RequirementMismatchError(req.name) + } + break + case wire.RequirementType.EXACT: + if (capability !== req.value) { + throw new RequirementMismatchError(req.name) + } + break + default: + throw new RequirementMismatchError(req.name) + } + + return true + }) +})