UNPKG

nativescript

Version:

Command-line interface for building NativeScript projects

408 lines • 18.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AndroidLivesyncTool = void 0; const path = require("path"); const _ = require("lodash"); const crypto = require("crypto"); const yok_1 = require("../../common/yok"); const color_1 = require("../../color"); const PROTOCOL_VERSION_LENGTH_SIZE = 1; const PROTOCOL_OPERATION_LENGTH_SIZE = 1; const SIZE_BYTE_LENGTH = 1; const REPORT_LENGTH = 1; const DO_REFRESH_LENGTH = 1; const SYNC_OPERATION_TIMEOUT = 60000; const TRY_CONNECT_TIMEOUT = 60000; const DEFAULT_LOCAL_HOST_ADDRESS = "127.0.0.1"; class AndroidLivesyncTool { constructor($androidProcessService, $errors, $fs, $logger, $mobileHelper, $injector) { this.$androidProcessService = $androidProcessService; this.$errors = $errors; this.$fs = $fs; this.$logger = $logger; this.$mobileHelper = $mobileHelper; this.$injector = $injector; this.pendingConnectionData = null; this.operationPromises = Object.create(null); this.socketError = null; this.socketConnection = null; } async connect(configuration) { if (!configuration.appIdentifier) { this.$errors.fail(AndroidLivesyncTool.APP_IDENTIFIER_MISSING_ERROR); } if (!configuration.appPlatformsPath) { this.$errors.fail(AndroidLivesyncTool.APP_PLATFORMS_PATH_MISSING_ERROR); } if (this.socketConnection) { this.$errors.fail(AndroidLivesyncTool.SOCKET_CONNECTION_ALREADY_EXISTS_ERROR); } if (!configuration.localHostAddress) { configuration.localHostAddress = process.env.NATIVESCRIPT_LIVESYNC_ADDRESS || DEFAULT_LOCAL_HOST_ADDRESS; } const connectTimeout = configuration.connectTimeout || TRY_CONNECT_TIMEOUT; this.configuration = configuration; this.socketError = null; const port = await this.$androidProcessService.forwardFreeTcpToAbstractPort({ deviceIdentifier: configuration.deviceIdentifier, appIdentifier: configuration.appIdentifier, abstractPort: `localabstract:${configuration.appIdentifier}-livesync`, }); const connectionResult = await this.connectEventuallyUntilTimeout(this.createSocket.bind(this, port), connectTimeout, configuration); this.handleConnection(connectionResult); } async sendFile(filePath) { await this.sendFileHeader(filePath); await this.sendFileContent(filePath); } async sendFiles(filePaths) { for (const filePath of filePaths) { if (this.$fs.getLsStats(filePath).isFile()) { if (!this.$fs.exists(filePath)) { this.$errors.fail(`${filePath} doesn't exist.`); } await this.sendFile(filePath); } } } sendDirectory(directoryPath) { const list = this.$fs.enumerateFilesInDirectorySync(directoryPath); return this.sendFiles(list); } async removeFile(filePath) { this.verifyActiveConnection(); const filePathData = this.getFilePathData(filePath); const headerBuffer = Buffer.alloc(PROTOCOL_OPERATION_LENGTH_SIZE + SIZE_BYTE_LENGTH + filePathData.filePathLengthSize + filePathData.filePathLengthBytes); let offset = 0; offset += headerBuffer.write(AndroidLivesyncTool.DELETE_FILE_OPERATION.toString(), offset, PROTOCOL_OPERATION_LENGTH_SIZE); offset = headerBuffer.writeInt8(filePathData.filePathLengthSize, offset); offset += headerBuffer.write(filePathData.filePathLengthString, offset, filePathData.filePathLengthSize); headerBuffer.write(filePathData.relativeFilePath, offset, filePathData.filePathLengthBytes); const hash = crypto.createHash("md5").update(headerBuffer).digest(); await this.writeToSocket(headerBuffer); await this.writeToSocket(hash); } async removeFiles(files) { for (const file of files) { await this.removeFile(file); } } generateOperationIdentifier() { return crypto.randomBytes(16).toString("hex"); } isOperationInProgress(operationId) { return !!this.operationPromises[operationId]; } sendDoSyncOperation(options) { options = _.assign({ doRefresh: true, timeout: SYNC_OPERATION_TIMEOUT }, options); const { doRefresh, timeout, operationId } = options; const id = operationId || this.generateOperationIdentifier(); const operationPromise = new Promise((resolve, reject) => { if (!this.verifyActiveConnection(reject)) { return; } const message = `${AndroidLivesyncTool.DO_SYNC_OPERATION}${id}`; const headerBuffer = Buffer.alloc(Buffer.byteLength(message) + DO_REFRESH_LENGTH); const socketId = this.socketConnection.uid; const doRefreshCode = doRefresh ? AndroidLivesyncTool.DO_REFRESH : AndroidLivesyncTool.SKIP_REFRESH; const offset = headerBuffer.write(message); headerBuffer.writeUInt8(doRefreshCode, offset); const hash = crypto.createHash("md5").update(headerBuffer).digest(); this.writeToSocket(headerBuffer) .then(() => { this.writeToSocket(hash).catch(reject); }) .catch(reject); const timeoutId = setTimeout(() => { if (this.isOperationInProgress(id)) { this.handleSocketError(socketId, "Sync operation is taking too long"); } }, timeout); this.operationPromises[id] = { resolve, reject, socketId, timeoutId, }; }); return operationPromise; } end(error) { if (this.socketConnection) { const socketUid = this.socketConnection.uid; const socket = this.socketConnection; error = error || this.getErrorWithMessage("Socket connection ended before sync operation is complete."); //remove listeners and delete this.socketConnection this.cleanState(socketUid); //call end of the connection (close and error callbacks won't be called - listeners removed) socket.end(); socket.destroy(); //reject all pending sync requests and clear timeouts this.rejectPendingSyncOperations(socketUid, error); } } hasConnection() { return !!this.socketConnection; } async sendFileHeader(filePath) { this.verifyActiveConnection(); const filePathData = this.getFilePathData(filePath); const stats = this.$fs.getFsStats(filePathData.filePath); const fileContentLengthBytes = stats.size; const fileContentLengthString = fileContentLengthBytes.toString(); const fileContentLengthSize = Buffer.byteLength(fileContentLengthString); const headerBuffer = Buffer.alloc(PROTOCOL_OPERATION_LENGTH_SIZE + SIZE_BYTE_LENGTH + filePathData.filePathLengthSize + filePathData.filePathLengthBytes + SIZE_BYTE_LENGTH + fileContentLengthSize); if (filePathData.filePathLengthSize > 255) { this.$errors.fail("File name size is longer that 255 digits."); } else if (fileContentLengthSize > 255) { this.$errors.fail("File name size is longer that 255 digits."); } let offset = 0; offset += headerBuffer.write(AndroidLivesyncTool.CREATE_FILE_OPERATION.toString(), offset, PROTOCOL_OPERATION_LENGTH_SIZE); offset = headerBuffer.writeUInt8(filePathData.filePathLengthSize, offset); offset += headerBuffer.write(filePathData.filePathLengthString, offset, filePathData.filePathLengthSize); offset += headerBuffer.write(filePathData.relativeFilePath, offset, filePathData.filePathLengthBytes); offset = headerBuffer.writeUInt8(fileContentLengthSize, offset); headerBuffer.write(fileContentLengthString, offset, fileContentLengthSize); const hash = crypto.createHash("md5").update(headerBuffer).digest(); await this.writeToSocket(headerBuffer); await this.writeToSocket(hash); } sendFileContent(filePath) { return new Promise((resolve, reject) => { if (!this.verifyActiveConnection(reject)) { return; } const fileStream = this.$fs.createReadStream(filePath); const fileHash = crypto.createHash("md5"); fileStream .on("data", (chunk) => { fileHash.update(chunk); this.writeToSocket(chunk).catch(reject); }) .on("end", () => { this.writeToSocket(fileHash.digest()) .then(() => resolve()) .catch(reject); }) .on("error", (error) => { reject(error); }); }); } createSocket(port) { const socket = this.$injector.resolve("LiveSyncSocket"); socket.connect(port, this.configuration.localHostAddress); return socket; } checkConnectionStatus() { if (this.socketConnection === null) { const defaultError = this.getErrorWithMessage(AndroidLivesyncTool.NO_SOCKET_CONNECTION_AVAILABLE_ERROR); const error = this.socketError || defaultError; return error; } } verifyActiveConnection(rejectHandler) { const error = this.checkConnectionStatus(); if (error && rejectHandler) { rejectHandler(error); return false; } if (error && !rejectHandler) { this.$errors.fail(error.toString()); } return true; } handleConnection({ socket, data, }) { this.socketConnection = socket; this.socketConnection.uid = this.generateOperationIdentifier(); const versionLength = data.readUInt8(0); const versionBuffer = data.slice(PROTOCOL_VERSION_LENGTH_SIZE, versionLength + PROTOCOL_VERSION_LENGTH_SIZE); const appIdentifierBuffer = data.slice(versionLength + PROTOCOL_VERSION_LENGTH_SIZE, data.length); const protocolVersion = versionBuffer.toString(); const appIdentifier = appIdentifierBuffer.toString(); this.$logger.trace(`Handle socket connection for app identifier: ${appIdentifier} with protocol version: ${protocolVersion}.`); this.protocolVersion = protocolVersion; this.socketConnection.on("data", (connectionData) => this.handleData(socket.uid, connectionData)); this.socketConnection.on("close", (hasError) => this.handleSocketClose(socket.uid, hasError)); this.socketConnection.on("error", (err) => { const error = new Error(`Socket Error:\n${err}`); if (this.configuration.errorHandler) { this.configuration.errorHandler(error); } else { this.handleSocketError(socket.uid, error.message); } }); } connectEventuallyUntilTimeout(factory, timeout, configuration) { return new Promise((resolve, reject) => { let lastKnownError, isConnected = false; const connectionTimer = setTimeout(async () => { if (!isConnected) { isConnected = true; if (this.pendingConnectionData && typeof this.pendingConnectionData.socketTimer === "number") { clearTimeout(this.pendingConnectionData.socketTimer); } const applicationPid = await this.$androidProcessService.getAppProcessId(configuration.deviceIdentifier, configuration.appIdentifier); if (!applicationPid) { this.$logger.trace("In Android LiveSync tool, lastKnownError is: ", lastKnownError); this.$logger.info(color_1.color.yellow(`Application ${configuration.appIdentifier} is not running on device ${configuration.deviceIdentifier}.`)); this.$logger.info(color_1.color.cyan(`This issue may be caused by: * crash at startup (try \`ns debug android --debug-brk\` to check why it crashes) * different application identifier in your package.json and in your gradle files (check your identifier in \`package.json\` and in all *.gradle files in your App_Resources directory) * device is locked * manual closing of the application`)); reject(new Error(`Application ${configuration.appIdentifier} is not running`)); } else { reject(lastKnownError || new Error(AndroidLivesyncTool.SOCKET_CONNECTION_TIMED_OUT_ERROR)); } this.pendingConnectionData = null; } }, timeout); this.pendingConnectionData = { connectionTimer, rejectHandler: reject, }; const tryConnect = () => { const socket = factory(); const tryConnectAfterTimeout = (error) => { if (isConnected) { this.pendingConnectionData = null; return; } if (typeof error === "boolean" && error) { error = new Error("Socket closed due to error"); } lastKnownError = error; socket.removeAllListeners(); this.pendingConnectionData.socketTimer = setTimeout(tryConnect, 1000); }; this.pendingConnectionData.socket = socket; socket.once("data", (data) => { socket.removeListener("close", tryConnectAfterTimeout); socket.removeListener("error", tryConnectAfterTimeout); isConnected = true; clearTimeout(connectionTimer); resolve({ socket, data }); }); socket.on("close", tryConnectAfterTimeout); socket.on("error", tryConnectAfterTimeout); }; tryConnect(); }); } handleData(socketId, data) { const reportType = data.readUInt8(); const infoBuffer = data.slice(REPORT_LENGTH, data.length); if (reportType === AndroidLivesyncTool.ERROR_REPORT) { const errorMessage = infoBuffer.toString(); this.handleSocketError(socketId, errorMessage); } else if (reportType === AndroidLivesyncTool.OPERATION_END_REPORT) { this.handleSyncEnd({ data: infoBuffer, didRefresh: true }); } else if (reportType === AndroidLivesyncTool.OPERATION_END_NO_REFRESH_REPORT_CODE) { this.handleSyncEnd({ data: infoBuffer, didRefresh: false }); } } handleSyncEnd({ data, didRefresh, }) { const operationId = data.toString(); const promiseHandler = this.operationPromises[operationId]; if (promiseHandler) { clearTimeout(promiseHandler.timeoutId); promiseHandler.resolve({ operationId, didRefresh }); delete this.operationPromises[operationId]; } } handleSocketClose(socketId, hasError) { const errorMessage = "Socket closed from server before operation end."; this.handleSocketError(socketId, errorMessage); } handleSocketError(socketId, errorMessage) { const error = this.getErrorWithMessage(errorMessage); if (this.socketConnection && this.socketConnection.uid === socketId) { this.socketError = error; this.end(error); } else { this.rejectPendingSyncOperations(socketId, error); } } cleanState(socketId) { if (this.socketConnection && this.socketConnection.uid === socketId) { this.socketConnection.removeAllListeners(); this.socketConnection = null; } } rejectPendingSyncOperations(socketId, error) { _.keys(this.operationPromises).forEach((operationId) => { const operationPromise = this.operationPromises[operationId]; if (operationPromise.socketId === socketId) { clearTimeout(operationPromise.timeoutId); operationPromise.reject(error); delete this.operationPromises[operationId]; } }); } getErrorWithMessage(errorMessage) { const error = new Error(errorMessage); error.message = errorMessage; return error; } getFilePathData(filePath) { const relativeFilePath = this.resolveRelativePath(filePath); const filePathLengthBytes = Buffer.byteLength(relativeFilePath); const filePathLengthString = filePathLengthBytes.toString(); const filePathLengthSize = Buffer.byteLength(filePathLengthString); return { relativeFilePath, filePathLengthBytes, filePathLengthString, filePathLengthSize, filePath, }; } resolveRelativePath(filePath) { const relativeFilePath = path.relative(this.configuration.appPlatformsPath, filePath); return this.$mobileHelper.buildDevicePath(relativeFilePath); } async writeToSocket(data) { this.verifyActiveConnection(); const result = await this.socketConnection.writeAsync(data); return result; } } exports.AndroidLivesyncTool = AndroidLivesyncTool; AndroidLivesyncTool.DELETE_FILE_OPERATION = 7; AndroidLivesyncTool.CREATE_FILE_OPERATION = 8; AndroidLivesyncTool.DO_SYNC_OPERATION = 9; AndroidLivesyncTool.ERROR_REPORT = 1; AndroidLivesyncTool.OPERATION_END_REPORT = 2; AndroidLivesyncTool.OPERATION_END_NO_REFRESH_REPORT_CODE = 3; AndroidLivesyncTool.DO_REFRESH = 1; AndroidLivesyncTool.SKIP_REFRESH = 0; AndroidLivesyncTool.APP_IDENTIFIER_MISSING_ERROR = 'You need to provide "appIdentifier" as a configuration property!'; AndroidLivesyncTool.APP_PLATFORMS_PATH_MISSING_ERROR = 'You need to provide "appPlatformsPath" as a configuration property!'; AndroidLivesyncTool.SOCKET_CONNECTION_ALREADY_EXISTS_ERROR = "Socket connection already exists."; AndroidLivesyncTool.SOCKET_CONNECTION_TIMED_OUT_ERROR = "Socket connection timed out."; AndroidLivesyncTool.NO_SOCKET_CONNECTION_AVAILABLE_ERROR = "No socket connection available."; yok_1.injector.register("androidLivesyncTool", AndroidLivesyncTool); //# sourceMappingURL=android-livesync-tool.js.map