| # This Source Code Form is subject to the terms of the Mozilla Public |
| # License, v. 2.0. If a copy of the MPL was not distributed with this file, |
| # You can obtain one at http://mozilla.org/MPL/2.0/. |
| |
| """ |
| Command-line client to control a device |
| """ |
| |
| import errno |
| import logging |
| import os |
| import posixpath |
| import StringIO |
| import sys |
| import mozdevice |
| import mozlog |
| import argparse |
| |
| class DMCli(object): |
| |
| def __init__(self): |
| self.commands = { 'deviceroot': { 'function': self.deviceroot, |
| 'help': 'get device root directory for storing temporary files' }, |
| 'install': { 'function': self.install, |
| 'args': [ { 'name': 'file' } ], |
| 'help': 'push this package file to the device and install it' }, |
| 'uninstall': { 'function': self.uninstall, |
| 'args': [ { 'name': 'packagename' } ], |
| 'help': 'uninstall the named app from the device' }, |
| 'killapp': { 'function': self.kill, |
| 'args': [ { 'name': 'process_name', 'nargs': '*' } ], |
| 'help': 'kills any processes with name(s) on device' }, |
| 'launchapp': { 'function': self.launchapp, |
| 'args': [ { 'name': 'appname' }, |
| { 'name': 'activity_name' }, |
| { 'name': '--intent', |
| 'action': 'store', |
| 'default': 'android.intent.action.VIEW' }, |
| { 'name': '--url', |
| 'action': 'store' }, |
| { 'name': '--no-fail-if-running', |
| 'action': 'store_true', |
| 'help': 'Don\'t fail if application is already running' } |
| ], |
| 'help': 'launches application on device' }, |
| 'listapps': { 'function': self.listapps, |
| 'help': 'list applications on device' }, |
| 'push': { 'function': self.push, |
| 'args': [ { 'name': 'local_file' }, |
| { 'name': 'remote_file' } |
| ], |
| 'help': 'copy file/dir to device' }, |
| 'pull': { 'function': self.pull, |
| 'args': [ { 'name': 'local_file' }, |
| { 'name': 'remote_file', 'nargs': '?' } ], |
| 'help': 'copy file/dir from device' }, |
| 'shell': { 'function': self.shell, |
| 'args': [ { 'name': 'command', 'nargs': argparse.REMAINDER }, |
| { 'name': '--root', 'action': 'store_true', |
| 'help': 'Run command as root' }], |
| 'help': 'run shell command on device' }, |
| 'info': { 'function': self.getinfo, |
| 'args': [ { 'name': 'directive', 'nargs': '?' } ], |
| 'help': 'get information on specified ' |
| 'aspect of the device (if no argument ' |
| 'given, print all available information)' |
| }, |
| 'ps': { 'function': self.processlist, |
| 'help': 'get information on running processes on device' |
| }, |
| 'logcat' : { 'function': self.logcat, |
| 'help': 'get logcat from device' |
| }, |
| 'ls': { 'function': self.listfiles, |
| 'args': [ { 'name': 'remote_dir' } ], |
| 'help': 'list files on device' |
| }, |
| 'rm': { 'function': self.removefile, |
| 'args': [ { 'name': 'remote_file' } ], |
| 'help': 'remove file from device' |
| }, |
| 'isdir': { 'function': self.isdir, |
| 'args': [ { 'name': 'remote_dir' } ], |
| 'help': 'print if remote file is a directory' |
| }, |
| 'mkdir': { 'function': self.mkdir, |
| 'args': [ { 'name': 'remote_dir' } ], |
| 'help': 'makes a directory on device' |
| }, |
| 'rmdir': { 'function': self.rmdir, |
| 'args': [ { 'name': 'remote_dir' } ], |
| 'help': 'recursively remove directory from device' |
| }, |
| 'screencap': { 'function': self.screencap, |
| 'args': [ { 'name': 'png_file' } ], |
| 'help': 'capture screenshot of device in action' |
| }, |
| 'sutver': { 'function': self.sutver, |
| 'help': 'SUTAgent\'s product name and version (SUT only)' |
| }, |
| 'clearlogcat': { 'function': self.clearlogcat, |
| 'help': 'clear the logcat' |
| }, |
| 'reboot': { 'function': self.reboot, |
| 'help': 'reboot the device', |
| 'args': [ { 'name': '--wait', |
| 'action': 'store_true', |
| 'help': 'Wait for device to come back up before exiting' } ] |
| |
| }, |
| 'isfile': { 'function': self.isfile, |
| 'args': [ { 'name': 'remote_file' } ], |
| 'help': 'check whether a file exists on the device' |
| }, |
| 'launchfennec': { 'function': self.launchfennec, |
| 'args': [ { 'name': 'appname' }, |
| { 'name': '--intent', 'action': 'store', |
| 'default': 'android.intent.action.VIEW' }, |
| { 'name': '--url', 'action': 'store' }, |
| { 'name': '--extra-args', 'action': 'store' }, |
| { 'name': '--mozenv', 'action': 'store', |
| 'help': 'Gecko environment variables to set in "KEY1=VAL1 KEY2=VAL2" format' }, |
| { 'name': '--no-fail-if-running', |
| 'action': 'store_true', |
| 'help': 'Don\'t fail if application is already running' } |
| ], |
| 'help': 'launch fennec' |
| }, |
| 'getip': { 'function': self.getip, |
| 'args': [ { 'name': 'interface', 'nargs': '*' } ], |
| 'help': 'get the ip address of the device' |
| } |
| } |
| |
| self.parser = argparse.ArgumentParser() |
| self.add_options(self.parser) |
| self.add_commands(self.parser) |
| mozlog.commandline.add_logging_group(self.parser) |
| |
| def run(self, args=sys.argv[1:]): |
| args = self.parser.parse_args() |
| |
| mozlog.commandline.setup_logging( |
| 'mozdevice', args, {'mach': sys.stdout}) |
| |
| if args.dmtype == "sut" and not args.host and not args.hwid: |
| self.parser.error("Must specify device ip in TEST_DEVICE or " |
| "with --host option with SUT") |
| |
| self.dm = self.getDevice(dmtype=args.dmtype, hwid=args.hwid, |
| host=args.host, port=args.port, |
| verbose=args.verbose) |
| |
| ret = args.func(args) |
| if ret is None: |
| ret = 0 |
| |
| sys.exit(ret) |
| |
| def add_options(self, parser): |
| parser.add_argument("-v", "--verbose", action="store_true", |
| help="Verbose output from DeviceManager", |
| default=bool(os.environ.get('VERBOSE'))) |
| parser.add_argument("--host", action="store", |
| help="Device hostname (only if using TCP/IP, " \ |
| "defaults to TEST_DEVICE environment " \ |
| "variable if present)", |
| default=os.environ.get('TEST_DEVICE')) |
| parser.add_argument("-p", "--port", action="store", |
| type=int, |
| help="Custom device port (if using SUTAgent or " |
| "adb-over-tcp)", default=None) |
| parser.add_argument("-m", "--dmtype", action="store", |
| help="DeviceManager type (adb or sut, defaults " \ |
| "to DM_TRANS environment variable, if " \ |
| "present, or adb)", |
| default=os.environ.get('DM_TRANS', 'adb')) |
| parser.add_argument("-d", "--hwid", action="store", |
| help="HWID", default=None) |
| parser.add_argument("--package-name", action="store", |
| help="Packagename (if using DeviceManagerADB)", |
| default=None) |
| |
| def add_commands(self, parser): |
| subparsers = parser.add_subparsers(title="Commands", metavar="<command>") |
| for (commandname, commandprops) in sorted(self.commands.iteritems()): |
| subparser = subparsers.add_parser(commandname, help=commandprops['help']) |
| if commandprops.get('args'): |
| for arg in commandprops['args']: |
| # this is more elegant but doesn't work in python 2.6 |
| # (which we still use on tbpl @ mozilla where we install |
| # this package) |
| # kwargs = { k: v for k,v in arg.items() if k is not 'name' } |
| kwargs = {} |
| for (k, v) in arg.items(): |
| if k is not 'name': |
| kwargs[k] = v |
| subparser.add_argument(arg['name'], **kwargs) |
| subparser.set_defaults(func=commandprops['function']) |
| |
| def getDevice(self, dmtype="adb", hwid=None, host=None, port=None, |
| packagename=None, verbose=False): |
| ''' |
| Returns a device with the specified parameters |
| ''' |
| logLevel = logging.ERROR |
| if verbose: |
| logLevel = logging.DEBUG |
| |
| if hwid: |
| return mozdevice.DroidConnectByHWID(hwid, logLevel=logLevel) |
| |
| if dmtype == "adb": |
| if host and not port: |
| port = 5555 |
| return mozdevice.DroidADB(packageName=packagename, |
| host=host, port=port, |
| logLevel=logLevel) |
| elif dmtype == "sut": |
| if not host: |
| self.parser.error("Must specify host with SUT!") |
| if not port: |
| port = 20701 |
| return mozdevice.DroidSUT(host=host, port=port, |
| logLevel=logLevel) |
| else: |
| self.parser.error("Unknown device manager type: %s" % type) |
| |
| def deviceroot(self, args): |
| print self.dm.deviceRoot |
| |
| def push(self, args): |
| (src, dest) = (args.local_file, args.remote_file) |
| if os.path.isdir(src): |
| self.dm.pushDir(src, dest) |
| else: |
| dest_is_dir = dest[-1] == '/' or self.dm.dirExists(dest) |
| dest = posixpath.normpath(dest) |
| if dest_is_dir: |
| dest = posixpath.join(dest, os.path.basename(src)) |
| self.dm.pushFile(src, dest) |
| |
| def pull(self, args): |
| (src, dest) = (args.local_file, args.remote_file) |
| if not self.dm.fileExists(src): |
| print 'No such file or directory' |
| return |
| if not dest: |
| dest = posixpath.basename(src) |
| if self.dm.dirExists(src): |
| self.dm.getDirectory(src, dest) |
| else: |
| self.dm.getFile(src, dest) |
| |
| def install(self, args): |
| basename = os.path.basename(args.file) |
| app_path_on_device = posixpath.join(self.dm.deviceRoot, |
| basename) |
| self.dm.pushFile(args.file, app_path_on_device) |
| self.dm.installApp(app_path_on_device) |
| |
| def uninstall(self, args): |
| self.dm.uninstallApp(args.packagename) |
| |
| def launchapp(self, args): |
| self.dm.launchApplication(args.appname, args.activity_name, |
| args.intent, url=args.url, |
| failIfRunning=(not args.no_fail_if_running)) |
| |
| def listapps(self, args): |
| for app in self.dm.getInstalledApps(): |
| print app |
| |
| def stopapp(self, args): |
| self.dm.stopApplication(args.appname) |
| |
| def kill(self, args): |
| for name in args.process_name: |
| self.dm.killProcess(name) |
| |
| def shell(self, args): |
| buf = StringIO.StringIO() |
| self.dm.shell(args.command, buf, root=args.root) |
| print str(buf.getvalue()[0:-1]).rstrip() |
| |
| def getinfo(self, args): |
| info = self.dm.getInfo(directive=args.directive) |
| for (infokey, infoitem) in sorted(info.iteritems()): |
| if infokey == "process": |
| pass # skip process list: get that through ps |
| elif args.directive is None: |
| print "%s: %s" % (infokey.upper(), infoitem) |
| else: |
| print infoitem |
| |
| def logcat(self, args): |
| print ''.join(self.dm.getLogcat()) |
| |
| def clearlogcat(self, args): |
| self.dm.recordLogcat() |
| |
| def reboot(self, args): |
| self.dm.reboot(wait=args.wait) |
| |
| def processlist(self, args): |
| pslist = self.dm.getProcessList() |
| for ps in pslist: |
| print " ".join(str(i) for i in ps) |
| |
| def listfiles(self, args): |
| filelist = self.dm.listFiles(args.remote_dir) |
| for file in filelist: |
| print file |
| |
| def removefile(self, args): |
| self.dm.removeFile(args.remote_file) |
| |
| def isdir(self, args): |
| if self.dm.dirExists(args.remote_dir): |
| print "TRUE" |
| return |
| |
| print "FALSE" |
| return errno.ENOTDIR |
| |
| def mkdir(self, args): |
| self.dm.mkDir(args.remote_dir) |
| |
| def rmdir(self, args): |
| self.dm.removeDir(args.remote_dir) |
| |
| def screencap(self, args): |
| self.dm.saveScreenshot(args.png_file) |
| |
| def sutver(self, args): |
| if args.dmtype == 'sut': |
| print '%s Version %s' % (self.dm.agentProductName, |
| self.dm.agentVersion) |
| else: |
| print 'Must use SUT transport to get SUT version.' |
| |
| def isfile(self, args): |
| if self.dm.fileExists(args.remote_file): |
| print "TRUE" |
| return |
| print "FALSE" |
| return errno.ENOENT |
| |
| def launchfennec(self, args): |
| mozEnv = None |
| if args.mozenv: |
| mozEnv = {} |
| keyvals = args.mozenv.split() |
| for keyval in keyvals: |
| (key, _, val) = keyval.partition("=") |
| mozEnv[key] = val |
| self.dm.launchFennec(args.appname, intent=args.intent, |
| mozEnv=mozEnv, |
| extraArgs=args.extra_args, url=args.url, |
| failIfRunning=(not args.no_fail_if_running)) |
| |
| def getip(self, args): |
| if args.interface: |
| print(self.dm.getIP(args.interface)) |
| else: |
| print(self.dm.getIP()) |
| |
| def cli(args=sys.argv[1:]): |
| # process the command line |
| cli = DMCli() |
| cli.run(args) |
| |
| if __name__ == '__main__': |
| cli() |