diff --git a/backends/webos/app-template/appinfo.json b/backends/webos/app-template/appinfo.json new file mode 100644 index 0000000..7d0fdae --- /dev/null +++ b/backends/webos/app-template/appinfo.json @@ -0,0 +1,10 @@ +{ + "id": "com.domain.app", + "version": "0.0.1", + "vendor": "My Company", + "type": "web", + "main": "index.html", + "title": "new app", + "icon": "icon.png", + "largeIcon": "largeIcon.png" +} \ No newline at end of file diff --git a/backends/webos/app-template/icon.png b/backends/webos/app-template/icon.png new file mode 100755 index 0000000..c1742ed Binary files /dev/null and b/backends/webos/app-template/icon.png differ diff --git a/backends/webos/app-template/index.html b/backends/webos/app-template/index.html new file mode 100644 index 0000000..43d4440 --- /dev/null +++ b/backends/webos/app-template/index.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/backends/webos/app-template/largeIcon.png b/backends/webos/app-template/largeIcon.png new file mode 100755 index 0000000..1e8d86e Binary files /dev/null and b/backends/webos/app-template/largeIcon.png differ diff --git a/backends/webos/package-lock.json b/backends/webos/package-lock.json new file mode 100644 index 0000000..1499d1c --- /dev/null +++ b/backends/webos/package-lock.json @@ -0,0 +1,286 @@ +{ + "name": "webos-webdriver-server", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "webos-webdriver-server", + "version": "1.0.0", + "license": "Apache-2.0", + "workspaces": [ + "../../base" + ], + "dependencies": { + "generic-webdriver-server": "^1.1.3", + "tmp-promise": "^3.0.2", + "wol": "^1.0.7" + }, + "bin": { + "webos-webdriver-cli": "webos-webdriver-cli.js", + "webos-webdriver-server": "webos-webdriver-server.js" + } + }, + "../../base": { + "version": "1.1.4", + "license": "Apache-2.0", + "dependencies": { + "express": "^4.17.3", + "yargs": "^17.4.0" + }, + "devDependencies": { + "eslint": "^8.11.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/generic-webdriver-server": { + "resolved": "../../base", + "link": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/wol": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/wol/-/wol-1.0.7.tgz", + "integrity": "sha512-kg7ETY8g3V5+3GVhUfWCVjeXuCmfrX6xfw4cw4c88+MtoxkbFmcs9Y5yhT1wwOL8inogFUQZ8JMzH9OekaaawQ==", + "bin": { + "wake": "bin/wake" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + }, + "dependencies": { + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "generic-webdriver-server": { + "version": "file:../../base", + "requires": { + "eslint": "^8.11.0", + "express": "^4.17.3", + "yargs": "^17.4.0" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "requires": { + "rimraf": "^3.0.0" + } + }, + "tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "requires": { + "tmp": "^0.2.0" + } + }, + "wol": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/wol/-/wol-1.0.7.tgz", + "integrity": "sha512-kg7ETY8g3V5+3GVhUfWCVjeXuCmfrX6xfw4cw4c88+MtoxkbFmcs9Y5yhT1wwOL8inogFUQZ8JMzH9OekaaawQ==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } +} diff --git a/backends/webos/package.json b/backends/webos/package.json new file mode 100644 index 0000000..59257fa --- /dev/null +++ b/backends/webos/package.json @@ -0,0 +1,39 @@ +{ + "name": "webos-webdriver-server", + "description": "A webOS WebDriver server that pushes URLs to webOS devices, built on generic-webdriver-server.", + "version": "1.0.0", + "homepage": "https://github.com/shaka-project/generic-webdriver-server", + "author": "Google", + "license": "Apache-2.0", + "maintainers": [ + { + "name": "Joey Parrish", + "email": "joeyparrish@google.com" + } + ], + "keywords": [ + "karma", + "selenium", + "webos", + "webdriver" + ], + "main": "webos-webdriver-server.js", + "scripts": { + "lint": "eslint --ignore-path ../../.gitignore --max-warnings 0 .", + "checkClean": "test -z \"$(git status --short .)\" || (echo \"Git not clean!\"; git status .; exit 1)", + "test": "npm run lint", + "prepack": "npm run lint && npm run checkClean" + }, + "bin": { + "webos-webdriver-cli": "./webos-webdriver-cli.js", + "webos-webdriver-server": "./webos-webdriver-server.js" + }, + "dependencies": { + "generic-webdriver-server": "^1.1.3", + "tmp-promise": "^3.0.2", + "wol": "^1.0.7" + }, + "workspaces": [ + "../../base" + ] +} diff --git a/backends/webos/webos-utils.js b/backends/webos/webos-utils.js new file mode 100644 index 0000000..ffbb3a9 --- /dev/null +++ b/backends/webos/webos-utils.js @@ -0,0 +1,117 @@ +const fs = require('fs') +const os = require('os') +const path = require('path') +const tmp = require('tmp-promise') +const util = require('util') +const wol = require('wol') + +const execFile = util.promisify(require('child_process').execFile) + +const hostAppTemplatePath = path.resolve(__dirname, 'app-template') + +/** + * Uses webOS CLI to connect to a webOS device, install a temporary container + * app, and load a URL into it. + * + * @param {!object} flags Parsed command-line flags. + * @param {Console} log A Console-like interface for logging. Can be "console". + * @param {?string} url If non-null, install the webOS container app and load + * the URL into it. If null, send the webOS device back to the home screen. + * @return {!Promise} + */ +async function loadOnWebos(flags, log, url) { + /** + * We build a set of commands which will be combined and executed in bash -c. + * For locally-installed copies of webOS CLI, these will be executed in + * bash directly. + * + * @type {!Array} + */ + let commands = [] + + const appTemplatePath = hostAppTemplatePath + + commands = commands.concat([ + //If the target device with a given name already exists, remove it. + `if "${flags.webosCliPath}"/bin/ares-setup-device --list | grep -q "${flags.deviceName}"; then "${flags.webosCliPath}"/bin/ares-setup-device --remove ${flags.deviceName};fi`, + + //Add the target device + `"${flags.webosCliPath}"/bin/ares-setup-device --add ${flags.deviceName} -i host="${flags.hostname}"`, + + //In order to connect your device to a webOS TV, pair them with passphrase. Because webOS CLI sends a prompt asking for passphrase here, I create an expect(.exp) file here and execute it. + `printf '#!/usr/bin/expect -f\nset timeout -1\nspawn ${flags.webosCliPath}/bin/ares-novacom --device ${flags.deviceName} --getkey\nexpect "input passphrase"\nsend -- ${flags.passphrase}; send "\r"\nexpect eof' > ${flags.deviceName}.exp`, + + `chmod +x ./${flags.deviceName}.exp`, + + `./${flags.deviceName}.exp`, + + `rm ./${flags.deviceName}.exp` + ]) + + if (url) { + // Since the URL will be quoted and used inside a bash command, we need a + // quote-safe version of the URL with some escaping applied. This + // replacement will make the URL safe for inclusion in single quotes. + // See https://stackoverflow.com/a/1250279 for an explanation. + const quoteSafeUrl = url.replace(`'`, `'"'"'`) + + commands = commands.concat([ + // Create a temporary directory for the webOS application source. + `TMP_APP=$(mktemp -d -t webos-webdriver-XXXXXXXX)`, + // Copy the application template to the temporary directory. + `cp ${appTemplatePath}/* "$TMP_APP"`, + // Replace the location href address with the requested URL. + // Warning: command below is changed, because it doesn't work in macOS. + // see https://stackoverflow.com/a/21243111 + `sed -i '' 's@DESTINATION@${quoteSafeUrl}@' "$TMP_APP"/index.html`, + // Package the webOS application. + `"${flags.webosCliPath}"/bin/ares-package "$TMP_APP" -o "$TMP_APP"`, + // Install the new version of the container app. + `"${flags.webosCliPath}"/bin/ares-install -d ${flags.deviceName} "$TMP_APP"/com.domain.app_0.0.1_all.ipk`, + // Run the container app. + `"${flags.webosCliPath}"/bin/ares-launch -d ${flags.deviceName} com.domain.app`, + ]) + } + + // Join the commands with "&&" to run them in a sequence until one fails. + const command = commands.join(' && ') + + await execFile('bash', ['-c', command]) +} + +/** + * Add webOS-specific arguments to the application's argument parser (from the + * "yargs" module). + * + * @param {Yargs} yargs The argument parser object from "yargs". + */ +function addWebosArgs(yargs) { + yargs + .option('hostname', { + description: 'The webOS hostname or IP address', + type: 'string', + demandOption: true, + }) + .option('webos-cli-path', { + description: 'The directory of the webOS CLI', + type: 'string', + demandOption: true, + }) + .option('device-name', { + description: 'Device name of your choice', + type: 'string', + default: 'webos_tv', + }) + .option('passphrase', { + description: + 'Passphrase from webOS "Developer App" on target device. You have to switch on "Key Server" to successfully authenticate your computer with passphrase. See https://webostv.developer.lge.com/develop/app-test/using-devmode-app/#connectingTVandPC', + type: 'string', + demandOption: true, + }) +} + + +module.exports = { + loadOnWebos, + addWebosArgs, +} diff --git a/backends/webos/webos-webdriver-cli.js b/backends/webos/webos-webdriver-cli.js new file mode 100755 index 0000000..188e03b --- /dev/null +++ b/backends/webos/webos-webdriver-cli.js @@ -0,0 +1,33 @@ +const yargs = require('yargs'); +const {loadOnWebos, addWebosArgs} = require('./webos-utils'); + +yargs + .strict() + .usage('Usage: $0 --hostname= --url=') + .usage(' or: $0 --hostname= --home') + .option('url', { + description: 'A URL to direct the webOS device to.\n' + + 'Either --url or --home must be specified.', + type: 'string', + }) + .option('home', { + description: 'Direct the webOS device to the home screen.\n' + + 'Either --url or --home must be specified.', + type: 'boolean', + }) + // You can't use both at once. + .conflicts('url', 'home') + .check((flags) => { + // Enforce that one or the other is specified. + if (!flags.url && !flags.home) { + throw new Error('Either --url or --home must be specified.'); + } + + return true; + }); + +addWebosArgs(yargs); + +const flags = yargs.argv; + +loadOnWebos(flags, /* log= */ console, flags.url); diff --git a/backends/webos/webos-webdriver-server.js b/backends/webos/webos-webdriver-server.js new file mode 100755 index 0000000..bf8e06c --- /dev/null +++ b/backends/webos/webos-webdriver-server.js @@ -0,0 +1,28 @@ +const { + GenericSingleSessionWebDriverServer, + yargs, +} = require('generic-webdriver-server'); + +const {loadOnWebos, addWebosArgs} = require('./webos-utils'); + +/** WebDriver server backend for webOS */ +class WebosWebDriverServer extends GenericSingleSessionWebDriverServer { + constructor() { + super(); + } + + /** @override */ + async navigateToSingleSession(url) { + await loadOnWebos(this.flags, this.log, url); + } + + /** @override */ + async closeSingleSession() { + // Send the device back to the home screen. + await loadOnWebos(this.flags, this.log, null); + } +} + +addWebosArgs(yargs); +const server = new WebosWebDriverServer(); +server.listen();