├── container ├── icon.png ├── res │ ├── connected.png │ └── disconnected.png ├── index.html ├── base.html ├── css │ ├── 1280 │ │ └── style.css │ └── 1920 │ │ └── style.css ├── js │ └── base.js └── LICENSE ├── .npmignore ├── .gitignore ├── lib ├── wrapper │ └── baseFilesystemWrapper.js ├── regexp.js ├── logger.js ├── cli.js ├── deviceConnectHelper.js ├── util.js ├── appLaunchHelper.js ├── certificationHelper.js ├── watchHelper.js ├── hostAppHelper.js └── userInfoHelper.js ├── package.json ├── command ├── watch.js ├── start.js ├── certificate.js └── init.js ├── index.js ├── doc ├── README_zh_HANS.md └── README_zh_HANT.md ├── README.md └── LICENSE /container/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/Wits/HEAD/container/icon.png -------------------------------------------------------------------------------- /container/res/connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/Wits/HEAD/container/res/connected.png -------------------------------------------------------------------------------- /container/res/disconnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/Wits/HEAD/container/res/disconnected.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | .git 3 | archive/ 4 | container.zip 5 | tools.zip 6 | resource.zip 7 | container/.resource 8 | container/build 9 | .DS_Store 10 | resource 11 | container/js/main.js 12 | container/config.xml 13 | container/WITs.wgt 14 | lib/wrapper/filesystemWrapper.js 15 | lib/wrapper/*.html 16 | test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | Wits.wgt 4 | www/ 5 | container.zip 6 | tools.zip 7 | resource.zip 8 | container/.resource 9 | container/build 10 | .DS_Store 11 | resource 12 | container/js/main.js 13 | container/config.xml 14 | container/WITs.wgt 15 | lib/wrapper/filesystemWrapper.js 16 | lib/wrapper/*.html 17 | test -------------------------------------------------------------------------------- /container/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Wits 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
Connected
32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /container/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Wits 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
Connected
32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /lib/wrapper/baseFilesystemWrapper.js: -------------------------------------------------------------------------------- 1 | var fsWrappingAPIList = [ 2 | 'openFile', 3 | 'createDirectory', 4 | 'deleteFile', 5 | 'deleteDirectory', 6 | 'copyFile', 7 | 'copyDirectory', 8 | 'moveFile', 9 | 'moveDirectory', 10 | 'rename', 11 | 'listDirectory', 12 | 'toURI', 13 | 'isFile', 14 | 'isDirectory', 15 | 'pathExists', 16 | 'resolve' 17 | ]; 18 | (function () { 19 | makeFsWrapper(); 20 | })(); 21 | 22 | function makeFsWrapper() { 23 | var originalTizenFs = {}; 24 | fsWrappingAPIList.forEach(function (apiName) { 25 | originalTizenFs[apiName] = deepClone(tizen.filesystem[apiName]); 26 | tizen.filesystem[apiName] = function () { 27 | if (typeof arguments[0] === 'string') { 28 | arguments[0] = arguments[0].replace( 29 | 'wgt-package', 30 | '{{CONTENT_PATH}}' 31 | ); 32 | } 33 | return originalTizenFs[apiName].apply(this, arguments); 34 | }; 35 | }); 36 | } 37 | 38 | function deepClone(obj) { 39 | if (obj === null || typeof obj !== 'object') { 40 | return obj; 41 | } 42 | 43 | var result = Array.isArray(obj) ? [] : {}; 44 | 45 | for (var key of Object.keys(obj)) { 46 | result[key] = deepClone(obj[key]); 47 | } 48 | 49 | return result; 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tizentv/wits", 3 | "version": "2.5.9", 4 | "description": "Instant live reload tool for Tizen Web Application development", 5 | "main": "index.js", 6 | "author": "Samsung Electronics Co., Ltd.", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Samsung/Wits.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/Samsung/Wits/issues" 13 | }, 14 | "homepage": "https://github.com/Samsung/Wits/issues", 15 | "dependencies": { 16 | "@tizentv/tools": "^1.1.3", 17 | "@tizentv/webide-common-tizentv": "^1.0.17", 18 | "chalk": "^4.1.0", 19 | "chrome-launcher": "^0.13.2", 20 | "commander": "^4.1.1", 21 | "express": "^4.16.3", 22 | "inquirer": "=5.2.0", 23 | "ip": "^1.1.5", 24 | "lodash": "^4.17.15", 25 | "mkdirp": "^1.0.3", 26 | "node-html-parser": "^1.3.1", 27 | "node-watch": "^0.6.2", 28 | "recursive-readdir": "^2.2.2", 29 | "socket.io": "^3.1.0", 30 | "xml2js": "^0.4.19" 31 | }, 32 | "license": "ISC", 33 | "keywords": [ 34 | "Wits", 35 | "Tizen", 36 | "Web developer", 37 | "live reload", 38 | "debugging tool", 39 | "samsung", 40 | "tizen web app", 41 | "samsung live reload", 42 | "samsung tizen tv", 43 | "tizen web application", 44 | "tizen web application developer", 45 | "tool" 46 | ], 47 | "bin": { 48 | "wits": "./lib/cli.js" 49 | }, 50 | "preferGlobal": true, 51 | "private": false 52 | } 53 | -------------------------------------------------------------------------------- /lib/regexp.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | REMOTE_URI: new RegExp( 3 | /(http|ftp|https):\/\/([\w+?\.\w+])+([a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\,\,]*)?/ 4 | ), 5 | COMMENT: new RegExp(//g), 6 | HOST_DATA: new RegExp( 7 | /({{CONTENT_PATH}})|({{HOST_IP}})|({{HOST_PORT}})|({{CONTENT_SRC}})|({{HOST_BASE_CONTENT_PATH}})/g 8 | ), 9 | HOST_WIDTH: new RegExp(/({{HOST_WIDTH}})/g), 10 | CONTENT_SRC: new RegExp(/(content*.*src=("|')*.*("|'))/gi), 11 | CONTENT_SRC_ATTRIBUTE: new RegExp(/((content*.*src=)|"|')/gi), 12 | APPLICATION_ID: new RegExp(/(tizen:application*.*id=("|')\w+.\w+)("|')/gi), 13 | APPLICATION_ID_ATTRIBUTE: new RegExp(/((tizen:application*.*id=)|"|')/gi), 14 | PRIVILEGE: new RegExp(/tizen:privilege/), 15 | DEBUG_PORT: new RegExp(/(port(.*):\s+\d+)/g), 16 | BACKSLASH: new RegExp(/\\/gi), 17 | NUMBER_ONLY: new RegExp(/^\d+(?!\w+)/g), 18 | IP_ADDRESS: new RegExp( 19 | /^(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))$/ 20 | ), 21 | PROXY: new RegExp( 22 | /(http|https):\/\/+(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])):\d{1,5}/gi 23 | ), 24 | NUMBER_WORD: new RegExp(/\d+/), 25 | FIRST_BACKSLASH: new RegExp(/^(\/)/), 26 | FIND_CR: new RegExp(/\r/), 27 | FIND_ALL_CR: new RegExp(/\r/g), 28 | PUSHED_FILE_MESSAGE: RegExp(/(pushed)*.*(100%)/), 29 | CONTAIN_WHITESPACE: new RegExp(/\s+|\s/g) 30 | }; 31 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | const outputChannel = []; 2 | 3 | const setOutputChannel = output => { 4 | if (typeof output !== 'function') { 5 | throw new Error(`output should be function`); 6 | } 7 | const id = generateId(); 8 | outputChannel.push({ id, output }); 9 | return id; 10 | }; 11 | 12 | function generateId() { 13 | const min = Math.ceil(1000); 14 | const max = Math.floor(9999); 15 | return Math.floor(Math.random() * (max - min)) + min; 16 | } 17 | 18 | const unsetOutputChannel = requestId => { 19 | const removedId = outputChannel.findIndex(({ id }) => requestId === id); 20 | if (removedId < 0) { 21 | console.error(`The request id(${requestId}) is not exist`); 22 | return; 23 | } 24 | outputChannel.splice(removedId, 1); 25 | }; 26 | 27 | function error() { 28 | const args = [].slice.call(arguments); 29 | const outputCallback = outputChannel.map(({ output }) => output); 30 | [console.error, ...outputCallback].forEach(print => { 31 | print(...args); 32 | }); 33 | } 34 | 35 | function warn() { 36 | const args = [].slice.call(arguments); 37 | const outputCallback = outputChannel.map(({ output }) => output); 38 | [console.warn, ...outputCallback].forEach(print => { 39 | print(...args); 40 | }); 41 | } 42 | 43 | function log() { 44 | const args = [].slice.call(arguments); 45 | const outputCallback = outputChannel.map(({ output }) => output); 46 | [console.log, ...outputCallback].forEach(print => { 47 | print(...args); 48 | }); 49 | } 50 | 51 | function debug() { 52 | const args = [].slice.call(arguments); 53 | console.debug(...args); 54 | } 55 | 56 | const logger = { 57 | error, 58 | warn, 59 | log, 60 | debug 61 | }; 62 | 63 | module.exports = { 64 | setOutputChannel, 65 | unsetOutputChannel, 66 | logger 67 | }; 68 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const commander = require('commander'); 4 | const program = new commander.Command(); 5 | const package = require('../package.json'); 6 | const { logger } = require('./logger'); 7 | 8 | const util = require('./util.js'); 9 | 10 | process.on('SIGINT', () => { 11 | // ctrl + C 12 | const watchHelper = require('./watchHelper.js'); 13 | watchHelper.closeSocketServer(); 14 | util.exit(); 15 | }); 16 | 17 | util.displayBanner(); 18 | 19 | program.version(`wits v${package.version}`, '-v, --version', 'WITs version'); 20 | program.allowUnknownOption(); 21 | 22 | program.option('-i, --init', 'Set configuration for running WITs.'); 23 | program.option( 24 | '-c, --certificate', 25 | 'Generate a certification for signing Tizen web app. You can set a log level for debugging. ex) wits -c --verbose' 26 | ); 27 | program.option( 28 | '-s, --start [deviceIp]', 29 | 'Install, launch app and enable live reload feature in a sequence. ex) wits -s / wits -s deviceIp=192.168.250.250 / wits -s --verbose' 30 | ); 31 | program.option( 32 | '-w, --watch [deviceIp]', 33 | 'Launch app and enable live reload feature without reinstalling. ex) wits -w / wits -w deviceIp=192.168.250.250 / wits -w --verbose' 34 | ); 35 | program.parse(process.argv); 36 | util.ISVERVOSE = checkVerbose(process.argv); 37 | 38 | if (program.init) { 39 | const initCommand = require('../command/init.js'); 40 | initCommand.run(); 41 | } else if (program.certificate) { 42 | const certificateCommand = require('../command/certificate.js'); 43 | certificateCommand.run(program.certificate); 44 | } else if (program.start) { 45 | const startCommand = require('../command/start.js'); 46 | startCommand.run(program.start); 47 | } else if (program.watch) { 48 | const watchCommand = require('../command/watch.js'); 49 | watchCommand.run(program.watch); 50 | } else { 51 | program.help(); 52 | } 53 | 54 | function checkVerbose(arguments) { 55 | const last = arguments.length - 1; 56 | if (arguments.includes('--verbose')) { 57 | if (arguments[last] !== '--verbose') { 58 | logger.log( 59 | `[Warning] Please check the options' order. "--verbose" should be at the end of command.` 60 | ); 61 | } 62 | return true; 63 | } 64 | return false; 65 | } 66 | -------------------------------------------------------------------------------- /command/watch.js: -------------------------------------------------------------------------------- 1 | const userInfoHelper = require('../lib/userInfoHelper.js'); 2 | const hostAppHelper = require('../lib/hostAppHelper.js'); 3 | const appLaunchHelper = require('../lib/appLaunchHelper.js'); 4 | const watchHelper = require('../lib/watchHelper.js'); 5 | const util = require('../lib/util.js'); 6 | const chalk = require('chalk'); 7 | const { logger } = require('../lib/logger'); 8 | 9 | module.exports = { 10 | run: async option => { 11 | logger.log( 12 | chalk.cyanBright(`Start running Wits watch mode............\n`) 13 | ); 14 | 15 | await util.initTools(); 16 | 17 | const data = await userInfoHelper.getLatestWitsconfigInfo() 18 | .connectionInfo; 19 | 20 | if (option !== undefined) { 21 | await supportDeviceIpOption(data, option); 22 | } 23 | 24 | const baseAppPath = userInfoHelper.getBaseAppPath(data.baseAppPath); 25 | const deviceInfo = await userInfoHelper.getDeviceInfo(data.deviceIp); 26 | 27 | const hostAppId = hostAppHelper.getHostAppId(baseAppPath); 28 | const deviceName = deviceInfo.deviceName; 29 | data.baseAppPath = baseAppPath; 30 | 31 | watchHelper.openSocketServer(data, deviceInfo); 32 | // appLaunchHelper.terminateApp(deviceName, hostAppId); 33 | 34 | try { 35 | data.isDebugMode 36 | ? appLaunchHelper.launchDebugMode( 37 | deviceName, 38 | hostAppId, 39 | data.deviceIp 40 | ) 41 | : appLaunchHelper.launchApp(deviceName, hostAppId); 42 | } catch (e) { 43 | logger.log(e); 44 | util.exit(); 45 | } 46 | } 47 | }; 48 | 49 | async function supportDeviceIpOption(data, option) { 50 | const optionDeviceIp = util.parseDeviceIp(option); 51 | if (optionDeviceIp === null && typeof option !== 'boolean') { 52 | logger.error( 53 | chalk.red( 54 | `Invalid Type of cli option. Please retry with correct type. ex) deviceIp=0.0.0.0` 55 | ) 56 | ); 57 | util.exit(); 58 | } 59 | 60 | if (optionDeviceIp) { 61 | data.deviceIp = optionDeviceIp; 62 | logger.log( 63 | chalk.cyanBright(`WITs tries connecting with ${optionDeviceIp}\n`) 64 | ); 65 | await userInfoHelper.updateLatestUserAnswer({ 66 | deviceIp: optionDeviceIp 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const util = require('./lib/util.js'); 2 | const userInfoHelper = require('./lib/userInfoHelper.js'); 3 | const { 4 | setOutputChannel, 5 | unsetOutputChannel, 6 | logger 7 | } = require('./lib/logger'); 8 | 9 | const setWitsconfigInfo = async data => { 10 | try { 11 | const initCommand = require('./command/init.js'); 12 | logger.log('WITs::setWitsconfigInfo'); 13 | 14 | if (data.hasOwnProperty('baseAppPath')) { 15 | util.CURRENT_PROJECT_PATH = userInfoHelper.getBaseAppPath( 16 | data.baseAppPath 17 | ); 18 | } 19 | 20 | if (data.hasOwnProperty('profilePath')) { 21 | if (!(await initCommand.isVaildProfile(data.profilePath))) { 22 | throw new Error( 23 | 'There is invalid profile. Please create a profile or active the profile.' 24 | ); 25 | } 26 | } 27 | 28 | await initCommand.prepareConfigure(); 29 | await userInfoHelper.updateLatestUserAnswer(data); 30 | return; 31 | /** 32 | { 33 | width: '1920', 34 | deviceIp: '192.168.250.250', 35 | hostIp: '192.168.250.250', 36 | baseAppPath: 'E:/dev/workspace/test', 37 | isDebugMode: false, 38 | profilePath: 'C:/tizen-studio-data/profile/profiles.xml', 39 | } 40 | */ 41 | } catch (error) { 42 | logger.log(`setWitsconfigInfo:::${error}`); 43 | } 44 | }; 45 | 46 | const start = async () => { 47 | try { 48 | if (!userInfoHelper.WITS_USER_DATA) { 49 | throw new Error('There is invalid WITS_USER_DATA'); 50 | } 51 | const startCommand = require('./command/start.js'); 52 | logger.log('WITs::start'); 53 | 54 | await startCommand.run(); 55 | return; 56 | } catch (error) { 57 | logger.log(`start:::${error}`); 58 | } 59 | }; 60 | 61 | const watch = async () => { 62 | try { 63 | if (!userInfoHelper.WITS_USER_DATA) { 64 | throw new Error('There is invalid WITS_USER_DATA'); 65 | } 66 | const watchCommand = require('./command/watch.js'); 67 | logger.log('WITs::watch'); 68 | 69 | await watchCommand.run(); 70 | return; 71 | } catch (error) { 72 | logger.log(`watch:::${error}`); 73 | } 74 | }; 75 | 76 | const disconnect = () => { 77 | const watchHelper = require('./lib/watchHelper.js'); 78 | 79 | try { 80 | watchHelper.closeSocketServer(); 81 | } catch (error) { 82 | logger.log(`disconnect:::${error}`); 83 | } 84 | }; 85 | 86 | module.exports = { 87 | setWitsconfigInfo, 88 | start, 89 | watch, 90 | disconnect, 91 | setOutputChannel, 92 | unsetOutputChannel 93 | }; 94 | -------------------------------------------------------------------------------- /command/start.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const util = require('../lib/util.js'); 3 | const userInfoHelper = require('../lib/userInfoHelper.js'); 4 | const hostAppHelper = require('../lib/hostAppHelper.js'); 5 | const appLaunchHelper = require('../lib/appLaunchHelper.js'); 6 | const watchHelper = require('../lib/watchHelper.js'); 7 | const { logger } = require('../lib/logger'); 8 | 9 | module.exports = { 10 | run: async option => { 11 | logger.log(chalk.cyanBright(`Start running Wits............\n`)); 12 | 13 | await util.initTools(); 14 | 15 | const data = userInfoHelper.getRefinedData(); 16 | 17 | if (option !== undefined) { 18 | await supportDeviceIpOption(data, option); 19 | } 20 | 21 | let deviceInfo = ''; 22 | 23 | try { 24 | deviceInfo = await userInfoHelper.getDeviceInfo(data.deviceIp); 25 | } catch (error) { 26 | logger.log(`Failed to getDeviceInfo: ${error}`); 27 | } 28 | 29 | await hostAppHelper.setHostAppEnv(data, deviceInfo); 30 | hostAppHelper 31 | .buildPackage() 32 | .then(() => { 33 | logger.log( 34 | chalk.cyanBright( 35 | '============================== Start to install the package' 36 | ) 37 | ); 38 | 39 | const hostAppId = hostAppHelper.getHostAppId(data.baseAppPath); 40 | const hostAppName = hostAppId.split('.')[1]; 41 | const deviceName = deviceInfo.deviceName; 42 | 43 | appLaunchHelper.unInstallPackage(deviceName, hostAppName); 44 | appLaunchHelper.installPackage(deviceInfo, hostAppName); 45 | watchHelper.openSocketServer(data, deviceInfo); 46 | try { 47 | data.isDebugMode 48 | ? appLaunchHelper.launchDebugMode( 49 | deviceName, 50 | hostAppId, 51 | data.deviceIp 52 | ) 53 | : appLaunchHelper.launchApp(deviceName, hostAppId); 54 | } catch (e) { 55 | logger.log(e); 56 | util.exit(); 57 | } 58 | }) 59 | .catch(e => { 60 | logger.error(chalk.red(`Failed to buildPackage: ${e}`)); 61 | util.exit(); 62 | }); 63 | } 64 | }; 65 | 66 | async function supportDeviceIpOption(data, option) { 67 | const optionDeviceIp = util.parseDeviceIp(option); 68 | if (optionDeviceIp === null && typeof option !== 'boolean') { 69 | logger.error( 70 | chalk.red( 71 | `Invalid Type of cli option. Please retry with correct type. ex) deviceIp=0.0.0.0` 72 | ) 73 | ); 74 | util.exit(); 75 | } 76 | 77 | if (optionDeviceIp) { 78 | data.deviceIp = optionDeviceIp; 79 | logger.log( 80 | chalk.cyanBright(`WITs tries connecting with ${optionDeviceIp}\n`) 81 | ); 82 | await userInfoHelper.updateLatestUserAnswer({ 83 | deviceIp: optionDeviceIp 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /container/css/1920/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: Verdana, Lucida Sans, Arial, Helvetica, sans-serif; 3 | } 4 | 5 | body { 6 | position: absolute; 7 | width: 1920px; 8 | height: 1080px; 9 | margin: 0px auto; 10 | padding: 0px auto; 11 | display: block; 12 | } 13 | 14 | .wits-container { 15 | position: absolute; 16 | left: 0px; 17 | top: 0px; 18 | width: 1920px; 19 | height: 1080px; 20 | margin: 0px auto; 21 | padding: 0px auto; 22 | background-color : #f1f1f1; 23 | } 24 | 25 | .wit-icon-container { 26 | position: absolute; 27 | left: 860px; 28 | top: 280px; 29 | width: 200px; 30 | height: 200px; 31 | margin: auto; 32 | } 33 | 34 | .wit-icon { 35 | position: absolute; 36 | left: 0px; 37 | top: 0px; 38 | width: 200px; 39 | height: 200px; 40 | margin: auto; 41 | } 42 | 43 | .pie-wrap-left { 44 | position:relative; 45 | } 46 | 47 | .slice-wrap:before { 48 | content:" "; 49 | position: absolute; 50 | width: 174px; 51 | height: 174px; 52 | border-radius: 100px; 53 | box-sizing: border-box; 54 | clip: rect(0, 87px, 174px, 0); 55 | background-color:rgba(0,0,0,0.1); 56 | transform:rotate(10deg); 57 | } 58 | .slice-wrap { 59 | position: absolute; 60 | left: 13px; 61 | top: 13px; 62 | width: 174px; 63 | height: 174px; 64 | border-radius: 87px; 65 | clip: rect(0, 174px, 174px, 87px); 66 | } 67 | .slice-right.slice-wrap { 68 | transform:rotate(180deg); 69 | } 70 | .slice-left:before { 71 | animation: 1s spin linear forwards; 72 | } 73 | .slice-right:before { 74 | transform:rotate(0.00001deg); 75 | animation: 1s spin2 linear 1s forwards; 76 | } 77 | .pie-wrap-right:after{display:none} 78 | 79 | @keyframes spin { 80 | from {transform: rotate(10deg);} 81 | to {transform: rotate(180deg);} 82 | } 83 | @keyframes spin2 { 84 | from {transform: rotate(0deg);} 85 | to {transform: rotate(180deg);} 86 | } 87 | 88 | .loading-progress { 89 | position: absolute; 90 | left: 5%; 91 | top: 50%; 92 | width: 90%; 93 | height: 45px; 94 | font-size: 20px; 95 | margin: auto; 96 | } 97 | 98 | .progress-bar-item { 99 | font-size: 20px; 100 | line-height: 45px; 101 | } 102 | 103 | .connectInfo-container { 104 | position: absolute; 105 | z-index: 1000; 106 | top: 22px; 107 | right: 15px; 108 | width: 225px; 109 | height: 120px; 110 | background-position: center; 111 | background-size: 100%; 112 | background-color:rgba(0,0,0,0.6); 113 | display: block; 114 | } 115 | 116 | #connectIcon { 117 | position: absolute; 118 | z-index: 1000; 119 | top: 7px; 120 | left: 75px; 121 | width: 75px; 122 | height: 75px; 123 | background-position: center; 124 | background-size: 100%; 125 | display: block; 126 | } 127 | 128 | .connected { 129 | background-image: url('../../res/connected.png'); 130 | } 131 | 132 | .disconnected { 133 | background-image: url('../../res/disconnected.png'); 134 | } 135 | 136 | #connectMessage { 137 | position: absolute; 138 | z-index: 1000; 139 | top: 82px; 140 | left: 0px; 141 | width: 225px; 142 | height: 30px; 143 | font-size: 25px; 144 | text-align: center; 145 | color: white; 146 | display: block; 147 | } 148 | 149 | #contentHTML { 150 | position: absolute; 151 | top : 0px; 152 | left: 0px; 153 | width: 1920px; 154 | height: 1080px; 155 | margin: 0; 156 | padding: 0; 157 | border: none; 158 | display: none; 159 | background: block; 160 | } -------------------------------------------------------------------------------- /container/css/1280/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: Verdana, Lucida Sans, Arial, Helvetica, sans-serif; 3 | } 4 | 5 | body { 6 | position: absolute; 7 | width: 1280px; 8 | height: 720px; 9 | margin: 0px auto; 10 | padding: 0px auto; 11 | display: block; 12 | } 13 | 14 | .wits-container { 15 | position: absolute; 16 | left: 0px; 17 | top: 0px; 18 | width: 1280px; 19 | height: 720px; 20 | margin: 0px auto; 21 | padding: 0px auto; 22 | background-color : #f1f1f1; 23 | } 24 | 25 | .wit-icon-container { 26 | position: absolute; 27 | left: 560px; 28 | top: 186px; 29 | width: 133px; 30 | height: 133px; 31 | margin: auto; 32 | } 33 | 34 | .wit-icon { 35 | position: absolute; 36 | left: 0px; 37 | top: 0px; 38 | width: 132px; 39 | height: 132px; 40 | margin: auto; 41 | } 42 | 43 | .pie-wrap-left { 44 | position:relative; 45 | } 46 | 47 | .slice-wrap:before { 48 | content:" "; 49 | position: absolute; 50 | width: 116px; 51 | height: 116px; 52 | border-radius: 58px; 53 | box-sizing: border-box; 54 | clip: rect(0, 58px, 116px, 0); 55 | background-color:rgba(0,0,0,0.1); 56 | transform:rotate(10deg); 57 | } 58 | .slice-wrap { 59 | position: absolute; 60 | left: 8px; 61 | top: 8px; 62 | width: 116px; 63 | height: 116px; 64 | border-radius: 58px; 65 | clip: rect(0, 116px, 116px, 58px); 66 | } 67 | .slice-right.slice-wrap { 68 | transform:rotate(180deg); 69 | } 70 | .slice-left:before { 71 | animation: 1s spin linear forwards; 72 | } 73 | .slice-right:before { 74 | transform:rotate(0.00001deg); 75 | animation: 1s spin2 linear 1s forwards; 76 | } 77 | .pie-wrap-right:after{display:none} 78 | 79 | @keyframes spin { 80 | from {transform: rotate(10deg);} 81 | to {transform: rotate(180deg);} 82 | } 83 | @keyframes spin2 { 84 | from {transform: rotate(0deg);} 85 | to {transform: rotate(180deg);} 86 | } 87 | 88 | .loading-progress { 89 | position: absolute; 90 | left: 5%; 91 | top: 50%; 92 | width: 90%; 93 | height: 30px; 94 | font-size: 20px; 95 | margin: auto; 96 | } 97 | 98 | .progress-bar-item { 99 | font-size: 20px; 100 | line-height: 30px; 101 | } 102 | 103 | .connectInfo-container { 104 | position: absolute; 105 | z-index: 1000; 106 | top: 15px; 107 | right: 10px; 108 | width: 150px; 109 | height: 80px; 110 | background-position: center; 111 | background-size: 100%; 112 | background-color:rgba(0,0,0,0.6); 113 | display: block; 114 | } 115 | 116 | #connectIcon { 117 | position: absolute; 118 | z-index: 1000; 119 | top: 5px; 120 | left: 50px; 121 | width: 50px; 122 | height: 50px; 123 | background-position: center; 124 | background-size: 100%; 125 | display: block; 126 | } 127 | 128 | .connected { 129 | background-image: url('../../res/connected.png'); 130 | } 131 | 132 | .disconnected { 133 | background-image: url('../../res/disconnected.png'); 134 | } 135 | 136 | #connectMessage { 137 | position: absolute; 138 | z-index: 1000; 139 | top: 55px; 140 | left: 0px; 141 | width: 150px; 142 | height: 20px; 143 | font-size: 17px; 144 | text-align: center; 145 | color: white; 146 | display: block; 147 | } 148 | 149 | #contentHTML { 150 | position: absolute; 151 | top : 0px; 152 | left: 0px; 153 | width: 1280px; 154 | height: 720px; 155 | margin: 0; 156 | padding: 0; 157 | border: none; 158 | display: none; 159 | background: block; 160 | } -------------------------------------------------------------------------------- /command/certificate.js: -------------------------------------------------------------------------------- 1 | const certificationHelper = require('../lib/certificationHelper.js'); 2 | const common = require('@tizentv/webide-common-tizentv'); 3 | const chalk = require('chalk'); 4 | const util = require('../lib/util'); 5 | const path = require('path'); 6 | const { logger } = require('../lib/logger'); 7 | 8 | module.exports = { 9 | run: async () => { 10 | logger.log(chalk.cyanBright('Generate a certification............\n')); 11 | 12 | const resourceDir = util.RESOURCE_PATH; 13 | 14 | try { 15 | const certInfo = await certificationHelper.getAnswers(); 16 | util.createEmptyDirectory(resourceDir); 17 | util.RESOURCE_PATH = resourceDir; 18 | 19 | const tizenCertManager = new common.TizenCertManager(resourceDir); 20 | await tizenCertManager.init(); 21 | const authorInfo = { 22 | keyFileName: certInfo.keyFileName, 23 | authorName: certInfo.authorName, 24 | authorPassword: certInfo.authorPassword, 25 | countryInfo: certInfo.countryInfo ? certInfo.countryInfo : '', 26 | stateInfo: certInfo.stateInfo ? certInfo.stateInfo : '', 27 | cityInfo: certInfo.cityInfo ? certInfo.cityInfo : '', 28 | organizationInfo: certInfo.organizationInfo 29 | ? certInfo.organizationInfo 30 | : '', 31 | departmentInfo: certInfo.departmentInfo 32 | ? certInfo.departmentInfo 33 | : '', 34 | emailInfo: certInfo.emailInfo ? certInfo.emailInfo : '' 35 | }; 36 | tizenCertManager.createCert(authorInfo); 37 | logger.log( 38 | chalk.cyanBright( 39 | '[Certification] Completed to generate a Tizen certification' 40 | ) 41 | ); 42 | 43 | const profileManager = new common.ProfileManager(resourceDir); 44 | const profileName = certInfo.profileName; 45 | const authorProfile = { 46 | authorCA: tizenCertManager.getTizenDeveloperCA(), 47 | authorCertPath: path.resolve( 48 | resourceDir, 49 | 'Author', 50 | `${certInfo.keyFileName}.p12` 51 | ), 52 | authorPassword: certInfo.authorPassword 53 | }; 54 | const distributorProfile = tizenCertManager.getTizenDistributorProfile( 55 | certInfo.privilegeLevel 56 | ); 57 | 58 | await profileManager.registerProfile( 59 | profileName, 60 | authorProfile, 61 | distributorProfile 62 | ); 63 | logger.log( 64 | chalk.cyanBright( 65 | '[Certification] Completed to register a profile' 66 | ) 67 | ); 68 | 69 | profileManager.setActivateProfile(profileName); 70 | 71 | logger.log( 72 | chalk.cyanBright( 73 | `[Certification] Completed to genarate a certification. ${path.resolve( 74 | path.join(resourceDir, 'profiles.xml') 75 | )}. Please "wits -i" to config your profile path.` 76 | ) 77 | ); 78 | } catch (e) { 79 | logger.error(chalk.red(`[Certification] Failed to run: ${e}`)); 80 | } 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /command/init.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const chalk = require('chalk'); 4 | const xml2js = require('xml2js'); 5 | const util = require('../lib/util.js'); 6 | const userInfoHelper = require('../lib/userInfoHelper.js'); 7 | const { logger } = require('../lib/logger'); 8 | 9 | let WITSCONFIG_PATH = ''; 10 | let WITSIGNORE_PATH = ''; 11 | 12 | module.exports = { 13 | run: async () => { 14 | logger.log( 15 | chalk.cyanBright(`Start configuration for Wits............\n`) 16 | ); 17 | 18 | try { 19 | await module.exports.prepareConfigure(); 20 | 21 | const wInfo = userInfoHelper.getRefinedData(); 22 | await userInfoHelper.askQuestion(wInfo.connectionInfo); 23 | } catch (e) { 24 | logger.error(chalk.red(`Failed to run: ${e}`)); 25 | } 26 | }, 27 | prepareConfigure: async () => { 28 | try { 29 | WITSCONFIG_PATH = path.join( 30 | util.CURRENT_PROJECT_PATH, 31 | '.witsconfig.json' 32 | ); 33 | WITSIGNORE_PATH = path.join( 34 | util.CURRENT_PROJECT_PATH, 35 | '.witsignore' 36 | ); 37 | 38 | makeWitsconfigFile(); 39 | util.chmodAll(WITSCONFIG_PATH); 40 | 41 | makeWitsignoreFile(); 42 | util.chmodAll(WITSIGNORE_PATH); 43 | 44 | await util.initTools(); 45 | return; 46 | } catch (error) { 47 | throw error; 48 | } 49 | }, 50 | isVaildProfile: async profilePath => { 51 | if (!util.isFileExist(profilePath)) { 52 | return false; 53 | } 54 | 55 | const profileFile = fs.readFileSync(profilePath, 'utf8'); 56 | const xmlParser = new xml2js.Parser(); 57 | const parsedProfiles = await xmlParser.parseStringPromise(profileFile); 58 | const activeProfile = parsedProfiles.profiles.$.active; 59 | 60 | if (typeof activeProfile === 'string' && activeProfile.length > 0) { 61 | return true; 62 | } 63 | return false; 64 | } 65 | }; 66 | 67 | function makeWitsignoreFile() { 68 | try { 69 | if (util.isFileExist(WITSIGNORE_PATH)) { 70 | logger.log('.witsignore is already exist.'); 71 | return; 72 | } 73 | 74 | util.createEmptyFile(WITSIGNORE_PATH, 'node_modules'); 75 | logger.log('witsignore is prepared.'); 76 | } catch (error) { 77 | logger.log(`[Warning] Failed to makeWitsignoreFile ${error}`); 78 | } 79 | } 80 | 81 | function makeWitsconfigFile() { 82 | try { 83 | if (util.isFileExist(WITSCONFIG_PATH) && isExistCustomFile()) { 84 | logger.log('.witsconfig.json is already exist.'); 85 | return; 86 | } 87 | util.createEmptyFile(WITSCONFIG_PATH, '{}'); 88 | logger.log('.witsconfig.json is prepared.'); 89 | } catch (error) { 90 | logger.log(`[Warning] Failed to makeWitsconfigFile ${error}`); 91 | } 92 | } 93 | 94 | function isExistCustomFile() { 95 | const customData = fs.readFileSync(WITSCONFIG_PATH, 'utf8'); 96 | if (isValidWitsconfigFile(customData)) { 97 | return true; 98 | } 99 | return false; 100 | } 101 | 102 | function isValidWitsconfigFile(data) { 103 | let witsConfigData = data; 104 | if (witsConfigData !== '' && typeof witsConfigData === 'string') { 105 | witsConfigData = JSON.parse(data); 106 | } 107 | 108 | if ( 109 | (witsConfigData.hasOwnProperty('profileInfo') && 110 | witsConfigData.hasOwnProperty('connectionInfo')) || 111 | witsConfigData.hasOwnProperty('optionalInfo') 112 | ) { 113 | return true; 114 | } 115 | return false; 116 | } 117 | -------------------------------------------------------------------------------- /lib/deviceConnectHelper.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const { execSync } = require('child_process'); 3 | 4 | const util = require('./util.js'); 5 | const regExp = require('./regexp.js'); 6 | const { logger } = require('./logger'); 7 | 8 | const EMULATOR_IP = '0.0.0.0'; 9 | const TV_CONNECT_PORT = '26101'; 10 | 11 | module.exports = { 12 | getConnectedDeviceInfo: async deviceIpAddress => { 13 | module.exports.connectDevice(deviceIpAddress); 14 | let deviceName = await module.exports.getDeviceName(deviceIpAddress); 15 | return { 16 | deviceName: deviceName, 17 | appInstallPath: module.exports.getAppInstallPath(deviceName) 18 | }; 19 | }, 20 | connectDevice: deviceIpAddress => { 21 | if (!util.isIpAddress(deviceIpAddress)) { 22 | throw logger.error( 23 | chalk.red( 24 | 'Invalid format of device IP address, please check it again.' 25 | ) 26 | ); 27 | } 28 | if (deviceIpAddress !== EMULATOR_IP) { 29 | logger.log(`connect to....${deviceIpAddress}`); 30 | const CONNECT_TV_COMMAND = `${util.TOOLS_SDB_PATH} connect ${deviceIpAddress}:${TV_CONNECT_PORT}`; 31 | const connectResult = execSync(CONNECT_TV_COMMAND, { 32 | encoding: 'utf-8', 33 | stdio: 'pipe' 34 | }); 35 | util.displayOutput(connectResult); 36 | 37 | if (connectResult.includes('connected')) { 38 | logger.log(`Success to connect ${deviceIpAddress}`); 39 | } else { 40 | logger.error(chalk.red(`Failed to connect ${deviceIpAddress}`)); 41 | util.exit(); 42 | } 43 | } 44 | }, 45 | getDeviceName: async deviceIpAddress => { 46 | const SINGLE_DEVICE = 1; 47 | const NONE_DEVICE = 0; 48 | const deviceNameList = getConnectedDeviceList(deviceIpAddress); 49 | 50 | if (deviceNameList.length === NONE_DEVICE) { 51 | logger.error(chalk.red(`No connected devices.`)); 52 | util.exit(); 53 | } else if (deviceNameList.length === SINGLE_DEVICE) { 54 | const deviceName = deviceNameList[0]; 55 | return deviceName; 56 | } else { 57 | if (deviceIpAddress === '0.0.0.0') { 58 | deviceIpAddress = 'emulator'; 59 | } 60 | let deviceName = ''; 61 | deviceNameList.forEach(device => { 62 | if (device.includes(deviceIpAddress)) { 63 | deviceName = device; 64 | } 65 | }); 66 | return deviceName; 67 | } 68 | }, 69 | getAppInstallPath: deviceName => { 70 | let appInstallPath = ''; 71 | 72 | const capability = execSync( 73 | `${util.TOOLS_SDB_PATH} -s ${deviceName} capability`, 74 | { encoding: 'utf-8', stdio: 'pipe' } 75 | ).split('\n'); 76 | 77 | capability.forEach(value => { 78 | util.displayOutput(value); 79 | if (value.indexOf('sdk_toolpath') !== -1) { 80 | appInstallPath = 81 | value.replace(regExp.FIND_CR, '').split(':')[1] + '/'; 82 | } 83 | }); 84 | return appInstallPath; 85 | } 86 | }; 87 | 88 | function getConnectedDeviceList(deviceIpAddress) { 89 | const devices = execSync(`${util.TOOLS_SDB_PATH} devices`, { 90 | encoding: 'utf-8', 91 | stdio: 'pipe' 92 | }); 93 | util.displayOutput(devices); 94 | 95 | let devicesInfo = []; 96 | let deviceNameList = []; 97 | if (!devices.includes('offline')) { 98 | devicesInfo = devices.trim().split('\n'); 99 | devicesInfo.shift(); 100 | deviceNameList = parsingDeviceName(devicesInfo); 101 | } else { 102 | logger.error(chalk.red(`Failed to connect ${deviceIpAddress}`)); 103 | util.exit(); 104 | } 105 | return deviceNameList; 106 | } 107 | 108 | function parsingDeviceName(devices) { 109 | const deviceNameList = []; 110 | devices.forEach(device => { 111 | deviceNameList.push(device.split('\t')[0].trim()); 112 | }); 113 | 114 | return deviceNameList; 115 | } 116 | -------------------------------------------------------------------------------- /doc/README_zh_HANS.md: -------------------------------------------------------------------------------- 1 | # WITs 2 | WITs 是一个非常有效的开发工具,很容易帮助开发属于你的 Tizen web 项目,并支持**2017+ Samsung tv**的机型。 3 | 它可以非常快地将你开发期间的实时代码推送到电视机上。每次您想查看项目如何在设备上运行时,都无需经历再次构建,打包和重新安装应用程序。我们称为 `实时开发`。 4 | **WITs 对于你的开发内容有持续性帮助。** 5 | 6 | 7 | 8 | ## 介绍 9 | - [ENGLISH](/README.md) 10 | - [正體中文](/doc/README_zh_HANT.md) 11 | 12 | ## 安装与配置WITs 13 | 14 | ### 开发者请使用npm 15 | 16 | 17 | #### 1. 全局安装 WITs 18 | 19 | ```sh 20 | $ npm install -g @tizentv/wits 21 | ``` 22 | 23 | ### 开发者们请使用git repository 24 | 25 | #### 1. 克隆对应项目 26 | ```sh 27 | $ git clone https://github.com/Samsung/Wits.git 28 | ``` 29 | 30 | #### 2. 安装WITs依赖 31 | 32 | ```sh 33 | $ cd ~/{path-to}/Wits 34 | $ npm install -g 35 | ``` 36 | 37 | #### 3. 在 Wits 的目录下更改 .witsconfig.json。 38 | 39 | Tizen Studio的证书路径(profiles.xml)是在 **.witsconfig.json** 的 `path` 上配置, 40 | 在mac与Windows系统上默认路径 `path` 都是 `tizen-studio-data/profile/profiles.xml` 。 41 | 42 | 43 | ## **系统要求** 44 | 45 | WITs 需要进一步地在你本地开发机器上配置对应的的步骤。 46 | 47 | #### 1. 打开 **`Terminal(终端器)` on MacOS / Linux** or **`CMD (命令提示符)` / `PowerShell` on Windows** 48 | 49 | #### 2. 安装 Node.js 和 Git (推荐 v7.10.1 ~) 50 | 51 | 我们不能很好告诉你这些安装步骤,因为有太多方法与开发者各有自己的性能配置,但我们推荐你使用一些,例如 `nvm` 或 `asdf` 等的项目管理器去管理不同的Node.js 版本去控制你的代码项目。 52 | 53 | 54 | #### 3. 安装最新的 [Samsung Tizen Studio](http://developer.samsung.com/tv). 55 | 56 | 57 | #### 4. 打开你的Samsung电视上的开发者模式: 58 | - 1 使用你的三星遥控器,按 `Home` 的按键。 59 | - 2 移动所选到 `Apps` 的按键并按 `Enter/OK`。 60 | - 3 如当前显示`Apps` 屏幕,依次在遥控器上按`1` `2` `3` `4` `5`并同时弹出`开发模式对话框`,如果不成功或不出现,再一次尝试。 61 | - 4 当开发者对话框出现, 切换并点击按钮 `On` 和 输入你的开发机器所对应的IP地址。 62 | 63 | ## WITs 细节 64 | 65 | ### WITs所使用的项目结构 66 | 67 | 68 | .witsconfig.json, .witsignore 只需这两个文件增加到你的tizen web项目。 69 | 70 | ### WITs 命令选项 71 | 72 | 73 | #### 1. `wits` 74 | 75 | 显示当前可供你选择的那些命令 76 | 77 | #### 2. `wits -i` / `wits --init` 78 | 79 | 对于正在配置的 WITs 80 | 请留意及注意,只需第一次运行在你的 tizen项目即可。 81 | .witsconfig.json 和 .witsignore会同时生成并增加到你的 tizen项目。 82 | 你可以稍后修改对这些文件的配置。 83 | 84 | 85 | #### 3. `wits -s` / `wits --start` 86 | 87 | 这命令是所有都一样,对于连接,安装及运行在TV都是使用实时加载。如`wits -i` 没有在此之前运行,这个是不允许的。 88 | 89 | #### 4. `wits -w` / `wits --watch` 90 | 91 | 对于连接TV时使用是实时加载。 92 | 在连接之后,每次你在 `你的tizen app项目`的改动文件都会即时反应同步你的电视设备。 93 | 94 | ### .witsconfig.json of WITs 95 | 96 | 细节: [关于.witsconfig.json的范例](https://github.com/Samsung/Wits/wiki/Set-Wits-Environment#data-structure-of-witsconfigjson) 97 | 98 | 无论在`Windows` 和 `MacOS`上,都必须认识到路径的唯一符号是分隔符 (**`/`**)。 99 | 100 | 101 | - **connectionInfo** (必选) 102 | - deviceIp [string] : 设备(TV) Ip 地址 (如是调试器,请填写 0.0.0.0) 103 | 104 | - hostIp [string] : Host(PC) Ip 地址 105 | - socketPort [integer] : TV 端口. 他是由WITs随机生成的。 106 | - width [string] : 分辨率宽度 107 | - isDebugMode [boolean] : 如设置为true, chrome调试器会打开并自动运行。如设置为false, 默认无动作。 108 | - **profileInfo** (必选) 109 | - path [string] : Tizen Studio 证书路径。 110 | - **optionalInfo** (可选) 111 | - proxyServer [string] : 如你使用代理,例如:http://255.255.255.255:8080 112 | 113 | ### WITs的.witsignore: 114 | 115 | 有时候如果你不想将你的一些文件,例如`.git` 或 `node_modules`这类文件推送到电视或设备, 你可以通过填写 .witsignore 文件规定这类文件格式,下次你就发现这些文件不会推送到调试的设备之上。 116 | 实现道理与`.gitignore` 一样。 117 | 118 | `.witsignore`的范例: 119 | 120 | ```text 121 | node_modules 122 | .git 123 | deprecated 124 | stglib 125 | ``` 126 | 127 | ## 运行你的APP 128 | 129 | 如果你想一步一步按以下设置。 130 | 请关注: [Running Your App using WITs](https://github.com/Samsung/Wits/wiki/Running-Your-App-using-Wits) 131 | 132 | ### wits -i 133 | 134 | ![witsi](https://user-images.githubusercontent.com/1733182/77503919-3ddef280-6ea2-11ea-9bb4-06f3cb9ebbc6.gif) 135 | 136 | ### wits -s 137 | 138 | ![witss](https://user-images.githubusercontent.com/1733182/77503927-420b1000-6ea2-11ea-88f5-49ab0c5fc227.gif) 139 | 140 | ### wits -w 141 | 142 | ![witsw](https://user-images.githubusercontent.com/1733182/77503928-43d4d380-6ea2-11ea-8ece-4f5182cb7d6d.gif) 143 | 144 | ## FAQ 145 | 146 | - [WITs FAQ](https://github.com/Samsung/Wits/wiki/Frequently-Asked-Questions) 147 | - [怎样使用Chrome inspector的开发模式](https://github.com/Samsung/Wits/wiki/Frequently-Asked-Questions#answer-1) 148 | - [使用proxy之后](https://github.com/Samsung/Wits/wiki/Frequently-Asked-Questions#question-6) 149 | - [怎么去获取你的电视或设备IP](https://github.com/Samsung/Wits/wiki/Frequently-Asked-Questions#question-7) 150 | 151 | ## 支持的设备 152 | 153 | - 2017 Samsung Smart TV (Tizen 3.0) 154 | - 2018 Samsung Smart TV (Tizen 4.0) 155 | - 2019 Samsung Smart TV (Tizen 5.0) 156 | 157 | 158 | -------------------------------------------------------------------------------- /doc/README_zh_HANT.md: -------------------------------------------------------------------------------- 1 | # WITs 2 | WITs 是一個非常有效的開發工具,很容易幫助開發屬於你的 Tizen web 項目,並支援**2017+ Samsung tv**的機型。 3 | 它可以非常快地將你開發期間的實時代碼推送到電視機上。每次您想檢視項目如何在設備上運行時,都無需經曆再次構建,打包和重新安裝應用程式。我們稱為 `實時開發`。 4 | **WITs 對於你的開發內容有持續性幫助。** 5 | 6 | 7 | 8 | ## 介紹 9 | - [ENGLISH](/README.md) 10 | - [簡體中文](/doc/README_zh_HANS.md) 11 | 12 | ## 安裝與配置WITs 13 | 14 | ### 開發者請使用npm 15 | 16 | 17 | #### 1. 全局安裝 WITs 18 | 19 | ```sh 20 | $ npm install -g @tizentv/wits 21 | ``` 22 | 23 | ### 開發者們請使用git repository 24 | 25 | #### 1. 克隆對應項目 26 | ```sh 27 | $ git clone https://github.com/Samsung/Wits.git 28 | ``` 29 | 30 | #### 2. 安裝WITs依賴 31 | 32 | ```sh 33 | $ cd ~/{path-to}/Wits 34 | $ npm install -g 35 | ``` 36 | 37 | #### 3. 在 Wits 的目錄下更改 .witsconfig.json。 38 | 39 | Tizen Studio的證書路徑(profiles.xml)是在 **.witsconfig.json** 的 `path` 上配置, 40 | 在mac與Windows係統上預設路徑 `path` 都是 `tizen-studio-data/profile/profiles.xml` 。 41 | 42 | 43 | ## **係統要求** 44 | 45 | WITs 需要進一步地在你在地開發機器上配置對應的的步驟。 46 | 47 | #### 1. 打開 **`Terminal(終端器)` on MacOS / Linux** or **`CMD (命令提示符)` / `PowerShell` on Windows** 48 | 49 | #### 2. 安裝 Node.js 和 Git (推薦 v7.10.1 ~) 50 | 51 | 我們不能很好告訴你這些安裝步驟,因為有太多方法與開發者各有自己的性能配置,但我們推薦你使用一些,例如 `nvm` 或 `asdf` 等的項目管理器去管理不同的Node.js 版本去控製你的代碼項目。 52 | 53 | 54 | #### 3. 安裝最新的 [Samsung Tizen Studio](http://developer.samsung.com/tv). 55 | 56 | 57 | #### 4. 打開你的Samsung電視上的開發者模式: 58 | - 1 使用你的三星遙控器,按 `Home` 的按鍵。 59 | - 2 移動所選到 `Apps` 的按鍵並按 `Enter/OK`。 60 | - 3 如當前顯示`Apps` 熒幕,依次在遙控器上按`1` `2` `3` `4` `5`並同時彈出`開發模式對話框`,如果不成功或不出現,再一次嘗試。 61 | - 4 當開發者對話框出現, 切換並點選按鈕 `On` 和 輸入你的開發機器所對應的IP位址。 62 | 63 | ## WITs 細節 64 | 65 | ### WITs所使用的項目結構 66 | 67 | 68 | .witsconfig.json, .witsignore 隻需這兩個文件增加到你的tizen web項目。 69 | 70 | ### WITs 命令選項 71 | 72 | 73 | #### 1. `wits` 74 | 75 | 顯示當前可供你選擇的那些命令 76 | 77 | #### 2. `wits -i` / `wits --init` 78 | 79 | 對於正在配置的 WITs 80 | 請留意及註意,隻需第一次運行在你的 tizen項目即可。 81 | .witsconfig.json 和 .witsignore會同時生成並增加到你的 tizen項目。 82 | 你可以稍後修改對這些文件的配置。 83 | 84 | 85 | #### 3. `wits -s` / `wits --start` 86 | 87 | 這命令是所有都一樣,對於連接,安裝及運行在TV都是使用實時加載。如`wits -i` 冇有在此之前運行,這個是不允許的。 88 | 89 | #### 4. `wits -w` / `wits --watch` 90 | 91 | 對於連接TV時使用是實時加載。 92 | 在連接之後,每次你在 `你的tizen app項目`的改動文件都會即時反應同步你的電視設備。 93 | 94 | ### .witsconfig.json of WITs 95 | 96 | 細節: [關於.witsconfig.json的範例](https://github.com/Samsung/Wits/wiki/Set-Wits-Environment#data-structure-of-witsconfigjson) 97 | 98 | 無論在`Windows` 和 `MacOS`上,都必須認識到路徑的唯一符號是分隔符 (**`/`**)。 99 | 100 | 101 | - **connectionInfo** (必選) 102 | - deviceIp [string] : 設備(TV) Ip 地址 (如是調試器,請填寫 0.0.0.0) 103 | 104 | - hostIp [string] : Host(PC) Ip 地址 105 | - socketPort [integer] : TV 端口. 他是由WITs隨機生成的。 106 | - width [string] : 分辨率寬度 107 | - isDebugMode [boolean] : 如設定為true, chrome調試器會打開並自動運行。如設定為false, 預設無動作。 108 | - **profileInfo** (必選) 109 | - path [string] : Tizen Studio 證書路徑。 110 | - **optionalInfo** (可選) 111 | - proxyServer [string] : 如你使用代理,例如:http://255.255.255.255:8080 112 | 113 | ### WITs的.witsignore: 114 | 115 | 有時候如果你不想將你的一些文件,例如`.git` 或 `node_modules`這類文件推送到電視或設備, 你可以通過填寫 .witsignore 文件規定這類文件格式,下次你就發現這些文件不會推送到調試的設備之上。 116 | 實現道理與`.gitignore` 一樣。 117 | 118 | `.witsignore`的範例: 119 | 120 | ```text 121 | node_modules 122 | .git 123 | deprecated 124 | stglib 125 | ``` 126 | 127 | ## 運行你的APP 128 | 129 | 如果你想一步一步按以下設定。 130 | 請關註: [Running Your App using WITs](https://github.com/Samsung/Wits/wiki/Running-Your-App-using-Wits) 131 | 132 | ### wits -i 133 | 134 | ![witsi](https://user-images.githubusercontent.com/1733182/77503919-3ddef280-6ea2-11ea-9bb4-06f3cb9ebbc6.gif) 135 | 136 | ### wits -s 137 | 138 | ![witss](https://user-images.githubusercontent.com/1733182/77503927-420b1000-6ea2-11ea-88f5-49ab0c5fc227.gif) 139 | 140 | ### wits -w 141 | 142 | ![witsw](https://user-images.githubusercontent.com/1733182/77503928-43d4d380-6ea2-11ea-8ece-4f5182cb7d6d.gif) 143 | 144 | ## FAQ 145 | 146 | - [WITs FAQ](https://github.com/Samsung/Wits/wiki/Frequently-Asked-Questions) 147 | - [怎樣使用Chrome inspector的開發模式](https://github.com/Samsung/Wits/wiki/Frequently-Asked-Questions#answer-1) 148 | - [使用proxy之後](https://github.com/Samsung/Wits/wiki/Frequently-Asked-Questions#question-6) 149 | - [怎麼去獲取你的電視或設備IP](https://github.com/Samsung/Wits/wiki/Frequently-Asked-Questions#question-7) 150 | 151 | ## 支援的設備 152 | 153 | - 2017 Samsung Smart TV (Tizen 3.0) 154 | - 2018 Samsung Smart TV (Tizen 4.0) 155 | - 2019 Samsung Smart TV (Tizen 5.0) 156 | 157 | 158 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const ip = require('ip'); 5 | const mkdirp = require('mkdirp'); 6 | const tools = require('@tizentv/tools'); 7 | const chalk = require('chalk'); 8 | 9 | const regExp = require('./regexp.js'); 10 | const { logger } = require('./logger'); 11 | 12 | const platform = os.platform(); 13 | const CURRENT_PROJECT_PATH = process.cwd(); 14 | 15 | module.exports = { 16 | WITS_BASE_PATH: __dirname, 17 | CURRENT_PROJECT_PATH: CURRENT_PROJECT_PATH, 18 | PROXY: '', 19 | RESOURCE_PATH: (() => { 20 | if (platform === 'win32') { 21 | return path.resolve(__dirname, '../', 'resource'); 22 | } else { 23 | return path.resolve(os.homedir(), '.wits', 'resource'); 24 | } 25 | })(), 26 | TOOLS_SDB_PATH: '', 27 | PLATFORM: platform, 28 | ISVERVOSE: false, 29 | initTools: async () => { 30 | module.exports.TOOLS_SDB_PATH = await tools.getSdbPath(); 31 | }, 32 | isIpAddress: ip => { 33 | return regExp.IP_ADDRESS.test(ip); 34 | }, 35 | 36 | isRemoteUrl: url => { 37 | return regExp.REMOTE_URI.test(url); 38 | }, 39 | 40 | isProxy: address => { 41 | return regExp.PROXY.test(address); 42 | }, 43 | 44 | getAbsolutePath: inputPath => { 45 | return path 46 | .join(CURRENT_PROJECT_PATH, inputPath) 47 | .replace(regExp.BACKSLASH, '/'); 48 | }, 49 | 50 | createEmptyFile: (filepath, content) => { 51 | if (content === undefined) { 52 | content = ''; 53 | } 54 | try { 55 | fs.accessSync(path.join(filepath)); 56 | } catch (e) { 57 | try { 58 | fs.writeFileSync(path.join(filepath), content, 'utf8'); 59 | fs.chmodSync(path.join(filepath), '0775'); 60 | } catch (error) { 61 | logger.error( 62 | chalk.red(`Failed to createEmptyFile ${filepath} ${error}`) 63 | ); 64 | process.exit(0); 65 | } 66 | } 67 | }, 68 | 69 | createEmptyDirectory: dirname => { 70 | try { 71 | mkdirp.sync(dirname); 72 | } catch (error) { 73 | logger.error( 74 | chalk.red(`Failed to createEmptyDirectory ${dirname} ${error}`) 75 | ); 76 | } 77 | }, 78 | 79 | removeFile: filepath => { 80 | if (fs.existsSync(filepath)) { 81 | try { 82 | fs.unlinkSync(filepath); 83 | } catch (e) { 84 | logger.error(chalk.red(`Failed to removeFile ${filepath} ${e}`)); 85 | throw e; 86 | } 87 | } 88 | }, 89 | 90 | moveFile: (src, dest) => { 91 | try { 92 | if (module.exports.isFileExist(src)) { 93 | module.exports.copyFile(src, dest); 94 | module.exports.removeFile(src); 95 | } 96 | } catch (e) { 97 | logger.error(chalk.red(`Failed to moveFile: ${e}`)); 98 | throw e; 99 | } 100 | }, 101 | 102 | copyFile: (src, dest) => { 103 | try { 104 | if (module.exports.isFileExist(src)) { 105 | fs.createReadStream(src).pipe(fs.createWriteStream(dest)); 106 | } 107 | } catch (e) { 108 | logger.error(chalk.red(`Failed to copyFile: ${e}`)); 109 | throw e; 110 | } 111 | }, 112 | 113 | setCurrentAppPath: path => { 114 | if (path !== '.') { 115 | module.exports.CURRENT_PROJECT_PATH = path; 116 | } 117 | }, 118 | 119 | isFileExist: filePath => { 120 | try { 121 | fs.accessSync(filePath); 122 | return true; 123 | } catch (e) { 124 | return false; 125 | } 126 | }, 127 | 128 | isPropertyExist: (data, propertyName) => { 129 | if ( 130 | data !== null && 131 | typeof data === 'object' && 132 | data.hasOwnProperty(propertyName) 133 | ) { 134 | return true; 135 | } 136 | return false; 137 | }, 138 | 139 | clearComment: data => { 140 | return data.replace(regExp.COMMENT, ''); 141 | }, 142 | 143 | displayOutput: logs => { 144 | if (module.exports.ISVERVOSE === true) { 145 | logger.log(logs); 146 | } 147 | }, 148 | getSocketPort: () => { 149 | const REMIND_SOCKET_PORT_LEN = 3; 150 | const MAX_DIGIT = 9; 151 | let port = Math.floor(Math.random() * MAX_DIGIT) + 1 + ''; 152 | for (let i = 0; i < REMIND_SOCKET_PORT_LEN; i++) { 153 | port += Math.floor(Math.random() * MAX_DIGIT) + ''; 154 | } 155 | return Number(port); 156 | }, 157 | 158 | getValidHostIp: (cInfo, answer) => { 159 | let hostIp = ip.address(); 160 | if (module.exports.isPropertyExist(cInfo, 'hostIp')) { 161 | hostIp = cInfo.hostIp; 162 | } 163 | if (module.exports.isPropertyExist(answer, 'hostIp')) { 164 | hostIp = answer.hostIp; 165 | } 166 | return hostIp; 167 | }, 168 | 169 | getHostIpAddresses: () => { 170 | const networkInterfaces = os.networkInterfaces(); 171 | const ipAddresses = []; 172 | 173 | for (var eth in networkInterfaces) { 174 | var interfaces = networkInterfaces[eth]; 175 | for (var i = 0; i < interfaces.length; i++) { 176 | var network = interfaces[i]; 177 | if (isIpv4Address(network)) { 178 | ipAddresses.push(network.address); 179 | } 180 | } 181 | } 182 | return ipAddresses; 183 | }, 184 | 185 | displayBanner() { 186 | logger.log(' _ ____________ '); 187 | logger.log('| | /| / / _/_ __/__'); 188 | logger.log('| |/ |/ // / / / (_-<'); 189 | logger.log('|__/|__/___/ /_/ /___/\n'); 190 | }, 191 | 192 | chmodAll(path) { 193 | try { 194 | fs.accessSync(path, fs.constants.S_IXUSR); 195 | } catch (e) { 196 | fs.chmodSync(path, fs.constants.S_IRWXU | fs.constants.S_IRWXG); 197 | } 198 | }, 199 | 200 | exit: () => { 201 | process.exit(0); 202 | }, 203 | 204 | parseDeviceIp: option => { 205 | if (typeof option !== 'string') { 206 | return null; 207 | } 208 | const deviceIp = option.split('deviceIp=')[1]; 209 | if (module.exports.isIpAddress(deviceIp)) { 210 | return deviceIp; 211 | } 212 | return null; 213 | } 214 | }; 215 | 216 | function isIpv4Address(eth) { 217 | if (eth.family === 'IPv4' && eth.address !== '127.0.0.1' && !eth.internal) { 218 | return true; 219 | } else { 220 | return false; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /lib/appLaunchHelper.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chalk = require('chalk'); 3 | const { execSync } = require('child_process'); 4 | const chromeLauncher = require('chrome-launcher'); 5 | const regExp = require('./regexp.js'); 6 | const util = require('./util.js'); 7 | const { logger } = require('./logger'); 8 | 9 | const PACKAGE_BASE_PATH = path.join(util.WITS_BASE_PATH, '../', 'container'); 10 | const WITS_PACKAGE = 'WITs.wgt'; 11 | const EMULATOR_IP = '0.0.0.0'; 12 | 13 | module.exports = { 14 | installPackage: (deviceInfo, hostAppName) => { 15 | const deviceName = deviceInfo.deviceName; 16 | const appInstallPath = deviceInfo.appInstallPath; 17 | const WGT_FILE_PUSH_COMMAND = `${ 18 | util.TOOLS_SDB_PATH 19 | } -s ${deviceName} push "${path.join( 20 | PACKAGE_BASE_PATH, 21 | WITS_PACKAGE 22 | )}" "${appInstallPath}"`; 23 | const APP_INSTALL_COMMAND = `${util.TOOLS_SDB_PATH} -s ${deviceName} shell 0 vd_appinstall ${hostAppName} ${appInstallPath}${WITS_PACKAGE}`; 24 | 25 | const pushResult = execSync(WGT_FILE_PUSH_COMMAND, { 26 | encoding: 'utf-8', 27 | stdio: 'pipe' 28 | }); 29 | util.displayOutput(pushResult); 30 | 31 | const installResult = execSync(APP_INSTALL_COMMAND, { 32 | encoding: 'utf-8', 33 | stdio: 'pipe' 34 | }); 35 | util.displayOutput(installResult); 36 | 37 | if (installResult.includes('failed[')) { 38 | logger.error(chalk.red(`\nFailed to install Wits`)); 39 | util.exit(); 40 | } 41 | }, 42 | unInstallPackage: (deviceName, hostAppName) => { 43 | const APP_UNINSTALL_COMMAND = `${util.TOOLS_SDB_PATH} -s ${deviceName} shell 0 vd_appuninstall ${hostAppName}`; 44 | const result = execSync(APP_UNINSTALL_COMMAND, { 45 | encoding: 'utf-8', 46 | stdio: 'pipe' 47 | }); 48 | util.displayOutput(result); 49 | 50 | if (result.includes('failed[')) { 51 | logger.warn(`\n[warning] Failed to uninstall Wits`); 52 | } 53 | }, 54 | launchApp: (deviceName, hostAppId) => { 55 | const APP_LAUNCH_COMMAND = `${util.TOOLS_SDB_PATH} -s ${deviceName} shell 0 was_execute ${hostAppId}`; 56 | 57 | const result = execSync(APP_LAUNCH_COMMAND, { 58 | encoding: 'utf-8', 59 | stdio: 'pipe' 60 | }); 61 | util.displayOutput(result); 62 | 63 | if (result === null || result.includes('failed[')) { 64 | throw new Error( 65 | 'Failed to launchApp. Please check the application is already installed on the Target.' 66 | ); 67 | } 68 | }, 69 | launchDebugMode: (deviceName, hostAppId, deviceIpAddress) => { 70 | const APP_LAUNCH_DEBUG_MODE_COMMAND = `${util.TOOLS_SDB_PATH} -s ${deviceName} shell 0 debug ${hostAppId}`; 71 | const APP_LAUNCH_DEBUG_MODE_COMMAND_TIMEOUT = `${APP_LAUNCH_DEBUG_MODE_COMMAND} 300`; 72 | 73 | const result = 74 | execSync(APP_LAUNCH_DEBUG_MODE_COMMAND, { 75 | encoding: 'utf-8', 76 | stdio: 'pipe' 77 | }) || 78 | execSync(APP_LAUNCH_DEBUG_MODE_COMMAND_TIMEOUT, { 79 | encoding: 'utf-8', 80 | stdio: 'pipe' 81 | }); 82 | util.displayOutput(result); 83 | 84 | if (result === null || result.includes('failed')) { 85 | throw new Error( 86 | 'Failed to launchDebugMode. Please check the application is already installed on the Target.' 87 | ); 88 | } 89 | 90 | const debugPort = result 91 | .match(regExp.DEBUG_PORT)[0] 92 | .match(regExp.NUMBER_WORD)[0]; 93 | let debugIP = ''; 94 | if (deviceIpAddress === EMULATOR_IP) { 95 | const LOCAL_HOST = '127.0.0.1'; 96 | setPortForward(deviceName, debugPort); 97 | debugIP = LOCAL_HOST; 98 | } else { 99 | debugIP = deviceIpAddress; 100 | } 101 | 102 | try { 103 | launchChrome(debugIP + ':' + debugPort); 104 | } catch (e) { 105 | logger.log( 106 | `Please install a Chrome browser or input ${debugIP}:${debugPort} into the address bar of the chromium based browser. ${e}` 107 | ); 108 | } 109 | }, 110 | terminateApp: (deviceName, hostAppId) => { 111 | const APP_TERMINATE_COMMAND = `${util.TOOLS_SDB_PATH} -s ${deviceName} shell 0 was_kill ${hostAppId}`; 112 | const result = execSync(APP_TERMINATE_COMMAND, { 113 | encoding: 'utf-8', 114 | stdio: 'pipe' 115 | }); 116 | util.displayOutput(result); 117 | } 118 | }; 119 | 120 | function setPortForward(deviceName, port) { 121 | const LOCAL_HOST = '127.0.0.1'; 122 | const removeResult = execSync( 123 | `${util.TOOLS_SDB_PATH} -s ${deviceName} forward --remove tcp:${port}`, 124 | { 125 | encoding: 'utf-8', 126 | stdio: 'pipe' 127 | } 128 | ); 129 | util.displayOutput(removeResult); 130 | 131 | const tcpResult = execSync( 132 | `${util.TOOLS_SDB_PATH} -s ${deviceName} forward tcp:${port} tcp:${port}`, 133 | { 134 | encoding: 'utf-8', 135 | stdio: 'pipe' 136 | } 137 | ); 138 | util.displayOutput(tcpResult); 139 | try { 140 | launchChrome(LOCAL_HOST + ':' + port); 141 | } catch (e) { 142 | logger.log( 143 | `Please install a Chrome browser or input ${LOCAL_HOST}:${port} into the address bar of the chromium based browser. ${e}` 144 | ); 145 | } 146 | } 147 | 148 | function launchChrome(url) { 149 | chromeLauncher 150 | .launch({ 151 | startingUrl: url, 152 | chromeFlags: [ 153 | '--disable-web-security', 154 | '--enable-blink-features=ShadowDOMV0,CustomElementsV0,HTMLImports' 155 | ] 156 | }) 157 | .then(chrome => { 158 | logger.log(`Chrome debugging port running on ${chrome.port}`); 159 | }) 160 | .catch(e => { 161 | logger.log(chalk.red(`Please install a Chrome browser. ${e}`)); 162 | }); 163 | } 164 | -------------------------------------------------------------------------------- /lib/certificationHelper.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | const xml2js = require('xml2js'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const util = require('./util'); 6 | const regExp = require('./regexp.js'); 7 | 8 | const CERTIFICATION_TYPE = { 9 | tizen: 'tizen', 10 | samsung: 'samsung' 11 | }; 12 | 13 | const PRIVILEGE_LEVEL = { 14 | public: 'public', 15 | partner: 'partner' 16 | }; 17 | 18 | let profileNames = []; 19 | 20 | module.exports = { 21 | CERTIFICATION_DATA: {}, 22 | 23 | getAnswers: async () => { 24 | profiles = await getProfileNames(); 25 | const preAnswer = await inquirer.prompt([ 26 | getCertificationTypeQuestion() 27 | ]); 28 | 29 | let answer = {}; 30 | switch (preAnswer.type) { 31 | case CERTIFICATION_TYPE.tizen: 32 | answer = await getTizenCertificationData(); 33 | break; 34 | case CERTIFICATION_TYPE.samsung: 35 | answer = await getSamsungCertificationData(); 36 | default: 37 | break; 38 | } 39 | return answer; 40 | } 41 | }; 42 | 43 | async function getProfileNames() { 44 | try { 45 | const profilePath = path.join( 46 | util.WITS_BASE_PATH, 47 | '../', 48 | 'resource', 49 | 'profiles.xml' 50 | ); 51 | util.isFileExist(profilePath); 52 | const profileFile = fs.readFileSync(profilePath, 'utf8'); 53 | const xmlParser = new xml2js.Parser(); 54 | const parsedProfiles = await xmlParser.parseStringPromise(profileFile); 55 | if (parsedProfiles.profiles && parsedProfiles.profiles.profile) { 56 | const profiles = parsedProfiles.profiles.profile; 57 | profiles.forEach(item => { 58 | profileNames.push(item.$.name); 59 | }); 60 | } 61 | } catch (error) { 62 | profileNames = []; 63 | } 64 | } 65 | 66 | async function getTizenCertificationData() { 67 | const privilegeInfo = []; 68 | privilegeInfo.push(getPrivilegeLevel()); 69 | const privilegeAnswer = await inquirer.prompt(privilegeInfo); 70 | 71 | const profileInfo = []; 72 | profileInfo.push(getProfileName()); 73 | const profileAnswer = await inquirer.prompt(profileInfo); 74 | 75 | const authorInfo = []; 76 | authorInfo.push(getKeyFileName()); 77 | authorInfo.push(getAuthorName()); 78 | authorInfo.push(getAuthorPassword()); 79 | const authorAnswer = await inquirer.prompt(authorInfo); 80 | 81 | const detailConfirm = await inquirer.prompt(getDetailOptionalQuestion()); 82 | const detailInfo = []; 83 | if (detailConfirm.details === true) { 84 | detailInfo.push(getCountryInfo()); 85 | detailInfo.push(getStateInfo()); 86 | detailInfo.push(getCityInfo()); 87 | detailInfo.push(getOrganizationInfo()); 88 | detailInfo.push(getDepartmentInfo()); 89 | detailInfo.push(getEmailInfo()); 90 | } 91 | const detailAnswer = await inquirer.prompt(detailInfo); 92 | 93 | return Object.assign( 94 | privilegeAnswer, 95 | profileAnswer, 96 | authorAnswer, 97 | detailAnswer 98 | ); 99 | } 100 | 101 | function getSamsungCertificationData() { 102 | let ask = []; 103 | ask.push(); 104 | return ask; 105 | } 106 | 107 | function getCertificationTypeQuestion() { 108 | return { 109 | type: 'list', 110 | name: 'type', 111 | message: 'Select certification type : ', 112 | choices: [CERTIFICATION_TYPE.tizen], 113 | // choices: [CERTIFICATION_TYPE.tizen, CERTIFICATION_TYPE.samsung], 114 | default: CERTIFICATION_TYPE.tizen 115 | }; 116 | } 117 | 118 | function getDetailOptionalQuestion() { 119 | return { 120 | type: 'confirm', 121 | name: 'details', 122 | message: 'Do you want to fill in more details? (Optional)', 123 | default: false 124 | }; 125 | } 126 | 127 | function getProfileName() { 128 | return { 129 | type: 'input', 130 | name: 'profileName', 131 | message: 'Input an unique "profileName" : ', 132 | validate: input => { 133 | return input !== '' 134 | ? (profileNames && profileNames.indexOf(input)) === -1 135 | ? true 136 | : 'The name is alreaday in use.' 137 | : 'Input an unique name'; 138 | } 139 | }; 140 | } 141 | 142 | function getKeyFileName() { 143 | return { 144 | type: 'input', 145 | name: 'keyFileName', 146 | message: 'Input keyFileName : ', 147 | validate: input => { 148 | return input !== '' 149 | ? !input.match(regExp.CONTAIN_WHITESPACE) 150 | ? true 151 | : 'Input without whitespace' 152 | : 'Invalid value'; 153 | } 154 | }; 155 | } 156 | 157 | function getAuthorName() { 158 | return { 159 | type: 'input', 160 | name: 'authorName', 161 | message: 'Input an authorName : ', 162 | validate: input => { 163 | return input !== '' 164 | ? !input.match(regExp.CONTAIN_WHITESPACE) 165 | ? true 166 | : 'Input without whitespace' 167 | : 'Invalid value'; 168 | } 169 | }; 170 | } 171 | 172 | function getAuthorPassword() { 173 | return { 174 | type: 'password', 175 | name: 'authorPassword', 176 | mask: true, 177 | message: 'Input a password for author certification : ', 178 | validate: input => { 179 | const MIN_LEN = 8; 180 | return input.length >= MIN_LEN ? true : 'Input over 8 characters'; 181 | } 182 | }; 183 | } 184 | 185 | function getCountryInfo() { 186 | return { 187 | type: 'input', 188 | name: 'countryInfo', 189 | message: 'Input countryInfo : ' 190 | }; 191 | } 192 | 193 | function getStateInfo() { 194 | return { 195 | type: 'input', 196 | name: 'stateInfo', 197 | message: 'Input stateInfo : ' 198 | }; 199 | } 200 | 201 | function getCityInfo() { 202 | return { 203 | type: 'input', 204 | name: 'cityInfo', 205 | message: 'Input cityInfo : ' 206 | }; 207 | } 208 | 209 | function getOrganizationInfo() { 210 | return { 211 | type: 'input', 212 | name: 'organizationInfo', 213 | message: 'Input organizationInfo : ' 214 | }; 215 | } 216 | 217 | function getDepartmentInfo() { 218 | return { 219 | type: 'input', 220 | name: 'departmentInfo', 221 | message: 'Input departmentInfo : ' 222 | }; 223 | } 224 | 225 | function getEmailInfo() { 226 | return { 227 | type: 'input', 228 | name: 'emailInfo', 229 | message: 'Input emailInfo : ' 230 | }; 231 | } 232 | 233 | function getPrivilegeLevel() { 234 | return { 235 | type: 'list', 236 | name: 'privilegeLevel', 237 | message: 'Select privilege Level : ', 238 | // choices: [PRIVILEGE_LEVEL.public, PRIVILEGE_LEVEL.partner], 239 | choices: [PRIVILEGE_LEVEL.public], 240 | default: PRIVILEGE_LEVEL.public 241 | }; 242 | } 243 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WITs 2 | 3 | WITs is a useful development tool for helping to run and develop your Tizen web application easily on your **2017+ Samsung TV**. 4 | It is the fastest way to get your local code running on the TV device during development. Saving you from having to build, package, and reinstall your application every time you want to see how it will run on device. We call it a `LIVE RELOAD`. 5 | **WITs is helpful to continue your developing context.** 6 | 7 | 8 | 9 | ## Supported WITs guide in other languages 10 | 11 | - [简体中文](doc/README_zh_HANS.md) 12 | - [正體中文](doc/README_zh_HANT.md) 13 | 14 | Always welcome, if you contribute WITs guide in your language! 15 | Guides should be placed in "doc" directory. 16 | Please pull-request and join it! 17 | 18 | ## Installing and Configuring WITs 19 | 20 | ### For developers using npm 21 | 22 | #### 1. Install WITs npm globally 23 | 24 | ```sh 25 | $ npm install -g @tizentv/wits 26 | ``` 27 | 28 | ### For developers using Git repository 29 | 30 | #### 1. Clone WITs git repository. 31 | 32 | ```sh 33 | $ git clone https://github.com/Samsung/Wits.git 34 | ``` 35 | 36 | #### 2. Install WITs Dependencies 37 | 38 | ```sh 39 | $ cd ~/{path-to}/Wits 40 | $ npm install -g 41 | ``` 42 | 43 | ## **System Requirements** 44 | 45 | WITs needs the following prerequisites on your local development machine. 46 | 47 | #### 1. Open **`Terminal` on MacOS / Linux** or **`CMD` / `PowerShell` on Windows** 48 | 49 | #### 2. Install Node.js and Git (recommend v7.10.1 ~) 50 | 51 | We will not describe how to do these installations as there are many ways to do it and its developer preference. We recommend using something like `nvm` or `asdf` to manage different versions of Node.js across your code projects. 52 | 53 | #### 3. Developer Mode is enabled on your Samsung TV. 54 | 55 | - 1 With your Samsung Remote, press the `Home` button. 56 | 57 | - 2 Navigate to the `Apps` button and press `Enter/OK`. 58 | 59 | - 3 When on the `Apps` screen, press `1` `2` `3` `4` `5` in order on the remote to open the `Developer Mode Dialog`. If this doesn't work, try it again. 60 | 61 | - 4 When the Developer Mode Dialog appears, toggle the switch to `On` and enter the IP address of your development machine. 62 | 63 | #### 4. Certification for packaging application (Tizen / Samsung) 64 | 65 | Certification(Tizen / Samsung) is required for packaging your tizen web application. 66 | 67 | `Using Editor` 68 | 69 | - Tizen Studio 70 | Install the latest version of [Tizen Studio](http://developer.samsung.com/tv). 71 | 72 | - VSCode 73 | Install the latest version of [VSCode](https://code.visualstudio.com/). 74 | And download the extension "tizensdk.tizentv". 75 | 76 | - Atom 77 | Install the latest version of [Atom](https://atom.io/). 78 | And download the package "atom-tizentv-2" 79 | 80 | `Using WITs` 81 | 82 | - WITs (v2.4.0 ~) supports creating a Tizen certification. 83 | Please do "wits -c" for making a new Tizen certification. 84 | 85 | ## WITs details 86 | 87 | ### The Project Structure for using WITs 88 | 89 | .witsconfig.json, .witsignore files are only added at the your tizen web application. 90 | 91 | ### WITs CLI 92 | 93 | #### `wits` 94 | 95 | For showing which options you can use 96 | 97 | #### `wits -i` / `wits --init` 98 | 99 | For configuring WITs 100 | Please note that, It should be run when you use first time on your tizen application project. 101 | .witsconfig.json and .witsignore files are generated on your tizen app project. 102 | After then, you can modify your information to them. 103 | 104 | ![witsi](https://user-images.githubusercontent.com/1733182/77503919-3ddef280-6ea2-11ea-9bb4-06f3cb9ebbc6.gif) 105 | 106 | #### `wits -c` / `wits --certificate` 107 | 108 | For creating a certification(Supported Tizen certification only). 109 | As following steps, you can create a certification on `~/{path-to}/wits/resource/profiles.xml`. 110 | 111 | ![witsc](https://user-images.githubusercontent.com/1733182/92706471-7fe7ec00-f38f-11ea-8d47-47b13f956906.gif) 112 | 113 | #### `wits -s` / `wits --start` 114 | 115 | All in one. For connecting to TV, installing and launching your app and using Live Reload 116 | If `wits -i` hasn't run before, It is not allowed to run. 117 | 118 | ```sh 119 | # Run wits --start 120 | $ wits -s 121 | 122 | # Run wits --start with deviceIp. Available to switch the device Ip easily. 123 | $ wits -s deviceIp=192.168.250.250 124 | 125 | # Run wits --start with deviceIp. Available to switch the device Ip easily. For debugging, add --verbose option. It should be at the end of command. 126 | $ wits -s deviceIp=192.168.250.250 --verbose 127 | ``` 128 | 129 | ![witss](https://user-images.githubusercontent.com/1733182/77503927-420b1000-6ea2-11ea-88f5-49ab0c5fc227.gif) 130 | 131 | #### `wits -w` / `wits --watch` 132 | 133 | For connecting to TV, using Live Reload 134 | After connecting, every time you make changes on `your tizen app project`, It is reflected to TV device instantly. 135 | 136 | ```sh 137 | # Run wits --watch 138 | $ wits -w 139 | 140 | # Run wits --watch with deviceIp. Available to switch the device Ip easily. 141 | $ wits -w deviceIp=192.168.250.250 142 | 143 | # Run wits --watch with deviceIp. Available to switch the device Ip easily. For debugging, add --verbose option. It should be at the end of command. 144 | $ wits -w deviceIp=192.168.250.250 --verbose 145 | ``` 146 | 147 | ![witsw](https://user-images.githubusercontent.com/1733182/77503928-43d4d380-6ea2-11ea-8ece-4f5182cb7d6d.gif) 148 | 149 | ### WITs API 150 | 151 | WITs supports the following APIs 152 | 153 | - [setWitsconfigInfo(WitsInfoData data)](https://github.com/Samsung/Wits/wiki/How-to-use-WITs-as-APIs#setwitsconfiginfo) : This API is for setting WITs environment, It should be called before start function or watch function. 154 | - [start()](https://github.com/Samsung/Wits/wiki/How-to-use-WITs-as-APIs#start) : This API is a sequence for building and installing your application, connecting PC and Target TV, pushing files, supporting live-reload feature. 155 | - [watch()](https://github.com/Samsung/Wits/wiki/How-to-use-WITs-as-APIs#watch) : This API is a sequence for connecting PC and Target TV, pushing files, supporting live-reload feature. (Except for re-building and re-installing your application.) 156 | - [disconnect()](https://github.com/Samsung/Wits/wiki/How-to-use-WITs-as-APIs#disconnect) : This API is for disconnecting communications between PC and Target TV. 157 | - [setOutputChannel(OutputCallback callback)](https://github.com/Samsung/Wits/wiki/How-to-use-WITs-as-APIs#setoutputchannel) : This is for getting WITs' log information. The return value is integer value between 1000 and 9999, and this value will be used to unsetOutputChannel(). 158 | - [unsetOutputChannel()](https://github.com/Samsung/Wits/wiki/How-to-use-WITs-as-APIs#unsetoutputchannel) : This is for unregistering the output callback. Call the unsetOutputChannel() with the return value of the setOutputChannel(). 159 | 160 | For detail, check whole APIs in [How to use WITs as APIs](https://github.com/Samsung/Wits/wiki/How-to-use-WITs-as-APIs). 161 | 162 | ### .witsconfig.json of WITs 163 | 164 | For details, [Sample data for .witsconfig.json](https://github.com/Samsung/Wits/wiki/Set-Wits-Environment#data-structure-of-witsconfigjson) 165 | on `Windows` and `MacOS` both, **WITs** recognises path segment only one separator(**`/`**). 166 | 167 | - **connectionInfo** (mandatory) 168 | - deviceIp [string] : Device(TV) Ip address (In case of Emulator, Please input 0.0.0.0) 169 | - hostIp [string] : Host(PC) Ip address 170 | - width [string] : Resolution 171 | - isDebugMode [boolean] : Setting true, chrome inspector is launched automatically. / Setting false, nothing happened. 172 | - **profileInfo** (mandatory) 173 | - path [string] : Tizen Studio Certificate Profile path 174 | 175 | ### .witsignore of WITs 176 | 177 | Sometimes there are a few files what you do not want to push to your TV device such as `.git` or `node_modules`. 178 | If you input unnecessary files or directories on .witsignore file before pushing files to the TV device, It would be pushed except them to your TV. 179 | You can use it optionally. 180 | This works exactly same as `.gitignore`. 181 | 182 | Example of `.witsignore`: 183 | 184 | ```text 185 | node_modules 186 | .git 187 | deprecated 188 | stglib 189 | ``` 190 | 191 | ## Known Issues 192 | 193 | ### failed to live reload on react application 194 | 195 | - Check the router what you uses, and change to MemoryRouter. 196 | Please refer the closed [Issue #114](https://github.com/Samsung/Wits/issues/114) 197 | 198 | ## FAQ 199 | 200 | - [WITs FAQ](https://github.com/Samsung/Wits/wiki/Frequently-Asked-Questions) 201 | - [How to use debug mode on WITs with Chrome inspector](https://github.com/Samsung/Wits/wiki/Frequently-Asked-Questions#answer-1) 202 | - [How to get your TV IP Address](https://github.com/Samsung/Wits/wiki/Frequently-Asked-Questions#question-7) 203 | 204 | ## Supported Platforms 205 | 206 | - 2017 Samsung Smart TV (Tizen 3.0) 207 | - 2018 Samsung Smart TV (Tizen 4.0) 208 | - 2019 Samsung Smart TV (Tizen 5.0) 209 | - 2020 Samsung Smart TV (Tizen 5.5) 210 | -------------------------------------------------------------------------------- /container/js/base.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Samsung Electronics Co., Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | (function () { 20 | var iframeElem = null; 21 | var loadingElem = null; 22 | var pieSliceLeftElem = null; 23 | var pieSliceRightElem = null; 24 | var connectIconElem = null; 25 | var connectMessageElem = null; 26 | var socket = null; 27 | var iconDimTimer = null; 28 | var isLoadingContents = false; 29 | var CONTENT_PATH = '{{CONTENT_PATH}}'; 30 | var CONTENT_SRC = '{{CONTENT_SRC}}'; 31 | var IP = '{{HOST_IP}}'; 32 | var PORT = '{{HOST_PORT}}'; 33 | var CONNECTED = 'Connected'; 34 | var DISCONNECTED = 'Disconnected'; 35 | 36 | window.onload = function () { 37 | console.log('onload!!!'); 38 | var CONNECTION_WAIT_TIME = 500; 39 | iframeElem = document.getElementById('contentHTML'); 40 | loadingElem = document.getElementById('loading'); 41 | connectIconElem = document.getElementById('connectIcon'); 42 | connectMessageElem = document.getElementById('connectMessage'); 43 | pieSliceLeftElem = document.getElementById('pieSliceLeft'); 44 | pieSliceRightElem = document.getElementById('pieSliceRight'); 45 | toggleConnectInfo(DISCONNECTED); 46 | runIconDimAnimation(); 47 | 48 | if (isLaunchFromCommand()) { 49 | tizen.filesystem.resolve( 50 | CONTENT_PATH, 51 | function (path) { 52 | path.parent.deleteDirectory( 53 | path.fullPath, 54 | true, 55 | function () { 56 | console.log('Directory Deleted'); 57 | connectPC(); 58 | }, 59 | function (e) { 60 | console.log( 61 | '[Warning]: Failed to delete directory ' + CONTENT_PATH 62 | ); 63 | connectPC(); 64 | } 65 | ); 66 | }, 67 | function (e) { 68 | console.log('[Warning]: Failed to resolved ' + CONTENT_PATH); 69 | connectPC(); 70 | }, 71 | 'rw' 72 | ); 73 | } else { 74 | tizen.filesystem.resolve( 75 | CONTENT_PATH, 76 | function (path) { 77 | loadingElem.innerHTML = 'loading : 100%'; 78 | loadingElem.style.width = '100%'; 79 | stopIconDimAnimation(); 80 | connectPC(); 81 | setTimeout(function () { 82 | loadContent(CONTENT_SRC); 83 | }, CONNECTION_WAIT_TIME); 84 | }, 85 | function (e) { 86 | alert('Failed to resolve Content Application'); 87 | tizen.application.getCurrentApplication().exit(); 88 | }, 89 | 'r' 90 | ); 91 | } 92 | }; 93 | 94 | function loadContent(contentSrc) { 95 | var CONTENT_LOAD_WAIT_TIME = 1000; 96 | try { 97 | setTimeout(function () { 98 | iframeElem.src = contentSrc; 99 | iframeElem.style.display = 'block'; 100 | iframeElem.onload = function () { 101 | iframeElem.focus(); 102 | hideWitsContainer(); 103 | }; 104 | }, CONTENT_LOAD_WAIT_TIME); 105 | } catch (e) { 106 | console.log('Failed to load content', e); 107 | } 108 | } 109 | 110 | function reloadContent() { 111 | try { 112 | iframeElem.contentDocument.location.reload(true); 113 | iframeElem.style.display = 'block'; 114 | iframeElem.focus(); 115 | } catch (e) { 116 | console.log('Failed to reload content', e); 117 | } 118 | } 119 | 120 | function toggleConnectInfo(status) { 121 | if (status === CONNECTED) { 122 | connectIconElem.className = 'connected'; 123 | connectMessageElem.innerHTML = CONNECTED; 124 | } else { 125 | connectIconElem.className = 'disconnected'; 126 | connectMessageElem.innerHTML = DISCONNECTED; 127 | } 128 | } 129 | 130 | function runIconDimAnimation() { 131 | var ANIMATION_DURATION_TIME = 3000; 132 | var ANIMATION_PAUSE_TIME = 500; 133 | 134 | pieSliceLeftElem.className = 'slice-left slice-wrap'; 135 | pieSliceRightElem.className = 'slice-right slice-wrap'; 136 | iconDimTimer = setInterval(function () { 137 | pieSliceLeftElem.className = ''; 138 | pieSliceRightElem.className = ''; 139 | if (iconDimTimer) { 140 | setTimeout(function () { 141 | pieSliceLeftElem.className = 'slice-left slice-wrap'; 142 | pieSliceRightElem.className = 'slice-right slice-wrap'; 143 | }, ANIMATION_PAUSE_TIME); 144 | } 145 | }, ANIMATION_DURATION_TIME); 146 | } 147 | 148 | function stopIconDimAnimation() { 149 | if (iconDimTimer) { 150 | clearInterval(iconDimTimer); 151 | iconDimTimer = null; 152 | } 153 | } 154 | function hideWitsContainer() { 155 | var witsContainerElem = document.getElementById('witsContainer'); 156 | witsContainerElem.style.display = 'none'; 157 | } 158 | 159 | function isLaunchFromCommand() { 160 | var reqAppControl = tizen.application 161 | .getCurrentApplication() 162 | .getRequestedAppControl(); 163 | 164 | var isFromCommand = true; 165 | 166 | if (reqAppControl.appControl.data) { 167 | console.log('reqAppControl.appControl.data'); 168 | console.log(reqAppControl.appControl.data); 169 | var launchData = reqAppControl.appControl.data; 170 | launchData.forEach(function (item) { 171 | if (item.key === 'callerid') { 172 | isFromCommand = false; 173 | } 174 | }); 175 | } 176 | return isFromCommand; 177 | } 178 | 179 | function connectPC() { 180 | var url = IP + ':' + PORT; 181 | var options = { 182 | reconnection: false, 183 | reconnectionAttempts: 2, 184 | reconnectionDelay: 1000, 185 | reconnectionDelayMax: 5000, 186 | timeout: 5000, 187 | autoConnect: false 188 | }; 189 | 190 | socket = io(url, options); 191 | 192 | socket.on('connect_error', function (err) { 193 | alert(`Connect Error(${url}): ${err.message}`); 194 | tizen.application.getCurrentApplication().exit(); 195 | }); 196 | 197 | socket.on('response', function (chunk) { 198 | console.log('socket on::::response', chunk); 199 | if (chunk.rsp.status === 'connected') { 200 | toggleConnectInfo(CONNECTED); 201 | if (isLaunchFromCommand()) { 202 | socket.emit('push_request'); 203 | } 204 | socket.emit('watch_request', { 205 | destPath: CONTENT_PATH 206 | }); 207 | } 208 | }); 209 | 210 | socket.on('disconnect', function () { 211 | console.log(' disconnect, id = ' + socket.id); 212 | toggleConnectInfo(DISCONNECTED); 213 | socket.disconnect(true); 214 | socket.close(); 215 | if (isLoadingContents) { 216 | alert('Failed to load Content Application'); 217 | tizen.application.getCurrentApplication().exit(); 218 | } 219 | }); 220 | 221 | socket.on('push_progress', function (info) { 222 | console.log('socket on::::push_progress'); 223 | isLoadingContents = true; 224 | loadingElem.innerHTML = 225 | 'loading : ' + 226 | info.progressRate + 227 | ' (' + 228 | info.load + 229 | '/' + 230 | info.total + 231 | ')'; 232 | loadingElem.style.width = info.progressRate; 233 | }); 234 | 235 | socket.on('push_completed', function () { 236 | console.log('socket on::::push_completed'); 237 | stopIconDimAnimation(); 238 | loadContent(CONTENT_SRC); 239 | isLoadingContents = false; 240 | }); 241 | 242 | socket.on('push_failed', function () { 243 | console.log('socket on::::push_failed'); 244 | alert('Failed to load Content Application'); 245 | tizen.application.getCurrentApplication().exit(); 246 | }); 247 | 248 | socket.on('changed', function () { 249 | reloadContent(); 250 | }); 251 | 252 | socket.on('remove', function (path) { 253 | tizen.filesystem.resolve( 254 | path, 255 | function (data) { 256 | if (data.isDirectory) { 257 | data.parent.deleteDirectory( 258 | data.fullPath, 259 | true, 260 | function () { 261 | console.log('Directory Deleted'); 262 | reloadContent(); 263 | }, 264 | function (e) { 265 | console.log('Error to Delete Directory.' + e.message); 266 | } 267 | ); 268 | } else { 269 | data.parent.deleteFile( 270 | data.fullPath, 271 | function () { 272 | console.log('file Deleted'); 273 | reloadContent(); 274 | }, 275 | function (e) { 276 | console.log('Error to Delete file.' + e.message); 277 | } 278 | ); 279 | } 280 | }, 281 | function (e) { 282 | console.log('Error: ' + e.message); 283 | }, 284 | 'rw' 285 | ); 286 | }); 287 | socket.open(); 288 | } 289 | })(); 290 | -------------------------------------------------------------------------------- /lib/watchHelper.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const chalk = require('chalk'); 4 | const { exec } = require('child_process'); 5 | const watch = require('node-watch'); 6 | const recursiveReadDir = require('recursive-readdir'); 7 | const express = require('express'); 8 | const app = express(); 9 | const http = require('http').createServer(app); 10 | const io = require('socket.io')(http); 11 | const htmlParser = require('node-html-parser'); 12 | 13 | const regExp = require('./regexp.js'); 14 | const appLaunchHelper = require('./appLaunchHelper.js'); 15 | const hostAppHelper = require('./hostAppHelper.js'); 16 | const util = require('./util.js'); 17 | const { logger } = require('./logger'); 18 | 19 | const WATCHER_EVENT_UPDATE = 'update'; 20 | const WATCHER_EVENT_REMOVE = 'remove'; 21 | const WITs_IGNORE_FILE = '.witsignore'; 22 | let watcher = {}; 23 | let mediator = {}; 24 | let witsIgnores = []; 25 | let deviceName = ''; 26 | 27 | module.exports = { 28 | openSocketServer: async (data, deviceInfo) => { 29 | const socketPort = data.socketPort; 30 | const hostAppId = hostAppHelper.getHostAppId(data.baseAppPath); 31 | const hostAppName = hostAppId.split('.')[1]; 32 | deviceName = deviceInfo.deviceName; //global 33 | const hostAppPath = deviceInfo.appInstallPath + hostAppName; 34 | module.exports.closeSocketServer(); 35 | appLaunchHelper.terminateApp(deviceName, hostAppId); 36 | 37 | http.listen(socketPort, data.hostIp, () => { 38 | logger.log(`listening on ${socketPort}`); 39 | }); 40 | 41 | http.on('close', () => { 42 | logger.log(chalk.cyanBright(`Close listening: ${socketPort}`)); 43 | }); 44 | 45 | mediator = io.on('connection', socket => { 46 | logger.log(`a user connected`); 47 | logger.log(`new client connected, id = ${socket.id} `); 48 | 49 | socket.emit('response', { 50 | rsp: { 51 | status: 'connected' 52 | } 53 | }); 54 | 55 | socket.on('disconnect', () => { 56 | logger.log(`disconnect, id = ${socket.id}`); 57 | socket.disconnect(true); 58 | watcher.close(); 59 | }); 60 | 61 | socket.once('connect_error', () => { 62 | logger.log(`socket once::::connect_error`); 63 | }); 64 | 65 | socket.on('push_request', () => { 66 | logger.log(`socket on::::push_request`); 67 | startPushFiles(data.baseAppPath, hostAppPath); 68 | }); 69 | 70 | socket.on('watch_request', path => { 71 | logger.log(`socket on::::watch_request`); 72 | watchAppCode(data.baseAppPath, path.destPath); 73 | }); 74 | }); 75 | }, 76 | closeSocketServer: () => { 77 | watcher.close && watcher.close(); 78 | http.close(); 79 | io.removeAllListeners('connection'); 80 | io.close(); 81 | } 82 | }; 83 | 84 | function ignoreFunc(file, stats) { 85 | return witsIgnores.includes(path.basename(file)); 86 | } 87 | 88 | function getWitsIgnore(baseAppPath) { 89 | const file = path.resolve(path.join(baseAppPath, WITs_IGNORE_FILE)); 90 | let ignore = []; 91 | 92 | try { 93 | let ignoreData = fs.readFileSync(file, 'utf8').trim(); 94 | if (ignoreData && ignoreData.length > 0) { 95 | ignore = ignoreData.replace(regExp.FIND_ALL_CR, '').split('\n'); 96 | } 97 | } catch (e) { 98 | logger.warn(`[warning] Failed to get Wits ignore ${e}`); 99 | } 100 | return ignore; 101 | } 102 | 103 | async function getContentFiles(baseAppPath) { 104 | let data = []; 105 | try { 106 | data = await recursiveReadDir(baseAppPath, [ignoreFunc]); 107 | } catch (e) { 108 | logger.error(chalk.red(`Failed to get content files`)); 109 | util.exit(); 110 | return; 111 | } 112 | 113 | return data; 114 | } 115 | 116 | function updatePushProgress(currentNumber, totalNumber) { 117 | if (currentNumber > totalNumber) { 118 | currentNumber = totalNumber; 119 | } 120 | 121 | mediator.emit('push_progress', { 122 | total: totalNumber, 123 | load: currentNumber, 124 | progressRate: Math.floor((currentNumber / totalNumber) * 100) + '%' 125 | }); 126 | } 127 | 128 | function startPushFiles(baseAppPath, hostAppPath) { 129 | const START_PUSH_INDEX = 0; 130 | let totalFileNum = 0; 131 | 132 | witsIgnores = getWitsIgnore(baseAppPath); 133 | 134 | getContentFiles(baseAppPath).then(files => { 135 | totalFileNum = files.length; 136 | logger.log(`Total File Number : ${totalFileNum}`); 137 | const contentFilesInfo = { 138 | files: files, 139 | curIdx: START_PUSH_INDEX, 140 | totalFileNum: totalFileNum 141 | }; 142 | pushFile(baseAppPath, hostAppPath, contentFilesInfo); 143 | }); 144 | } 145 | 146 | function pushFile(baseAppPath, hostAppPath, filesInfo) { 147 | if (filesInfo.curIdx >= filesInfo.totalFileNum) { 148 | mediator.emit('push_completed'); 149 | } else { 150 | const file = filesInfo.files[filesInfo.curIdx]; 151 | let filePath = path.isAbsolute(file) 152 | ? file.replace(regExp.BACKSLASH, '/') 153 | : util.getAbsolutePath(file); 154 | const fileName = filePath.replace(baseAppPath, ''); 155 | const contentSrc = getContentSrc(baseAppPath); 156 | if ( 157 | !util.isRemoteUrl(contentSrc) && 158 | contentSrc === fileName.replace(regExp.FIRST_BACKSLASH, '') 159 | ) { 160 | try { 161 | pushFsWrapperFile(hostAppPath); 162 | filePath = getWrappedContentFiles(filePath, fileName); 163 | } catch (e) { 164 | logger.log('[Warning]: Failed to wrapped FileSystem to contents file.'); 165 | } 166 | } 167 | const CONTENT_FILE_PUSH_COMMAND = `${util.TOOLS_SDB_PATH} -s ${deviceName} push "${filePath}" "${hostAppPath}${fileName}"`; 168 | const pushResult = exec(CONTENT_FILE_PUSH_COMMAND, { 169 | async: true, 170 | encoding: 'utf-8', 171 | stdio: 'pipe' 172 | }); 173 | // util.displayOutput(pushResult); 174 | 175 | pushResult.stderr.on('data', data => { 176 | const COMPATIBILITY_ERROR = 'version compatibility problems'; 177 | if (!data.includes(COMPATIBILITY_ERROR)) { 178 | mediator.emit('push_failed'); 179 | } 180 | }); 181 | pushResult.stdout.on('data', data => { 182 | if (regExp.PUSHED_FILE_MESSAGE.test(data)) { 183 | ++filesInfo.curIdx; 184 | updatePushProgress(filesInfo.curIdx, filesInfo.totalFileNum); 185 | pushFile(baseAppPath, hostAppPath, filesInfo); 186 | } 187 | }); 188 | } 189 | } 190 | 191 | function watchAppCode(basePath, destPath) { 192 | watcher = watch(basePath, { recursive: true }, (evt, name) => { 193 | logger.log(`${name} ${evt}`); 194 | const filePath = name.replace(regExp.BACKSLASH, '/').replace(basePath, ''); 195 | logger.log(`watch file : ${filePath}`); 196 | if (!isIgnore(filePath)) { 197 | if (evt === WATCHER_EVENT_UPDATE) { 198 | pushUpdated(basePath, destPath, filePath); 199 | } else if (evt === WATCHER_EVENT_REMOVE) { 200 | emitRemoved(basePath, destPath, filePath); 201 | } 202 | } 203 | }); 204 | } 205 | 206 | function isIgnore(path) { 207 | let isIgnore = false; 208 | witsIgnores.some(ignore => { 209 | if (path.includes(ignore)) { 210 | logger.log(`This watch file is ignore.`); 211 | isIgnore = true; 212 | return true; 213 | } 214 | }); 215 | return isIgnore; 216 | } 217 | 218 | function pushUpdated(basePath, destPath, filePath) { 219 | const contentSrc = getContentSrc(basePath); 220 | let fileName = filePath.replace(regExp.FIRST_BACKSLASH, ''); 221 | let fileFullPath = basePath + filePath; 222 | if (!util.isRemoteUrl(contentSrc) && contentSrc === fileName) { 223 | try { 224 | fileFullPath = getWrappedContentFiles(fileFullPath, fileName); 225 | } catch (e) { 226 | logger.log('[Warning]: Failed to wrapped FileSystem to contents file.'); 227 | } 228 | } 229 | const UPDATE_FILE_PUSH_COMMAND = `${util.TOOLS_SDB_PATH} -s ${deviceName} push "${fileFullPath}" "${destPath}${filePath}"`; 230 | const result = exec( 231 | UPDATE_FILE_PUSH_COMMAND, 232 | { encoding: 'utf-8', stdio: 'pipe' }, 233 | (code, stdout, stderr) => { 234 | if (stderr) { 235 | const COMPATIBILITY_ERROR = 'version compatibility problems'; 236 | if (!stderr.includes(COMPATIBILITY_ERROR)) { 237 | logger.log(`Failed ${stderr}`); 238 | util.exit(); 239 | } 240 | } 241 | logger.log(`Program output : ${stdout}`); 242 | if (stdout.includes('file(s) pushed')) { 243 | mediator.emit('changed'); 244 | } 245 | } 246 | ); 247 | 248 | // util.displayOutput(result); 249 | } 250 | 251 | function emitRemoved(basePath, destPath, filePath) { 252 | logger.log(filePath); 253 | mediator.emit(WATCHER_EVENT_REMOVE, destPath + filePath); 254 | } 255 | 256 | function getContentSrc(baseAppPath) { 257 | let contentSrc = 'index.html'; 258 | 259 | try { 260 | const file = path.resolve(path.join(baseAppPath, 'config.xml')); 261 | let data = fs.readFileSync(file, 'utf8'); 262 | data = util.clearComment(data); 263 | contentSrc = data 264 | .match(regExp.CONTENT_SRC)[0] 265 | .replace(regExp.CONTENT_SRC_ATTRIBUTE, ''); 266 | } catch (e) { 267 | logger.warn( 268 | `[warning] Failed to read config.xml. Set Content src to default.` 269 | ); 270 | } 271 | return contentSrc.replace(regExp.FIRST_BACKSLASH, ''); 272 | } 273 | 274 | function pushFsWrapperFile(hostAppPath) { 275 | const WRAPPER_FILE = 'wrapper/filesystemWrapper.js'; 276 | const WRAPPER_FILE_PATH = path 277 | .join(util.WITS_BASE_PATH, WRAPPER_FILE) 278 | .replace(regExp.BACKSLASH, '/'); 279 | const WRAPPER_FILE_PUSH_COMMAND = `${util.TOOLS_SDB_PATH} -s ${deviceName} push "${WRAPPER_FILE_PATH}" "${hostAppPath}/${WRAPPER_FILE}"`; 280 | const result = exec(WRAPPER_FILE_PUSH_COMMAND, { 281 | async: true, 282 | encoding: 'utf-8', 283 | stdio: 'pipe' 284 | }); 285 | // util.displayOutput(result); 286 | } 287 | 288 | function getWrappedContentFiles(filePath, fileName) { 289 | let fileData = fs.readFileSync(filePath, 'utf8'); 290 | let newFileData = appendFsWrapperScript(fileData); 291 | let newFilePath = path.join(util.WITS_BASE_PATH, 'wrapper', fileName); 292 | fs.writeFileSync(newFilePath, newFileData, 'utf8'); 293 | return newFilePath; 294 | } 295 | 296 | function appendFsWrapperScript(fileData) { 297 | const root = htmlParser.parse(fileData); 298 | root 299 | .querySelector('head') 300 | .appendChild( 301 | '\n' 302 | ); 303 | return root.toString(); 304 | } 305 | -------------------------------------------------------------------------------- /lib/hostAppHelper.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const xml2js = require('xml2js'); 4 | const chalk = require('chalk'); 5 | const common = require('@tizentv/webide-common-tizentv'); 6 | 7 | const util = require('./util.js'); 8 | const userInfoHelper = require('./userInfoHelper.js'); 9 | const regExp = require('./regexp.js'); 10 | const { logger } = require('./logger'); 11 | 12 | const CONFIG_FILE = 'config.xml'; 13 | const WITS_NAME_TAG = 'WITs'; 14 | 15 | module.exports = { 16 | setHostAppEnv: async (userAnswer, deviceInfo) => { 17 | await makeHostAppConfigFile(userAnswer.baseAppPath); 18 | setBaseJSData(userAnswer, deviceInfo); 19 | setBaseFsWrapper(userAnswer, deviceInfo); 20 | setBaseHtmlData(userAnswer); 21 | }, 22 | getHostAppId: baseAppPath => { 23 | try { 24 | const file = baseAppPath + '/' + CONFIG_FILE; 25 | let data = fs.readFileSync(file, 'utf8'); 26 | data = util.clearComment(data); 27 | const id = data 28 | .match(regExp.APPLICATION_ID)[0] 29 | .replace(regExp.APPLICATION_ID_ATTRIBUTE, ''); 30 | return id + WITS_NAME_TAG; 31 | } catch (e) { 32 | logger.error(chalk.red(`Failed to read base app config.xml. ${e}`)); 33 | util.exit(); 34 | } 35 | }, 36 | buildPackage: async () => { 37 | const buildPath = path.join(util.WITS_BASE_PATH, '../', 'container'); 38 | const profilePath = userInfoHelper.getLatestWitsconfigInfo().profileInfo 39 | .path; 40 | const hostAppId = module.exports.getHostAppId( 41 | util.CURRENT_PROJECT_PATH 42 | ); 43 | try { 44 | logger.log( 45 | chalk.cyanBright( 46 | `\nStart packaging Samsung Tizen TV Platform......\n` 47 | ) 48 | ); 49 | const app = new common.TVWebApp( 50 | WITS_NAME_TAG, 51 | buildPath, 52 | hostAppId 53 | ); 54 | if (!util.isFileExist(profilePath)) { 55 | logger.error( 56 | chalk.red( 57 | `Please check ${profilePath} is valid. For making a new certification, please do "wits -c"` 58 | ) 59 | ); 60 | util.exit(); 61 | } 62 | await app.buildWidget(profilePath); 63 | 64 | logger.log( 65 | chalk.cyanBright( 66 | '============================== Build Package completed!' 67 | ) 68 | ); 69 | logger.log(''); 70 | return; 71 | } catch (error) { 72 | logger.log('Failed to buildPackage : ' + error); 73 | } 74 | } 75 | }; 76 | 77 | async function makeHostAppConfigFile(baseAppPath) { 78 | let userConfigData = ''; 79 | try { 80 | userConfigData = fs.readFileSync( 81 | baseAppPath + '/' + CONFIG_FILE, 82 | 'utf8' 83 | ); 84 | } catch (e) { 85 | logger.error(chalk.red(`Failed to read user config.xml. ${e}`)); 86 | util.exit(); 87 | return; 88 | } 89 | 90 | const xmlParser = new xml2js.Parser({ attrkey: 'attributes' }); 91 | 92 | const parsedXmlData = await new Promise((resolve, reject) => 93 | xmlParser.parseString(userConfigData, function (err, result) { 94 | resolve(result); 95 | }) 96 | ); 97 | 98 | if (parsedXmlData && parsedXmlData.widget) { 99 | setDefaultConfigData(parsedXmlData.widget); 100 | } else { 101 | logger.log(`User config.xml is not supported format.`); 102 | util.exit(); 103 | } 104 | 105 | const xmlBuilder = new xml2js.Builder({ 106 | attrkey: 'attributes', 107 | xmldec: { version: '1.0', encoding: 'UTF-8' } 108 | }); 109 | 110 | const witsConfigData = xmlBuilder.buildObject(parsedXmlData); 111 | 112 | try { 113 | fs.writeFileSync( 114 | path.join(util.WITS_BASE_PATH, '../', 'container', CONFIG_FILE), 115 | witsConfigData, 116 | 'utf8' 117 | ); 118 | } catch (e) { 119 | logger.error(chalk.red(`Failed to write Wits config.xml. ${e}`)); 120 | util.exit(); 121 | return; 122 | } 123 | } 124 | 125 | function setDefaultConfigData(configData) { 126 | const WITS_CONFIG_APPLICATION = 'tizen:application'; 127 | const WITS_CONFIG_ACCESS_TAG = 'access'; 128 | const WITS_CONFIG_CONTENT_TAG = 'content'; 129 | const WITS_CONFIG_ICON_TAG = 'icon'; 130 | const WITS_CONFIG_PRIVILEGE_TAG = 'tizen:privilege'; 131 | const FILESYSTEM_READ_PRIVILEGE = 132 | 'http://tizen.org/privilege/filesystem.read'; 133 | const FILESYSTEM_WRITE_PRIVILEGE = 134 | 'http://tizen.org/privilege/filesystem.write'; 135 | 136 | configData[WITS_CONFIG_APPLICATION][0].attributes.id += WITS_NAME_TAG; 137 | 138 | configData[WITS_CONFIG_ACCESS_TAG] = [ 139 | { 140 | attributes: { 141 | origin: '*', 142 | subdomains: 'true' 143 | } 144 | } 145 | ]; 146 | 147 | // configData[WITS_CONFIG_CONTENT_TAG] = [ 148 | // { 149 | // attributes: { 150 | // src: 'index.html' 151 | // } 152 | // } 153 | // ]; 154 | 155 | configData[WITS_CONFIG_ICON_TAG] = [ 156 | { 157 | attributes: { 158 | src: 'icon.png' 159 | } 160 | } 161 | ]; 162 | 163 | if (configData.hasOwnProperty(WITS_CONFIG_PRIVILEGE_TAG)) { 164 | configData[WITS_CONFIG_PRIVILEGE_TAG].push( 165 | { 166 | attributes: { 167 | name: FILESYSTEM_READ_PRIVILEGE 168 | } 169 | }, 170 | { 171 | attributes: { 172 | name: FILESYSTEM_WRITE_PRIVILEGE 173 | } 174 | } 175 | ); 176 | } else { 177 | configData[WITS_CONFIG_PRIVILEGE_TAG] = [ 178 | { 179 | attributes: { 180 | name: FILESYSTEM_READ_PRIVILEGE 181 | } 182 | }, 183 | { 184 | attributes: { 185 | name: FILESYSTEM_WRITE_PRIVILEGE 186 | } 187 | } 188 | ]; 189 | } 190 | } 191 | 192 | function setBaseJSData(userAnswer, deviceInfo) { 193 | try { 194 | const file = path.join( 195 | util.WITS_BASE_PATH, 196 | '../', 197 | 'container', 198 | 'js', 199 | 'base.js' 200 | ); 201 | const data = fs.readFileSync(file, 'utf8'); 202 | const contentSrc = getContentSrc(userAnswer.baseAppPath); 203 | const hostAppId = module.exports.getHostAppId(userAnswer.baseAppPath); 204 | const hostAppName = hostAppId.split('.')[1]; 205 | const hostAppPath = deviceInfo.appInstallPath + hostAppName; 206 | 207 | contentSrc.replace(regExp.FIRST_BACKSLASH, ''); 208 | const contentFullSrc = util.isRemoteUrl(contentSrc) 209 | ? contentSrc 210 | : hostAppPath + 211 | '/' + 212 | contentSrc.replace(regExp.FIRST_BACKSLASH, ''); 213 | 214 | const hostIp = userAnswer.hostIp; 215 | 216 | const convertData = { 217 | '{{CONTENT_PATH}}': hostAppPath, 218 | '{{CONTENT_SRC}}': contentFullSrc, 219 | '{{HOST_IP}}': 'http://' + hostIp, 220 | '{{HOST_PORT}}': userAnswer.socketPort, 221 | '{{HOST_BASE_CONTENT_PATH}}': userAnswer.baseAppPath 222 | }; 223 | 224 | const str = data.replace(regExp.HOST_DATA, key => { 225 | return convertData[key]; 226 | }); 227 | 228 | fs.writeFileSync( 229 | path.join(util.WITS_BASE_PATH, '../', 'container', 'js', 'main.js'), 230 | str, 231 | 'utf8' 232 | ); 233 | } catch (e) { 234 | logger.error(chalk.red(`Failed to set Wits baseJS data to file ${e}`)); 235 | util.exit(); 236 | } 237 | } 238 | 239 | function setBaseHtmlData(userAnswer) { 240 | try { 241 | const file = path.join( 242 | util.WITS_BASE_PATH, 243 | '../', 244 | 'container', 245 | 'base.html' 246 | ); 247 | const data = fs.readFileSync(file, 'utf8'); 248 | 249 | const str = data.replace(regExp.HOST_WIDTH, userAnswer.width); 250 | 251 | fs.writeFileSync( 252 | path.join(util.WITS_BASE_PATH, '../', 'container', 'index.html'), 253 | str, 254 | 'utf8' 255 | ); 256 | } catch (e) { 257 | logger.error(chalk.red(`Failed to set Wits baseHtml data to file`)); 258 | util.exit(); 259 | } 260 | } 261 | 262 | function getContentSrc(baseAppPath) { 263 | let contentSrc = 'index.html'; 264 | 265 | try { 266 | const file = path.resolve(path.join(baseAppPath, 'config.xml')); 267 | let data = fs.readFileSync(file, 'utf8'); 268 | data = util.clearComment(data); 269 | contentSrc = data 270 | .match(regExp.CONTENT_SRC)[0] 271 | .replace(regExp.CONTENT_SRC_ATTRIBUTE, ''); 272 | } catch (e) { 273 | logger.warn( 274 | `[warning] Failed to read config.xml. Set Content src to default.` 275 | ); 276 | } 277 | 278 | logger.log(`content src is : ${contentSrc}`); 279 | 280 | return contentSrc; 281 | } 282 | 283 | function setBaseFsWrapper(userAnswer, deviceInfo) { 284 | try { 285 | const file = path.join( 286 | util.WITS_BASE_PATH, 287 | 'wrapper', 288 | 'baseFilesystemWrapper.js' 289 | ); 290 | const data = fs.readFileSync(file, 'utf8'); 291 | const hostAppId = module.exports.getHostAppId(userAnswer.baseAppPath); 292 | const hostAppName = hostAppId.split('.')[1]; 293 | const hostAppPath = deviceInfo.appInstallPath + hostAppName; 294 | 295 | const convertData = { 296 | '{{CONTENT_PATH}}': hostAppPath 297 | }; 298 | 299 | const str = data.replace(regExp.HOST_DATA, key => { 300 | return convertData[key]; 301 | }); 302 | 303 | fs.writeFileSync( 304 | path.join(util.WITS_BASE_PATH, 'wrapper', 'filesystemWrapper.js'), 305 | str, 306 | 'utf8' 307 | ); 308 | } catch (e) { 309 | console.error( 310 | chalk.red(`Failed to set Wits baseFsWrapper data to file ${e}`) 311 | ); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /lib/userInfoHelper.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | const chalk = require('chalk'); 3 | const fs = require('fs'); 4 | const os = require('os'); 5 | const path = require('path'); 6 | const _ = require('lodash'); 7 | 8 | const util = require('./util.js'); 9 | const deviceConnectHelper = require('./deviceConnectHelper.js'); 10 | const regExp = require('./regexp.js'); 11 | const { logger } = require('./logger'); 12 | 13 | const EMULATOR_IP = '0.0.0.0'; 14 | const WITS_CONFIG_FILE_NAME = '.witsconfig.json'; 15 | 16 | module.exports = { 17 | WITS_USER_DATA: null, 18 | getRefinedData: () => { 19 | const result = {}; 20 | 21 | const wInfo = module.exports.getLatestWitsconfigInfo(); 22 | const cInfo = wInfo.connectionInfo; 23 | const pInfo = wInfo.profileInfo; 24 | 25 | const baseAppPath = module.exports.getBaseAppPath(cInfo.baseAppPath); 26 | 27 | util.setCurrentAppPath(baseAppPath); 28 | result.baseAppPath = baseAppPath; 29 | 30 | result.width = cInfo.width; 31 | result.deviceIp = cInfo.deviceIp; 32 | result.socketPort = cInfo.socketPort; 33 | result.hostIp = cInfo.hostIp; 34 | result.isDebugMode = cInfo.isDebugMode; 35 | result.profileName = pInfo.name; 36 | result.profilePath = pInfo.path; 37 | 38 | displayStoredInfo(result); 39 | 40 | return result; 41 | }, 42 | askQuestion: async cInfo => { 43 | const result = {}; 44 | const ask = await getUserAskData(); 45 | const answer = await inquirer.prompt(ask); 46 | const baseAppPath = module.exports.getBaseAppPath(answer.baseAppPath); 47 | const hostIp = util.getValidHostIp(cInfo, answer); 48 | 49 | answer.socketPort = util.getSocketPort(); 50 | answer.hostIp = hostIp; 51 | 52 | result.baseAppPath = baseAppPath; 53 | result.width = answer.width; 54 | result.deviceIp = answer.deviceIp; 55 | result.socketPort = answer.socketPort; 56 | result.hostIp = hostIp; 57 | result.isDebugMode = answer.isDebugMode; 58 | result.profileName = answer.profileName; 59 | result.profilePath = answer.profilePath; 60 | 61 | await module.exports.updateLatestUserAnswer(answer); 62 | return result; 63 | }, 64 | getDeviceInfo: async deviceIp => { 65 | if ((deviceIp === null) | (deviceIp === undefined)) { 66 | logger.error( 67 | chalk.red( 68 | 'There is no deviceIp, Please do "wits -i" for configuration.' 69 | ) 70 | ); 71 | util.exit(); 72 | } 73 | return await deviceConnectHelper.getConnectedDeviceInfo(deviceIp); 74 | }, 75 | getLatestWitsconfigInfo: () => { 76 | const result = initWitsconfigInfo(); 77 | try { 78 | const wInfo = getWitsconfigData(); 79 | 80 | const cInfo = wInfo.connectionInfo; 81 | const pInfo = wInfo.profileInfo; 82 | 83 | for (const key in cInfo) { 84 | result.connectionInfo[key] = cInfo[key]; 85 | } 86 | 87 | for (const key in pInfo) { 88 | result.profileInfo[key] = pInfo[key]; 89 | } 90 | 91 | if (util.isPropertyExist(wInfo, 'optionalInfo')) { 92 | result.optionalInfo = wInfo.optionalInfo; 93 | } 94 | 95 | return result; 96 | } catch (e) { 97 | logger.warn(`[warning] Failed to getLatestWitsconfigInfo >> ${e}`); 98 | } 99 | }, 100 | getBaseAppPath: baseAppPath => { 101 | const appPath = baseAppPath ? baseAppPath : '.'; 102 | return path.isAbsolute(appPath) 103 | ? appPath.replace(regExp.BACKSLASH, '/') 104 | : util.getAbsolutePath(appPath); 105 | }, 106 | getOptionalInfo: async () => { 107 | const CONFIG_PATH = path.join( 108 | util.CURRENT_PROJECT_PATH, 109 | WITS_CONFIG_FILE_NAME 110 | ); 111 | 112 | if (!util.isFileExist(CONFIG_PATH)) { 113 | return null; 114 | } 115 | const data = fs.readFileSync(CONFIG_PATH, 'utf8'); 116 | if (data !== '' && typeof data === 'string') { 117 | const witsConfigData = JSON.parse(data); 118 | if (util.isPropertyExist(witsConfigData, 'optionalInfo')) { 119 | return witsConfigData.optionalInfo; 120 | } 121 | } 122 | return null; 123 | }, 124 | updateLatestUserAnswer: async userAnswer => { 125 | const savingInfo = {}; 126 | const wInfo = module.exports.getLatestWitsconfigInfo(); 127 | const cInfo = wInfo.connectionInfo; 128 | const pInfo = wInfo.profileInfo; 129 | 130 | const latestConnectionInfo = { 131 | deviceIp: userAnswer.deviceIp ? userAnswer.deviceIp : cInfo.deviceIp, 132 | hostIp: userAnswer.hostIp ? userAnswer.hostIp : cInfo.hostIp, 133 | socketPort: userAnswer.socketPort 134 | ? userAnswer.socketPort 135 | : cInfo.socketPort, 136 | width: userAnswer.width ? userAnswer.width : cInfo.width, 137 | isDebugMode: userAnswer.hasOwnProperty('isDebugMode') 138 | ? userAnswer.isDebugMode 139 | : cInfo.isDebugMode 140 | }; 141 | 142 | const profilePath = userAnswer.profilePath 143 | ? userAnswer.profilePath 144 | : pInfo.path; 145 | const latestProfileInfo = { 146 | path: profilePath.trim() 147 | }; 148 | 149 | if (userAnswer.baseAppPath) { 150 | latestConnectionInfo['baseAppPath'] = userAnswer.baseAppPath; 151 | util.CURRENT_PROJECT_PATH = userAnswer.baseAppPath; 152 | } 153 | 154 | if (cInfo.baseAppPaths) { 155 | latestConnectionInfo['baseAppPaths'] = cInfo.baseAppPaths; 156 | } 157 | 158 | if (userAnswer.hostIp) { 159 | latestConnectionInfo.hostIp = userAnswer.hostIp; 160 | } 161 | 162 | savingInfo.connectionInfo = latestConnectionInfo; 163 | savingInfo.profileInfo = latestProfileInfo; 164 | 165 | if (util.isPropertyExist(wInfo, 'optionalInfo')) { 166 | savingInfo.optionalInfo = wInfo.optionalInfo; 167 | } 168 | 169 | module.exports.WITS_USER_DATA = savingInfo; 170 | 171 | try { 172 | fs.writeFileSync( 173 | path.join(util.CURRENT_PROJECT_PATH, WITS_CONFIG_FILE_NAME), 174 | JSON.stringify(savingInfo, null, 2), 175 | 'utf8' 176 | ); 177 | } catch (e) { 178 | logger.warn('[warning] Failed to set recently connection info'); 179 | } 180 | } 181 | }; 182 | 183 | function initWitsconfigInfo() { 184 | return { 185 | connectionInfo: { 186 | deviceIp: null, 187 | socketPort: '8498', 188 | width: '1920', 189 | isDebugMode: false 190 | }, 191 | profileInfo: { 192 | path: getProfilePath() 193 | } 194 | }; 195 | } 196 | 197 | function getProfilePath() { 198 | let profilePath = ''; 199 | switch (util.PLATFORM) { 200 | case 'win32': 201 | profilePath = 'C:/tizen-studio-data/profile/profiles.xml'; 202 | break; 203 | case 'linux': 204 | default: 205 | profilePath = path.resolve( 206 | os.homedir(), 207 | 'tizen-studio-data', 208 | 'profile', 209 | 'profiles.xml' 210 | ); 211 | break; 212 | } 213 | 214 | if (!util.isFileExist(profilePath)) { 215 | profilePath = path.resolve( 216 | path.join(util.WITS_BASE_PATH, '..', 'resource', 'profiles.xml') 217 | ); 218 | } 219 | return profilePath; 220 | } 221 | 222 | function displayStoredInfo(data) { 223 | logger.log(``); 224 | logger.log(` > [ Stored Information ]`); 225 | logger.log(``); 226 | logger.log(` > baseAppPath : ${data.baseAppPath}`); 227 | logger.log(` > width : ${data.width}`); 228 | logger.log(` > deviceIp : ${data.deviceIp}`); 229 | logger.log(` > isDebugMode : ${data.isDebugMode}`); 230 | logger.log(``); 231 | logger.log(` > profile path : ${data.profilePath}`); 232 | logger.log(` > hostIp : ${data.hostIp}`); 233 | logger.log(``); 234 | } 235 | 236 | function getWitsconfigData() { 237 | try { 238 | const witsConfigInfo = JSON.parse( 239 | fs.readFileSync( 240 | path.join(util.CURRENT_PROJECT_PATH, WITS_CONFIG_FILE_NAME), 241 | 'utf8' 242 | ) 243 | ); 244 | return witsConfigInfo; 245 | } catch (e) { 246 | logger.error(chalk.red(`Failed to getWitsconfig: ${e}`)); 247 | util.exit(); 248 | } 249 | } 250 | 251 | async function getUserAskData() { 252 | const wData = module.exports.getLatestWitsconfigInfo(); 253 | const cInfo = wData.connectionInfo; 254 | const pInfo = wData.profileInfo; 255 | 256 | const baseAppPathQuestion = getBaseAppPathQuestion(cInfo); 257 | const hostIpQuestion = getHostIpQuestion(cInfo); 258 | 259 | const ask = []; 260 | ask.push(getDeviceIpQuestion(cInfo)); 261 | if (hostIpQuestion !== null) { 262 | ask.push(hostIpQuestion); 263 | } 264 | ask.push(getWidthQuestion(cInfo)); 265 | ask.push(getProfilePathQuestion(pInfo)); 266 | ask.push(getIsDebugModeQuestion(cInfo)); 267 | baseAppPathQuestion.type && ask.unshift(baseAppPathQuestion); 268 | 269 | return ask; 270 | } 271 | 272 | function getDeviceIpQuestion(cInfo) { 273 | return { 274 | type: 'input', 275 | name: 'deviceIp', 276 | message: 277 | 'Input your Device Ip address(If using Emulator, input ' + 278 | EMULATOR_IP + 279 | ') :', 280 | default: cInfo.deviceIp, 281 | validate: function (input) { 282 | return util.isIpAddress(input) 283 | ? true 284 | : 'Invalid format of Ip address which is entered.'; 285 | } 286 | }; 287 | } 288 | 289 | function getWidthQuestion(cInfo) { 290 | return { 291 | type: 'input', 292 | name: 'width', 293 | message: 'Input your Application width (1920 or 1280) :', 294 | default: cInfo.width, 295 | validate: function (input) { 296 | return input === '1920' || input === '1280' 297 | ? true 298 | : 'Tizen web Application only support 1920 or 1280 width'; 299 | } 300 | }; 301 | } 302 | 303 | function getProfilePathQuestion(pInfo) { 304 | return { 305 | type: 'input', 306 | name: 'profilePath', 307 | message: 'Input the path of profile.xml :', 308 | default: pInfo.path 309 | }; 310 | } 311 | 312 | function getIsDebugModeQuestion(cInfo) { 313 | return { 314 | type: 'confirm', 315 | name: 'isDebugMode', 316 | message: 'Do you want to launch with chrome DevTools? : ', 317 | default: cInfo.isDebugMode 318 | }; 319 | } 320 | 321 | function getHostIpQuestion(cInfo) { 322 | let question = {}; 323 | let recentlyIndex = 0; 324 | const addresses = util.getHostIpAddresses(); 325 | if (addresses.length <= 1) { 326 | return null; 327 | } 328 | if (cInfo.hostIp) { 329 | recentlyIndex = addresses.indexOf(cInfo.hostIp); 330 | } 331 | question = { 332 | type: 'list', 333 | name: 'hostIp', 334 | message: 'Select your valid PC Ip address for connecting TV:', 335 | choices: addresses, 336 | default: recentlyIndex 337 | }; 338 | return question; 339 | } 340 | 341 | function getBaseAppPathQuestion(connectionInfo) { 342 | let question = {}; 343 | let baseAppPathIndex = 0; 344 | if (connectionInfo.baseAppPaths) { 345 | if (connectionInfo.baseAppPaths.length === 1) { 346 | question = { 347 | type: 'input', 348 | name: 'baseAppPath', 349 | message: 'Input your Application Path :', 350 | default: connectionInfo.baseAppPaths[0] 351 | }; 352 | } else { 353 | baseAppPathIndex = connectionInfo.baseAppPaths.indexOf( 354 | connectionInfo.baseAppPath 355 | ); 356 | question = { 357 | type: 'list', 358 | name: 'baseAppPath', 359 | message: 'Select the app path to launch Wits :', 360 | choices: connectionInfo.baseAppPaths, 361 | default: baseAppPathIndex >= 0 ? baseAppPathIndex : 0 362 | }; 363 | } 364 | } 365 | return question; 366 | } 367 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /container/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | --------------------------------------------------------------------------------