@hkvstore/taco-cli
Version:
taco-cli is a command-line interface for rapid Apache Cordova development (forked from Microsoft taco-cli)
627 lines (625 loc) • 33.1 kB
JavaScript
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
/// <reference path="../../../typings/node.d.ts" />
/// <reference path="../../../typings/request.d.ts" />
/// <reference path="../../../typings/fstream.d.ts" />
/// <reference path="../../../typings/tar.d.ts" />
/// <reference path="../../../typings/remoteBuild.d.ts" />
/// <reference path="../../../typings/tacoRemote.d.ts" />
/// <reference path="../../../typings/tacoUtils.d.ts" />
/// <reference path="../../../typings/adm-zip.d.ts" />
;
var AdmZip = require("adm-zip");
var fs = require("fs");
var fstream = require("fstream");
var path = require("path");
var Q = require("q");
var querystring = require("querystring");
var request = require("request");
var tar = require("tar");
var url = require("url");
var util = require("util");
var zlib = require("zlib");
var ConnectionSecurityHelper = require("./connectionSecurityHelper");
var resources = require("../../resources/resourceManager");
var Settings = require("../utils/settings");
var TacoErrorCodes = require("../tacoErrorCodes");
var errorHelper = require("../tacoErrorHelper");
var tacoUtils = require("taco-utils");
var BuildInfo = tacoUtils.BuildInfo;
var CountStream = tacoUtils.CountStream;
var LogLevel = tacoUtils.LogLevel;
var Logger = tacoUtils.Logger;
var GlobalConfig = tacoUtils.TacoGlobalConfig;
var NewlineNormalizerStream = tacoUtils.NewlineNormalizerStream;
var UtilHelper = tacoUtils.UtilHelper;
var telemetryProperty = tacoUtils.TelemetryHelper.telemetryProperty;
var RemoteBuildClientHelper = (function () {
function RemoteBuildClientHelper() {
}
/**
* Submit a build to a remote build server, poll for completion, print the build log when the build completes, and if building for a device then download the end result
*/
RemoteBuildClientHelper.build = function (settings, telemetryProperties) {
var outputBuildDir = settings.platformConfigurationBldDir;
var buildInfoFilePath = settings.buildInfoFilePath;
if (!RemoteBuildClientHelper.isValidBuildServerUrl(settings.buildServerUrl)) {
throw errorHelper.get(TacoErrorCodes.InvalidRemoteBuildUrl, settings.buildServerUrl);
}
var changeTimeFile = path.join(settings.platformConfigurationBldDir, "lastChangeTime.json");
var lastChangeTime = {};
var buildLog = RemoteBuildClientHelper.buildLogPath(settings);
var promise = RemoteBuildClientHelper.checkForBuildOnServer(settings, buildInfoFilePath)
.then(function (buildInfo) {
settings.incrementalBuild = buildInfo ? buildInfo.buildNumber : null;
Logger.log(resources.getString("IncrementalBuild", !!settings.incrementalBuild));
if (!settings.incrementalBuild) {
try {
fs.unlinkSync(changeTimeFile);
}
catch (e) {
}
}
// Clear build log if it exists
if (fs.existsSync(buildLog)) {
try {
fs.unlinkSync(buildLog);
}
catch (e) {
}
}
var platformJsonFile = path.join(settings.projectSourceDir, "plugins", util.format("remote_%s.json", settings.platform));
if (fs.existsSync(platformJsonFile)) {
fs.unlinkSync(platformJsonFile);
}
})
.then(function () { return RemoteBuildClientHelper.appAsTgzStream(settings, lastChangeTime, changeTimeFile, telemetryProperties); })
.then(function (tgz) {
return RemoteBuildClientHelper.submitBuildRequestToServer(settings, tgz);
})
.then(function (buildingUrl) {
return RemoteBuildClientHelper.pollForBuildComplete(settings, buildingUrl, RemoteBuildClientHelper.PING_INTERVAL, 0);
})
.then(function (result) {
if (result.buildNumber) {
Logger.log(resources.getString("RemoteBuildSuccessful"));
return RemoteBuildClientHelper.logBuildOutput(result, settings);
}
}, function (err) {
if (err.buildInfo) {
// If we successfully submitted a build but the remote build server reports an error about the build, then grab the
// build log from the remote machine before propagating the reported failure
return RemoteBuildClientHelper.logBuildOutput(err.buildInfo, settings)
.then(function (buildInfo) {
throw errorHelper.wrap(TacoErrorCodes.RemoteBuildUnsuccessful, err);
});
}
throw errorHelper.wrap(TacoErrorCodes.RemoteBuildUnsuccessful, err);
})
.then(function (buildInfo) {
return RemoteBuildClientHelper.downloadRemotePluginFile(buildInfo, settings, path.join(settings.projectSourceDir, "plugins"));
})
.then(function (buildInfo) {
UtilHelper.createDirectoryIfNecessary(outputBuildDir);
fs.writeFileSync(buildInfoFilePath, JSON.stringify(buildInfo));
fs.writeFileSync(changeTimeFile, JSON.stringify(lastChangeTime));
return buildInfo;
});
// If build is for a device we will download the build as a zip with the build output in it
if (RemoteBuildClientHelper.isDeviceBuild(settings)) {
promise = promise
.then(function (buildInfo) {
return RemoteBuildClientHelper.downloadBuild(buildInfo, settings, outputBuildDir)
.then(function (zipFile) {
return RemoteBuildClientHelper.unzipBuildFiles(zipFile, outputBuildDir);
}).then(function () {
return buildInfo;
});
});
}
return promise;
};
RemoteBuildClientHelper.run = function (buildInfo, serverSettings) {
var buildUrlBase = Settings.getRemoteServerUrl(serverSettings) + "/build/" + buildInfo.buildNumber;
var httpSettings = { language: buildInfo.buildLang, agent: ConnectionSecurityHelper.getAgent(serverSettings) };
return RemoteBuildClientHelper.httpOptions(buildUrlBase + "/deploy", httpSettings).then(RemoteBuildClientHelper.promiseForHttpGet)
.then(function () {
return RemoteBuildClientHelper.httpOptions(buildUrlBase + "/run", httpSettings).then(RemoteBuildClientHelper.promiseForHttpGet);
}).then(function (responseAndBody) {
return BuildInfo.createNewBuildInfoFromDataObject(JSON.parse(responseAndBody.body));
});
};
RemoteBuildClientHelper.emulate = function (buildInfo, serverSettings, target) {
var buildUrlBase = Settings.getRemoteServerUrl(serverSettings) + "/build/" + buildInfo.buildNumber;
var httpSettings = { language: buildInfo.buildLang, agent: ConnectionSecurityHelper.getAgent(serverSettings) };
return RemoteBuildClientHelper.httpOptions(buildUrlBase + "/emulate?" + querystring.stringify({ target: target }), httpSettings).then(RemoteBuildClientHelper.promiseForHttpGet)
.then(function (responseAndBody) {
return BuildInfo.createNewBuildInfoFromDataObject(JSON.parse(responseAndBody.body));
});
};
RemoteBuildClientHelper.debug = function (buildInfo, serverSettings) {
var buildUrlBase = Settings.getRemoteServerUrl(serverSettings) + "/build/" + buildInfo.buildNumber;
var httpSettings = { language: buildInfo.buildLang, agent: ConnectionSecurityHelper.getAgent(serverSettings) };
return RemoteBuildClientHelper.httpOptions(buildUrlBase + "/debug", httpSettings).then(RemoteBuildClientHelper.promiseForHttpGet).then(function (responseAndBody) {
return BuildInfo.createNewBuildInfoFromDataObject(JSON.parse(responseAndBody.body));
});
};
/**
* Try to find whether the server has a previous build of this project
*/
RemoteBuildClientHelper.checkForBuildOnServer = function (settings, buildInfoFilePath) {
var deferred = Q.defer();
// If we don't have a buildInfo.json file then we cannot query the server.
if (!fs.existsSync(buildInfoFilePath)) {
deferred.resolve(null);
return deferred.promise;
}
var buildInfo;
try {
var fileContents = fs.readFileSync(buildInfoFilePath).toString();
buildInfo = JSON.parse(fileContents);
}
catch (e) {
deferred.resolve(null);
return deferred.promise;
}
var buildNumber = buildInfo.buildNumber;
var serverUrl = settings.buildServerUrl;
var buildUrl = serverUrl + "/build/" + buildNumber;
// If we do have a buildInfo.json, check whether the server still has that build number around
return RemoteBuildClientHelper.httpOptions(buildUrl, settings).then(function (requestOptions) {
request.get((requestOptions), function (error, response, body) {
if (error) {
deferred.reject(RemoteBuildClientHelper.errorFromRemoteBuildServer(serverUrl, error, TacoErrorCodes.RemoteBuildError));
}
else if (response.statusCode === 200) {
deferred.resolve(buildInfo);
}
else {
// Build was bad: remove outdated buildInfo file
try {
fs.unlinkSync(buildInfoFilePath);
}
catch (e) {
}
deferred.resolve(null);
}
});
return deferred.promise;
});
};
RemoteBuildClientHelper.isValidBuildServerUrl = function (serverUrl) {
return (serverUrl.indexOf("http://") === 0 || serverUrl.indexOf("https://") === 0);
};
RemoteBuildClientHelper.httpOptions = function (url, settings) {
return settings.agent.then(function (agent) {
// TODO: Remove the casting once we've get some complete/up-to-date .d.ts files. See https://github.com/Microsoft/TACO/issues/18
return {
url: url,
headers: { "Accept-Language": settings.language },
agent: agent
};
}).finally(function () {
// Add a slight delay to work around node race condition to do with certificates.
// See https://github.com/nodejs/io.js/issues/712 for more discussion
return Q.delay(1);
});
};
/**
* Convert errors from error codes to localizable strings
*/
RemoteBuildClientHelper.errorFromRemoteBuildServer = function (serverUrl, requestError, fallbackErrorCode) {
if (!requestError.code) {
return errorHelper.wrap(fallbackErrorCode, requestError, serverUrl);
}
else if (requestError.code.indexOf("CERT_") !== -1) {
return errorHelper.get(TacoErrorCodes.InvalidRemoteBuildClientCert);
}
else if (serverUrl.indexOf("https://") === 0 && requestError.code === "ECONNRESET") {
return errorHelper.get(TacoErrorCodes.RemoteBuildSslConnectionReset, serverUrl);
}
else if (serverUrl.indexOf("http://") === 0 && requestError.code === "ECONNRESET") {
return errorHelper.get(TacoErrorCodes.RemoteBuildNonSslConnectionReset, serverUrl);
}
else if (requestError.code === "ENOTFOUND") {
// Host unreachable regardless of whether http or https
return errorHelper.get(TacoErrorCodes.RemoteBuildHostNotFound, serverUrl);
}
else if (requestError.code === "ECONNREFUSED") {
// Host reachable but connection not established (e.g. Server not running)
return errorHelper.get(TacoErrorCodes.RemoteBuildNoConnection, serverUrl);
}
return errorHelper.wrap(fallbackErrorCode, requestError, serverUrl);
};
/**
* Create a gzipped tarball of the cordova project ready to submit to the server.
*/
RemoteBuildClientHelper.appAsTgzStream = function (settings, lastChangeTime, changeTimeFile, telemetryProperties) {
var platform = settings.platform;
var projectSourceDir = settings.projectSourceDir;
var changeListFile = path.join(projectSourceDir, "changeList.json");
var newChangeTime = {};
var isIncremental = false;
try {
var json = JSON.parse(fs.readFileSync(changeTimeFile, "utf8"));
Object.keys(json).forEach(function (file) {
lastChangeTime[file] = json[file];
});
isIncremental = true;
}
catch (e) {
}
telemetryProperties["remoteBuild." + platform + ".wasIncremental"] = telemetryProperty(isIncremental, /*isPii*/ false);
var upToDateFiles = [];
var filterForChanges = function (reader, props) {
var shouldInclude = RemoteBuildClientHelper.filterForRemote(settings, reader, lastChangeTime);
var appRelPath = path.relative(settings.projectSourceDir, reader.path);
if (shouldInclude) {
var newmtime = reader.props.mtime.getTime();
if (reader.props.type === "Directory" || !lastChangeTime[appRelPath] || lastChangeTime[appRelPath] !== newmtime) {
// If this is a directory, or a new file, or a file that has changed since we last looked, then include it
newChangeTime[appRelPath] = newmtime;
return true;
}
}
// Either we did not want to include the file, or it has not changed
if (appRelPath in lastChangeTime) {
// It is a file that has not changed. Remember that, and do not include it.
upToDateFiles.push(appRelPath);
}
return false;
};
var property = telemetryProperty(0, /*isPii*/ false);
telemetryProperties["remoteBuild." + platform + ".filesChangedCount"] = property;
var filterForTar = function (reader, props) {
var appRelPath = path.relative(settings.projectSourceDir, reader.path);
var wasModifiedRecently = appRelPath === "changeList.json" || appRelPath in newChangeTime;
if (wasModifiedRecently && reader.props.type !== "Directory") {
property.value++; // We found another file that was modified
}
return wasModifiedRecently;
};
var deferred = Q.defer();
// TODO: Remove the casting once we've get some complete/up-to-date .d.ts files. See https://github.com/Microsoft/TACO/issues/18
var firstPassReader = new fstream.Reader({ path: projectSourceDir, type: "Directory", filter: filterForChanges });
firstPassReader.on("close", function () {
// We have now determined which files are new and which files are old. Construct changeList.json
var previousFiles = Object.keys(lastChangeTime);
var changeList = {
deletedFiles: previousFiles.filter(function (file) {
return !(file in newChangeTime) && upToDateFiles.indexOf(file) === -1;
})
};
// Save the changeList.json file
fs.writeFileSync(changeListFile, JSON.stringify(changeList));
// Save the new modification times
changeList.deletedFiles.forEach(function (file) {
// Stop tracking deleted files
delete lastChangeTime[file];
});
Object.keys(newChangeTime).forEach(function (file) {
// Update the modification time of changed and new files
lastChangeTime[file] = newChangeTime[file];
});
deferred.resolve({});
});
firstPassReader.on("error", function (err) {
deferred.reject(errorHelper.wrap(TacoErrorCodes.ErrorPatchCreation, err));
});
return deferred.promise.then(function () {
// TODO: Remove the casting once we've get some complete/up-to-date .d.ts files. See https://github.com/Microsoft/TACO/issues/18
var projectSourceDirReader = new fstream.Reader({ path: projectSourceDir, type: "Directory", filter: filterForTar });
var tarProducingStream = CountStream.count(projectSourceDirReader.pipe(tar.Pack()), function (sz) { return telemetryProperties["remotebuild." + platform + ".projectSizeInBytes"] = telemetryProperty(sz, /*isPii*/ false); });
var tgzProducingStream = CountStream.count(tarProducingStream.pipe(zlib.createGzip()), function (sz) { return telemetryProperties["remotebuild." + platform + ".gzipedProjectSizeInBytes"] = telemetryProperty(sz, /*isPii*/ false); });
return tgzProducingStream;
});
};
/**
* Determine which files should be included in the tarball which is sent to the server.
* We want to include all changed user data, and exclude all irrelevant metadata or data for different platforms
*/
RemoteBuildClientHelper.filterForRemote = function (settings, reader, lastChangeTime) {
var appRelPath = path.relative(settings.projectSourceDir, reader.path);
var exclusions = [
"remote",
"platforms" // The /platforms folder is for locally installed platforms, and thus irrelevant to remote builds
];
if (exclusions.indexOf(appRelPath) !== -1) {
return false;
}
// We want to exclude /merges/<x> if x is not the current platform
// Similarly for the others here
var otherPlatformExclusions = [
"merges",
path.join("res", "screens"),
path.join("res", "icons"),
path.join("res", "cert"),
path.join("res", "native")
];
var shouldBeExcluded = function (exclusion) {
// If we are looking at <exclusion>\<basename> and basename is not the platform, then it should be excluded
var checkFullPath = path.join(exclusion, reader.basename);
return reader.basename !== settings.platform && !!appRelPath.match(new RegExp("^" + checkFullPath + "$"));
};
if (appRelPath && otherPlatformExclusions.some(shouldBeExcluded)) {
return false;
}
if (settings.incrementalBuild && appRelPath) {
var stat = fs.statSync(reader.path);
if (stat.isDirectory()) {
// Consider all directories, since their contents may be modified
return true;
}
if (appRelPath === "changeList.json") {
// Always include changeList.json
return true;
}
if (appRelPath in lastChangeTime && lastChangeTime[appRelPath] === stat.mtime.getTime()) {
// If we know when the file was last modified, and it hasn't been changed since then, we must have sent this file to the server already.
return false;
}
}
return true;
};
/*
* Submit a new build request to the remote server, including the project to be compiled as a tarball attached to the POST
*/
RemoteBuildClientHelper.submitBuildRequestToServer = function (settings, appAsTgzStream) {
var serverUrl = settings.buildServerUrl;
var vcordova = settings.cordovaVersion;
var cfg = settings.configuration ? settings.configuration.toLowerCase() : "release";
var cliVersion = require("../../package.json").version;
var deferred = Q.defer();
var params = {
command: "build",
vcordova: vcordova,
vcli: cliVersion,
cfg: cfg,
platform: settings.platform
};
var buildOptions = [];
if (RemoteBuildClientHelper.isDeviceBuild(settings)) {
buildOptions.push("--device");
}
if (settings.options) {
buildOptions.concat(settings.options);
}
if (buildOptions.length > 0) {
params["options"] = buildOptions.join(" ");
}
if (settings.incrementalBuild) {
params["buildNumber"] = settings.incrementalBuild.toString();
}
var buildUrl = serverUrl + "/build/tasks?" + querystring.stringify(params);
Logger.log(resources.getString("SubmittingRemoteBuild", buildUrl));
appAsTgzStream.on("error", function (error) {
deferred.reject(errorHelper.wrap(TacoErrorCodes.ErrorUploadingRemoteBuild, error, serverUrl));
});
return RemoteBuildClientHelper.httpOptions(buildUrl, settings).then(function (requestOptions) {
appAsTgzStream.pipe(request.post(requestOptions, function (error, response, body) {
if (error) {
deferred.reject(RemoteBuildClientHelper.errorFromRemoteBuildServer(serverUrl, error, TacoErrorCodes.ErrorUploadingRemoteBuild));
}
else if (response.statusCode === 400) {
// Build server sends back http 400 for invalid submissions with response like this. We will fail the build with a formatted message.
// {"status": "Invalid build submission", "errors": ["The requested cordova version 3.5.0-0.2.4 is not supported by this build manager. Installed cordova version is 3.4.1-0.1.0"]}
var errorsJson = JSON.parse(body);
deferred.reject(errorHelper.get(TacoErrorCodes.InvalidBuildSubmission400, errorsJson.status, errorsJson.errors.toString()));
}
else if (response.statusCode === 202) {
// Expect http 202 for a valid submission which is "Accepted" with a content-location to the Url to check for build status
if (GlobalConfig.logLevel === LogLevel.Diagnostic) {
Logger.log(resources.getString("NewRemoteBuildInfo", body));
}
// The server responds with a content-location header indicating where the newly submitted build is now located
// However the URL in that header may have a different host to what we expect, especially in the case of proxies
// or ipv6 addresses. To fix this, we'll ignore the host part of the URL and replace it with the host we want to
// communicate with ourselves.
var reportedBuildUrl = url.parse(response.headers["content-location"]);
var buildServerUrl = url.parse(buildUrl);
reportedBuildUrl.host = buildServerUrl.host;
deferred.resolve(url.format(reportedBuildUrl));
}
else {
deferred.reject(errorHelper.get(TacoErrorCodes.ErrorDuringRemoteBuildSubmission, body));
}
}));
return deferred.promise;
});
};
RemoteBuildClientHelper.isDeviceBuild = function (settings) {
return settings.buildTarget && (settings.buildTarget.toLowerCase() === "device");
};
/*
* Status progression: uploaded -> extracted -> building -> [complete|invalid|error] -> downloaded [if a device targeted build]
*/
RemoteBuildClientHelper.pollForBuildComplete = function (settings, buildingUrl, interval, attempts, logOffset) {
var thisAttempt = attempts + 1;
Logger.log(resources.getString("CheckingRemoteBuildStatus", (new Date()).toLocaleTimeString(), buildingUrl, thisAttempt));
return RemoteBuildClientHelper.httpOptions(buildingUrl, settings).then(RemoteBuildClientHelper.promiseForHttpGet)
.then(function (responseAndBody) {
if (responseAndBody.response.statusCode !== 200) {
throw errorHelper.get(TacoErrorCodes.RemoteBuildStatusPollFailed, responseAndBody.response.statusCode, responseAndBody.body);
}
var buildInfo = BuildInfo.createNewBuildInfoFromDataObject(JSON.parse(responseAndBody.body));
Logger.log(buildInfo.status + " - " + buildInfo.message);
buildInfo["logOffset"] = logOffset || 0;
if (buildInfo.status === BuildInfo.COMPLETE) {
return Q(buildInfo);
}
else if (buildInfo.status === BuildInfo.INVALID) {
throw errorHelper.get(TacoErrorCodes.InvalidRemoteBuild, buildInfo.message);
}
else if (buildInfo.status === BuildInfo.ERROR) {
var err = errorHelper.get(TacoErrorCodes.RemoteBuildError, buildInfo.message);
err.buildInfo = buildInfo;
throw err;
}
return RemoteBuildClientHelper.logBuildOutput(buildInfo, settings)
.then(function (loggedBuildInfo) {
return Q.all([Q.delay(interval), RemoteBuildClientHelper.checkQueuePosition(settings, loggedBuildInfo)])
.then(function () {
return RemoteBuildClientHelper.pollForBuildComplete(settings, buildingUrl, interval, thisAttempt, loggedBuildInfo["logOffset"]);
});
});
});
};
RemoteBuildClientHelper.checkQueuePosition = function (settings, buildInfo) {
if (buildInfo.status === BuildInfo.BUILDING) {
return Q({});
}
var queueUrl = settings.buildServerUrl + "/build/tasks";
return RemoteBuildClientHelper.httpOptions(queueUrl, settings).then(RemoteBuildClientHelper.promiseForHttpGet)
.then(function (responseAndBody) {
try {
var serverInfo = JSON.parse(responseAndBody.body);
var queueIndex = serverInfo.queuedBuilds.map(function (bi) { return bi.buildNumber === buildInfo.buildNumber; }).indexOf(true);
if (queueIndex >= 0) {
Logger.log(resources.getString("RemoteBuildQueued", queueIndex + 1));
}
}
catch (e) {
Logger.log(e);
}
return Q({});
});
};
/*
* Download the finished log of a build, regardless of whether it succeeded or failed
*/
RemoteBuildClientHelper.logBuildOutput = function (buildInfo, settings) {
var serverUrl = settings.buildServerUrl;
var deferred = Q.defer();
var offset = buildInfo["logOffset"] || 0;
var logFlags = offset > 0 ? "r+" : "w";
var buildNumber = buildInfo.buildNumber;
var downloadUrl = util.format("%s/build/tasks/%d/log?offset=%d", serverUrl, buildNumber, offset);
return RemoteBuildClientHelper.httpOptions(downloadUrl, settings).then(request).then(function (req) {
var logPath = RemoteBuildClientHelper.buildLogPath(settings);
UtilHelper.createDirectoryIfNecessary(settings.platformConfigurationBldDir);
var endOfFile = 0;
if (offset > 0 && fs.existsSync(logPath)) {
var logFileStat = fs.statSync(logPath);
endOfFile = logFileStat.size;
}
// TODO: Remove the casting once we've get some complete/up-to-date .d.ts files. See https://github.com/Microsoft/TACO/issues/18
var logStream = fs.createWriteStream(logPath, { start: endOfFile, flags: logFlags });
var countStream = new CountStream();
var newlineNormalizerStream = new NewlineNormalizerStream();
logStream.on("finish", function () {
Logger.log(resources.getString("BuildLogWrittenTo", logPath));
deferred.resolve(buildInfo);
});
req.on("end", function () {
buildInfo["logOffset"] = offset + countStream.count;
}).pipe(countStream).pipe(newlineNormalizerStream);
// Output to build log
newlineNormalizerStream.pipe(logStream);
// Output to stdout
newlineNormalizerStream.pipe(process.stdout);
return deferred.promise;
});
};
/*
* Download the <platform>.json file that tracks the installed plugins on the remote machine.
* This file is used by vs-mda/vs-tac, but it is also good for checking what plugins are actually installed remotely.
*/
RemoteBuildClientHelper.downloadRemotePluginFile = function (buildInfo, settings, toDir) {
var serverUrl = settings.buildServerUrl;
var deferred = Q.defer();
var buildNumber = buildInfo.buildNumber;
var downloadUrl = util.format("%s/files/%d/cordovaApp/plugins/%s.json", serverUrl, buildNumber, settings.platform);
UtilHelper.createDirectoryIfNecessary(toDir);
var remotePluginStream = fs.createWriteStream(path.join(toDir, util.format("remote_%s.json", settings.platform)));
remotePluginStream.on("finish", function () {
deferred.resolve(buildInfo);
});
return RemoteBuildClientHelper.httpOptions(downloadUrl, settings).then(request).invoke("pipe", remotePluginStream).then(function () {
return deferred.promise;
});
};
/*
* Download a completed build from the remote server as a zip
*/
RemoteBuildClientHelper.downloadBuild = function (buildInfo, settings, toDir) {
var serverUrl = settings.buildServerUrl;
UtilHelper.createDirectoryIfNecessary(toDir);
var deferred = Q.defer();
var buildNumber = buildInfo.buildNumber;
var downloadUrl = serverUrl + "/build/" + buildNumber + "/download";
Logger.log(resources.getString("DownloadingRemoteBuild", downloadUrl, toDir));
var zipFile = path.join(toDir, buildNumber + ".zip");
var outZip = fs.createWriteStream(zipFile);
outZip.on("error", function (error) {
deferred.reject(errorHelper.wrap(TacoErrorCodes.ErrorDownloadingRemoteBuild, error, toDir));
});
outZip.on("finish", function () {
Logger.log(resources.getString("DownloadedRemoteBuild", toDir));
deferred.resolve(zipFile);
});
return RemoteBuildClientHelper.httpOptions(downloadUrl, settings).then(request).invoke("pipe", outZip).then(function () {
return deferred.promise;
});
};
/*
* Unzip the downloaded build
*/
RemoteBuildClientHelper.unzipBuildFiles = function (zipFile, toDir) {
Logger.log(resources.getString("ExtractingRemoteBuild", toDir));
UtilHelper.createDirectoryIfNecessary(toDir);
var deferred = Q.defer();
try {
var zip = new AdmZip(zipFile);
zip.extractAllTo(toDir, true);
Logger.log(resources.getString("DoneExtractingRemoteBuild", toDir));
fs.unlink(zipFile, function (err) {
if (err) {
Logger.log(resources.getString("FailedToDeleteRemoteZip", zipFile));
}
deferred.resolve({});
});
}
catch (error) {
deferred.reject(errorHelper.wrap(TacoErrorCodes.ErrorDownloadingRemoteBuild, error, toDir));
}
return deferred.promise;
};
/*
* perform a HTTP GET request and return a promise which is resolved with the response or rejected with an error
*/
RemoteBuildClientHelper.promiseForHttpGet = function (urlOptions) {
var deferred = Q.defer();
request.get(urlOptions, function (error, response, body) {
if (error) {
deferred.reject(errorHelper.wrap(TacoErrorCodes.ErrorHttpGet, error, urlOptions.url));
}
else {
if (response.statusCode !== 200 && response.statusCode !== 202) {
// see if the response is JSON with a message
try {
var bodyJson = JSON.parse(response.body);
if (bodyJson.message) {
deferred.reject(errorHelper.get(TacoErrorCodes.HttpGetFailed, response.statusCode, bodyJson.message));
return;
}
}
catch (e) {
}
deferred.reject(errorHelper.get(TacoErrorCodes.HttpGetFailed, response.statusCode, response.body));
}
deferred.resolve({ response: response, body: body });
}
});
return deferred.promise;
};
/**
* Returns the path to the local build log.
*/
RemoteBuildClientHelper.buildLogPath = function (settings) {
return path.join(settings.platformConfigurationBldDir, "build.log");
};
RemoteBuildClientHelper.PING_INTERVAL = 5000;
return RemoteBuildClientHelper;
}());
module.exports = RemoteBuildClientHelper;
//# sourceMappingURL=remoteBuildClientHelper.js.map