nativescript
Version:
Command-line interface for building NativeScript projects
408 lines • 18.9 kB
JavaScript
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
;