UNPKG

minxing-devtools-core

Version:
365 lines (350 loc) 13.1 kB
const WebSocketServer = require('ws').Server; const Path = require('path'); const URL = require('url'); const Fse = require('fs-extra'); const fsp = require('fs-promise'); const EventEmitter = require('events'); const os = require('os'); const _ = require('underscore'); const co = require('co'); const promisify = require('promisify-node'); const qrcode = require('qrcode'); const qs = require('qs'); const Utils = require('../utils'); const {loggerBuilder} = require('../utils'); const {QRCODE_TEMP} = require('../utils/config'); const Package = require('./package'); const handlebars = require('handlebars'); const path = require('path'); handlebars.registerHelper({ eq(v1, v2){ return v1 === v2; }, gte(v1, v2){ return v1 >= v2; } }); const buildHtmlPage = co.wrap(function *(title, ips, dataURLs){ let source = yield fsp.readFile(path.join(__dirname, 'qrcodes.html')); if (Buffer.isBuffer(source)) { source = source.toString('utf-8'); } const template = handlebars.compile(source); return template({title, ips, dataURLs}); }); const toDataURL = promisify(qrcode.toDataURL); const COMMAND = { HEART_BEAT: 4, DOWNLOAD: 2, LOG: 8, WEB_PREVIEW: 9 }; const moduleOptions = { port: null, tempPath: null, socketServer: null, workspace: null, httpServer: null, remoteIps: [], remoteIp: null, connectionCount: 0, logEmitter: new EventEmitter() }; module.exports = { getLocalIp(){ const interfaces = os.networkInterfaces(); return Object.keys(interfaces).reduce((addr, dev) => { const newAddress = interfaces[dev] .filter(detail => detail.family === 'IPv4' && detail.address !== '127.0.0.1') .map(d => d.address); return [...addr, ...newAddress]; }, []); }, info(qrCodeTitle){ // 如果 qrCodeTitle 是 非空字符串,就返回一个Promise。否则,就是一个同步方法。 const data = { ip: this.getLocalIp(), port: moduleOptions.port, connectionCount: moduleOptions.connectionCount, remoteIps: moduleOptions.remoteIps }; if (_.isString(qrCodeTitle) && !_.isEmpty(qrCodeTitle)) { return co(function *(){ yield fsp.mkdirs(QRCODE_TEMP); const {ip: ips, port} = data; const ipsStr = ips.join('_'); const qrcodeFileName = `${ipsStr}_${data.port}.html`; const qrcodeFilePath = Path.join(QRCODE_TEMP, qrcodeFileName); const exists = yield fsp.stat(qrcodeFilePath).then(stat => stat.isFile(), () => false); if (!exists) { const dataURLs = yield Promise.all(ips.map(ip => toDataURL(`native://mxDevApp?${qs.stringify({ ip, port, type: 'connect-minxing-devtool' })}`, {scale: 8}))); const html = yield buildHtmlPage(qrCodeTitle, ips, dataURLs); yield fsp.writeFile(qrcodeFilePath, html); } return _.extendOwn(data, {qrcodeFilePath}); }); } return data; }, start({tempPath, port = 8686, onConnection, onClose}){ const logDebug = loggerBuilder.debug('start'); const logInfo = loggerBuilder.info('start'); const server = require('http').createServer(co.wrap(function *(req, res){ const pathname = URL.parse(req.url).pathname; let zipPath = decodeURIComponent(pathname); if (zipPath.indexOf('/') === 0) { zipPath = zipPath.replace('/', ''); } // const zipPath = Path.join(moduleOptions.tempPath, pathname); logDebug('http download zipPath-->', zipPath); const fileSize = yield fsp.stat(zipPath).then(stats => stats.size, () => false); logDebug('file stat size->', fileSize); if (!_.isNumber(fileSize)) { this.notFound({res}); return; } res.setHeader('Accept-Ranges', 'bytes'); if (req.headers.range) { const range = Utils.parseRange(req.headers.range, fileSize); if (range) { res.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${fileSize}`); res.setHeader('Content-Length', range.end - range.start + 1); Fse.createReadStream(zipPath, { start: range.start, end: range.end }).pipe(res); } else { res.removeHeader('Content-Length'); res.writeHead(416, 'Request Range Not Satisfiable'); res.end(); } } else { res.setHeader('Content-Length', fileSize); Fse.createReadStream(zipPath).pipe(res); } })).listen(port, () => logInfo('敏行服务: %s, port: %d', JSON.stringify(this.getLocalIp()), server.address().port)); const wss = new WebSocketServer({server}).on('connection', socket => this.afterSocketConnect({ socket, onConnection, onClose })); _.extendOwn(moduleOptions, { port, tempPath, socketServer: wss, httpServer: server }); }, end(){ const logInfo = loggerBuilder.info('end'); moduleOptions.socketServer.close(); moduleOptions.httpServer.close(); logInfo('敏行 Wifi 真机同步服务 已关闭...'); }, afterSocketConnect({socket, onConnection, onClose}){ const logDebug = loggerBuilder.debug('afterSocketConnect'); const logInfo = loggerBuilder.info('afterSocketConnect'); const logWarn = loggerBuilder.warn('afterSocketConnect'); const logErr = loggerBuilder.error('afterSocketConnect'); moduleOptions.remoteIp = socket._sender._socket.remoteAddress; moduleOptions.remoteIps.push(moduleOptions.remoteIp); moduleOptions.connectionCount += 1; _.isFunction(onConnection) && onConnection(moduleOptions.remoteIp); logInfo('有设备连接, 当前连接设备数:%d, 连接主机:%s, port:%d', moduleOptions.connectionCount, JSON.stringify(moduleOptions.remoteIps), moduleOptions.port); socket.on('error', logErr).on('close', function(){ moduleOptions.remoteIp = this._sender._socket.remoteAddress; const delIndex = moduleOptions.remoteIps.indexOf(moduleOptions.remoteIp); delIndex > -1 && moduleOptions.remoteIps.splice(delIndex, 1); moduleOptions.connectionCount -= 1; logInfo(`有设备断开,当前连接设备数:${moduleOptions.connectionCount},客户端:${JSON.stringify(moduleOptions.remoteIps)}`); _.isFunction(onClose) && onClose(moduleOptions.remoteIp); }).on('message', message => { let incoming = ''; try { incoming = JSON.parse(message); } catch (e) { logWarn(`socket on message JSON.parse error->${e}`); incoming = message; } incoming.command = incoming.command - 0; if (incoming.command === COMMAND.HEART_BEAT) { const cmd = this.replyLoaderHeartbeatCmd({ command: incoming.command }); this.sendCommand({ socket, cmd }); } if (incoming.command === COMMAND.DOWNLOAD) { logDebug('incomming->', incoming); Package.getDownloadCmd({ appId: incoming.appId, projectPath: incoming.projectPath, timestamp: incoming.timestamp, tempPath: moduleOptions.tempPath }).then(cmd => { logDebug('command->', cmd); if (cmd) { this.sendCommand({ socket, cmd }); } }); } if (incoming.command === COMMAND.LOG) { this.handleLog({ cmd: incoming }); } }).send(JSON.stringify(this.httpPortCmd({port: moduleOptions.port}))); }, sync({project, updateAll}){ // 更新,全量或增量. const logWarn = loggerBuilder.warn('sync'); if (typeof project !== 'string') { logWarn(`${project} 不是一个有效的文件路径`); return; } const projectPath = Path.resolve(project); const configPath = Path.resolve(projectPath, 'plugin.properties'); const workspace = Path.resolve(projectPath, '..'); let appId = null; if (Fse.existsSync(configPath)) { const appInfo = Utils.readPropertiesSync(configPath); if (appInfo) { appId = appInfo.app_id; } } if (!appId) { logWarn(`${project} 似乎不是一个有效的敏行项目`); return; } moduleOptions.workspace = workspace; const cmd = this.syncCmd({ appId, projectPath, updateAll }); this.broadcastCommand({ socketServer: moduleOptions.socketServer, cmd }); }, webPreview({src}){ // src 9200/index.html const cmd = this.webPreviewCmd({src}); this.broadcastCommand({ socketServer: moduleOptions.socketServer, cmd }); }, preview({file}){ // 页面实时预览. const logWarn = loggerBuilder.warn('preview'); if (typeof file !== 'string') { logWarn(`${file} 不是一个有效的文件路径`); return; } file = Path.resolve(file); const {app_id: appId, project} = Utils.fetchProjectRootInfoByFile(file); if (!appId) { logWarn(`${file} 似乎不在有效的敏行项目中`); return; } moduleOptions.workspace = Path.resolve(project, '..'); const cmd = this.previewCmd({ file, appId, workspace: moduleOptions.workspace }); this.broadcastCommand({ socketServer: moduleOptions.socketServer, cmd }); }, broadcastCommand({socketServer, cmd}){ socketServer.clients.forEach(socket => { this.sendCommand({socket, cmd}); }); }, sendCommand({socket, cmd}){ const logTrace = loggerBuilder.trace('sendCommand'); const logWarn = loggerBuilder.warn('sendCommand'); const cmdStr = JSON.stringify(cmd); logTrace('send cmd->', cmd); socket.send(cmdStr, error => { if (error) { logWarn(`Socket send message error->${error}`); } }); }, log(callback){ return new Promise(resolve => { this.on('log', log => { callback(log); // eslint-disable-line callback-return resolve(); }); }); }, handleLog({cmd}){ moduleOptions.logEmitter.emit('log', { content: cmd.content, level: cmd.level }); }, on(event, callback){ moduleOptions.logEmitter.on(event, callback); }, syncCmd({appId, projectPath, updateAll = true}){ return { command: 1, appId, projectPath, updateAll, }; }, replyLoaderHeartbeatCmd({command}){ return { command }; }, webPreviewCmd({src}){ const logDebug = loggerBuilder.debug('webPreviewCmd'); src = src.indexOf('http') > -1 ? src : `http://${src}`; logDebug('src-->', src); return { command: COMMAND.WEB_PREVIEW, path: src }; }, previewCmd({file, workspace, appId}){ // 发送‘页面实时预览’指令 const absoluteUrlPath = this.absoluteUrlPath({ file, workspace, appId }); return { command: 6, path: absoluteUrlPath, appId }; }, httpPortCmd({port}){ // 返回 http 端口信息. return { command: 7, port }; }, absoluteUrlPath({file, workspace, appId}){ // 本地文件对应的服务器地址. let relativePath = Path.relative(workspace, file); relativePath = relativePath.replace(/\\/g, '/'); const absoluteUrlPath = `/${relativePath.replace(/^[^\/]*/, appId)}`; return absoluteUrlPath; }, notFound({res}){ // 404 res.writeHead(404, { 'Content-Type': 'text/plain' }); res.write('404 Not found'); res.end(); } };