diff --git a/README.md b/README.md index 6be59b7..d56af67 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,15 @@ This is the initial public release. For best results you should also install th ### Setup Instructions #### Build - Streaming Analytics Credentials -The build-ibmstreams package requires a running IBM Streaming Analytics service. SPL applications will be built and deployed on this service. If you need to create one, start here and follow the instructions to create an account. +The build-ibmstreams package requires a running IBM Streaming Analytics service. SPL applications will be built and deployed on this service. If you need to create one, start here and follow the instructions to create an account. Note:The service needs to support V2 of the rest api. -Once you have an account go to your dashboard and select the Streaming Analytic service you want to use. You need to make sure it is running and then copy your credentials to the clipboard. To get your credentials select Service Credentials from the actions on the left. From the credentials page, press View credentials for the one you want to use and press the copy button in the upper right side of the credentials to copy them to the clipboard. +Once you have an account go to your dashboard and select the Streaming Analytics service you want to use. You need to make sure it is running and then copy your credentials to the clipboard. To get your credentials select Service Credentials from the actions on the left. From the credentials page, press View credentials for the one you want to use and press the copy button in the upper right side of the credentials to copy them to the clipboard. In Atom there is a setting in the build-ibmstreams package for the credentials. Go to Atom->Preferences->Packages and press the Settings button on the build-ibmstreams package and paste your credentials into the setting. ![](./images/atomcredssetting.png) ### SPL Application build -![](./images/build.gif) \ No newline at end of file +![](./images/build.gif) diff --git a/lib/LintHandler.js b/lib/LintHandler.js index 38d2351..cb15e3f 100644 --- a/lib/LintHandler.js +++ b/lib/LintHandler.js @@ -57,7 +57,10 @@ export class LintHandler { location: { file: absolutePath, - position: [[parts[2]-1,parts[3]-1],[parts[2]-1,parts[3]]], // 0-indexed + position: [ + [parseInt(parts[2])-1 ,parseInt(parts[3])-1], + [parseInt(parts[2])-1,parseInt(parts[3])] + ], // 0-indexed }, excerpt: parts[4], description: parts[5], @@ -67,6 +70,9 @@ export class LintHandler { this.linter.setAllMessages(convertedMessages); + if (Array.isArray(convertedMessages) && convertedMessages.length > 0) { + atom.workspace.open("atom://nuclide/diagnostics"); + } } } } diff --git a/lib/MessageHandler.js b/lib/MessageHandler.js index 39445ba..9e270ac 100644 --- a/lib/MessageHandler.js +++ b/lib/MessageHandler.js @@ -3,11 +3,6 @@ "use strict"; "use babel"; -import path from "path"; - -const packageRoot = atom.packages.resolvePackagePath("build-ibmstreams"); -const STREAMING_ANALYTICS_ICON_PATH = `${packageRoot}${path.sep}assets${path.sep}streaming_analytics_200x200.png`; - export class MessageHandler { consoleService: null; @@ -15,154 +10,144 @@ export class MessageHandler { this.consoleService = service; } - handleBuildProgressMessage(messageOutput: Array | string, showNotification?: boolean) { - if (Array.isArray(messageOutput)) { - const message = this.getLoggableMessage(messageOutput); - if (message) { - this.consoleService.log(message); - if (showNotification) { - atom.notifications.addInfo("building...", {detail: message}); - } - } - } else if (typeof(messageOutput) === "string") { - this.consoleService.log(messageOutput); - if (showNotification) { - atom.notifications.addInfo(messageOutput, {}); - } + handleInfo( + message, + { + detail = null, + description = null, + showNotification = true, + showConsoleMessage = true, + notificationAutoDismiss = true, + notificationButtons = [] + } = {} + ) { + const addedButtons = this.processButtons(notificationButtons); + const detailMessage = this.joinMessageArray(detail); + + if (showConsoleMessage) { + this.consoleService.log(`${message}${detailMessage ? "\n"+detailMessage: ""}`); } - } - - handleBuildSuccess(messageOutput: Array) { - const message = this.getLoggableMessage(messageOutput); - if (message) { - this.consoleService.success(message); - atom.notifications.addSuccess("Build succeeded", {detail: message, dismissable: true}); - } else { - this.consoleService.success("Build succeeded"); - atom.notifications.addSuccess("Build succeeded", {dismissable: true}); + if (showNotification && typeof(message) === "string") { + const notificationOptions = { + ...addedButtons, + dismissable: !notificationAutoDismiss, + detail: detailMessage ? detailMessage : "", + description: description ? description : "" + }; + return atom.notifications.addInfo(message, notificationOptions); } } - handleBuildFailure(messageOutput: Array) { - const message = this.getLoggableMessage(messageOutput); - if (message) { + handleError( + message, + { + detail, + description, + stack, + showNotification = true, + showConsoleMessage = true, + consoleErrorLog = true, + notificationAutoDismiss = false, + notificationButtons = [] + } = {} + ) { + const addedButtons = this.processButtons(notificationButtons); + const detailMessage = this.joinMessageArray(detail); + const stackMessage = this.joinMessageArray(stack); + + if (consoleErrorLog) { + if (stack) { + console.error(message, stack); + } else { + console.error(message); + } + } + if (showConsoleMessage) { this.consoleService.error(message); - atom.notifications.addError("Build failed", {detail: message, dismissable: true}); - } else { - this.consoleService.error("Build failed"); - atom.notifications.addError("Build failed", {dismissable: true}); + if (typeof(detailMessage) === "string" && detailMessage.length > 0) { + this.consoleService.error(detailMessage); + } } - } - - handleSubmitProgressMessage(input) { - if (typeof(input) === "string") { - atom.notifications.addInfo(input, {}); + if (showNotification && typeof(message) === "string") { + const notificationOptions = { + ...addedButtons, + dismissable: !notificationAutoDismiss, + detail: detailMessage ? detailMessage : "", + stack: stackMessage ? stackMessage: "", + description: description ? description : "" + }; + return atom.notifications.addError(message, notificationOptions); } - this.consoleService.log(input); } - handleSubmitSuccess(input, notificationButtons = []) { - let addedButtons = {}; - if (Array.isArray(notificationButtons)) { - addedButtons.buttons = notificationButtons.map(obj => ({onDidClick: obj.callbackFn, text: obj.label})); + handleSuccess( + message, + { + detail = null, + description = null, + showNotification = true, + showConsoleMessage = true, + notificationAutoDismiss = false, + notificationButtons = [] + } = {} + ) { + const addedButtons = this.processButtons(notificationButtons); + const detailMessage = this.joinMessageArray(detail); + + if (showConsoleMessage) { + this.consoleService.log(`${message}${detailMessage ? "\n"+detailMessage: ""}`); } - atom.notifications.addSuccess(`Job ${input.name} is ${input.health}`, {...addedButtons, dismissable: true}); - - if (this.consoleService) { - this.consoleService.success(`Job ${input.name} is ${input.health}`); + if (showNotification && typeof(message) === "string") { + const notificationOptions = { + ...addedButtons, + dismissable: !notificationAutoDismiss, + detail: detailMessage ? detailMessage : "", + description: description ? description : "" + }; + return atom.notifications.addSuccess(message, notificationOptions); } } - handleSubmitFailure(input) { - const errorString = input.errors.map(err => err.message).join("\n"); - atom.notifications.addError(`Job submission failed`, {detail: errorString, dismissable: true}); - - if (this.consoleService) { - this.consoleService.error(`Job submission failed\n${errorString}`); - } - } - handleError(input, notificationButtons = []) { - let addedButtons = {}; - if (Array.isArray(notificationButtons)) { - addedButtons.buttons = notificationButtons.map(obj => ({onDidClick: obj.callbackFn, text: obj.label})); - } - if (typeof(input) === "string") { - atom.notifications.addError(input, {...addedButtons, dismissable: true}); - this.consoleService.error(input); - } else if (input.message) { - atom.notifications.addError( - input.message, - {...addedButtons, dismissable: true, detail: input.stack, stack: input.stack} - ); - this.consoleService.error(input.message); - } - console.error(input); - } - handleSuccess(input, detail, showNotification, showConsoleMsg, notificationButtons = []) { - let addedButtons = {}; - if (Array.isArray(notificationButtons)) { - addedButtons.buttons = notificationButtons.map(obj => ({onDidClick: obj.callbackFn, text: obj.label})); - } - if (showNotification) { - atom.notifications.addSuccess(input, {...addedButtons, detail: detail, dismissable: true}); - } - if (showConsoleMsg) { - if (this.consoleService) { - this.consoleService.success(`${input}\n${detail}`); - } - } - } - - handleWarning(message) { - if (message && typeof(message) === "string") { - this.consoleService.warn(message); - atom.notifications.addWarning(message, {}); - } - } - - showDialog(message, detail, buttonObjs) { - - const nativeImage = require("electron").nativeImage; - - const labels = buttonObjs.map(obj => obj.label); - const callbacks = buttonObjs.map(obj => obj.callbackFn); - let buttons = {}; - labels.forEach((label, index) => { - buttons[label] = callbacks[index]; - }); - atom.confirm( - { - message: message, - detail: detail, - buttons: labels, - icon: STREAMING_ANALYTICS_ICON_PATH - }, - (chosen, checkboxChecked) => { - const callback = callbacks[chosen]; - if (typeof(callback) === "function") { - return callback(); - } - } - ); - } - - handleCredentialsMissing() { - atom.notifications.addError( + handleCredentialsMissing(errorNotification) { + const n = atom.notifications.addError( "Copy and paste the Streaming Analytics service credentials into the build-ibmstreams package settings page.", { dismissable: true, buttons: [{ text: "Open package settings", - onDidClick: () => {atom.workspace.open("atom://config/packages/build-ibmstreams")} + onDidClick: () => { + this.dismissNotification(errorNotification); + this.dismissNotification(n); + atom.workspace.open("atom://config/packages/build-ibmstreams"); + } }] } ); + return n; + } + + processButtons(btns) { + let buttons = {}; + if (Array.isArray(btns)) { + buttons.buttons = btns.map(obj => ({onDidClick: obj.callbackFn, text: obj.label})); + } + return buttons; + } + + joinMessageArray(msgArray) { + if (Array.isArray(msgArray)) { + return msgArray.join("\n").trimRight(); + } + return msgArray; + } + + dismissNotification(notification) { + if (notification && typeof(notification.dismiss) === "function") { + notification.dismiss(); + } } getLoggableMessage(messages: Array) { - return messages - .map(outputMsg => outputMsg.message_text) - .join("\n") - .trimRight(); + return this.joinMessageArray(messages.map(outputMsg => outputMsg.message_text)); } } diff --git a/lib/spl-build-common.js b/lib/spl-build-common.js index 7f90ed7..69ebcd8 100644 --- a/lib/spl-build-common.js +++ b/lib/spl-build-common.js @@ -7,8 +7,8 @@ import * as path from "path"; import * as fs from "fs"; import * as _ from "underscore"; -import { Observable, of, empty, forkJoin } from "rxjs"; -import { switchMap, map, expand, filter, tap, debounceTime, mergeMap } from "rxjs/operators"; +import { Observable, of, empty, forkJoin, interval } from "rxjs"; +import { switchMap, map, expand, filter, tap, debounceTime, mergeMap, takeUntil } from "rxjs/operators"; import * as ncp from "copy-paste"; const request = require("request"); @@ -19,6 +19,7 @@ const defaultIgnoreFiles = [ ".project", ".classpath", "toolkit.xml", + ".build*zip", "___bundle.zip" ]; @@ -28,12 +29,18 @@ const defaultIgnoreDirectories = [ "samples", "opt/client", ".settings", + ".apt_generated", + ".build*", "___bundle" ]; +const buildConsoleUrl = (url, instanceId) => `${url}#application/dashboard/Application%20Dashboard?instance=${instanceId}`; + +const ibmCloudDashboardUrl = "https://cloud.ibm.com/resources"; + export class SplBuilder { static BUILD_ACTION = {DOWNLOAD: 0, SUBMIT: 1}; - static SPL_MSG_REGEX = /^([\w.]+\/[\w.]+)\:(\d+)\:(\d+)\:\s+(\w{5}\d{4}[IWE])\s+((ERROR|WARN|INFO)\:.*)$/; + static SPL_MSG_REGEX = /^([\w.]+(?:\/[\w.]+)?)\:(\d+)\:(\d+)\:\s+(\w{5}\d{4}[IWE])\s+((ERROR|WARN|INFO)\:.*)$/; static SPL_NAMESPACE_REGEX = /^\s*(?:\bnamespace\b)\s+([a-z|A-Z|0-9|\.|\_]+)\s*\;/gm; static SPL_MAIN_COMPOSITE_REGEX = /.*?(?:\bcomposite\b)(?:\s*|\/\/.*?|\/\*.*?\*\/)+([a-z|A-Z|0-9|\.|\_]+)(?:\s*|\/\/.*?|\/\*.*?\*\/)*\{/gm; static STATUS_POLL_FREQUENCY = 5000; @@ -44,18 +51,16 @@ export class SplBuilder { openUrlHandler = null; serviceCredentials = null; accessToken = null; + originatorString = null; - constructor(messageHandler, lintHandler, openUrlHandler) { + constructor(messageHandler, lintHandler, openUrlHandler, originator) { this.messageHandler = messageHandler; this.lintHandler = lintHandler; this.openUrlHandler = openUrlHandler; + this.originatorString = originator ? `${originator.originator}-${originator.version}:${originator.type}` : ""; } dispose() { - messageHandler = null; - lintHandler = null; - serviceCredentials = null; - accessToken = null; } /** @@ -69,15 +74,26 @@ export class SplBuilder { async buildSourceArchive(appRoot: string, toolkitRootPath: string, options: {useMakefile: boolean, makefilePath: string, fqn: string} = {useMakefile: false}) { const archiver = require("archiver"); + this.useMakefile = options.useMakefile; + if (options.makefilePath) { + this.makefilePath = options.makefilePath; + } + if (options.fqn) { + this.fqn = options.fqn; + } + const appRootContents = fs.readdirSync(appRoot); const makefilesFound = appRootContents.filter(entry => typeof(entry) === "string" && entry.toLowerCase() === "makefile"); const buildTarget = options.useMakefile ? " with makefile" : ` for ${options.fqn}`; - this.messageHandler.handleBuildProgressMessage(`Building application archive${buildTarget}...`, true); + this.messageHandler.handleInfo(`Building application archive${buildTarget}...`); - const outputFilePath = `${appRoot}${path.sep}___bundle.zip`; + // temporary build archive filename is of format + // .build_[fqn]_[time].zip or .build_make_[parent_dir]_[time].zip for makefile build + // eg: .build_sample.Vwap_1547066810853.zip , .build_make_Vwap_1547066810853.zip + const outputFilePath = `${appRoot}${path.sep}.build_${options.useMakefile ? "make_"+appRoot.split(path.sep).pop() : options.fqn.replace("::",".")}_${Date.now()}.zip`; - // delete existing ___bundle.zip file before creating new one + // delete existing build archive file before creating new one // TODO: handle if file is open better (windows file locks) try { if (fs.existsSync(outputFilePath)) { @@ -88,17 +104,14 @@ export class SplBuilder { const archive = archiver("zip", { zlib: { level: 9} // compression level }); - const self = this; - output.on("close", function() { + //const self = this; + output.on("close", () => { console.log("Application source archive built"); - self.messageHandler.handleBuildProgressMessage("Application archive created, submitting to build service...", true); + this.messageHandler.handleInfo("Application archive created, submitting to build service..."); }); - // TODO: handle warnings/errors instead of throwing? archive.on("warning", function(err) { if (err.code === "ENOENT") { - // log warning } else { - // throw error throw err; } }); @@ -111,7 +124,7 @@ export class SplBuilder { const toolkitPaths = SplBuilder.getToolkits(toolkitRootPath); let tkPathString = ""; - if (toolkitPaths) { + if (Array.isArray(toolkitPaths) && toolkitPaths.length > 0) { const rootContents = fs.readdirSync(appRoot); const newRoot = path.basename(appRoot); let ignoreFiles = defaultIgnoreFiles; @@ -124,13 +137,27 @@ export class SplBuilder { // Add files rootContents .filter(item => fs.lstatSync(`${appRoot}/${item}`).isFile()) - .filter(item => !_.some(ignoreFiles, name => item.includes(name))) + .filter(item => !_.some(ignoreFiles, name => { + if (name.includes("*")) { + const regex = new RegExp(name.replace(".","\.").replace("*",".*")); + return regex.test(item); + } else { + return item.includes(name); + } + })) .forEach(item => archive.append(fs.readFileSync(`${appRoot}/${item}`), { name: `${newRoot}/${item}` })); // Add directories rootContents .filter(item => fs.lstatSync(`${appRoot}/${item}`).isDirectory()) - .filter(item => !_.some(ignoreDirs, name => item.endsWith(name))) + .filter(item => !_.some(ignoreDirs, name => { + if (name.includes("*")) { + const regex = new RegExp(name.replace(".","\.").replace("*",".*")); + return regex.test(item); + } else { + return item.includes(name); + } + })) .forEach(item => archive.directory(`${appRoot}/${item}`, `${newRoot}/${item}`)); toolkitPaths.forEach(tk => archive.directory(tk.tkPath, `toolkits/${tk.tk}`)); @@ -160,7 +187,7 @@ export class SplBuilder { const archiveStream = await archive.finalize(); } catch (err) { - this.messageHandler.handleError(err); + this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack, consoleErrorLog: false}); return Promise.reject(err); } @@ -178,8 +205,8 @@ export class SplBuilder { this.buildAndSubmitJob(input); } } else { - this.messageHandler.handleError("Unable to determine Streaming Analytics service credentials."); - this.messageHandler.handleCredentialsMissing(); + const errorNotification = this.messageHandler.handleError("Unable to determine Streaming Analytics service credentials."); + this.messageHandler.handleCredentialsMissing(errorNotification); throw new Error("Error parsing VCAP_SERVICES environment variable"); } } @@ -199,11 +226,24 @@ export class SplBuilder { ).subscribe( next => {}, err => { - console.log("build error\n", err); - this.messageHandler.handleError(err); - this.checkKnownErrors(err); + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); + } else { + errorNotification = this.messageHandler.handleError(err); + } + this.checkKnownErrors(err, errorNotification, this.buildAndDownloadBundle.bind(this), input); }, - downloadResult => console.log("download result\n",downloadResult), + complete => { + console.log("buildAndDownloadBundle observable complete"); + try { + if (input.filename && fs.existsSync(input.filename)) { + fs.unlinkSync(input.filename); + } + } catch (err) { + this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); + } + } ); } @@ -221,7 +261,7 @@ export class SplBuilder { map(consoleResult => { const [ artifacts, consoleResponse ] = consoleResult; if (consoleResponse.body["streams_console"] && consoleResponse.body["id"]) { - const consoleUrl = `${consoleResponse.body["streams_console"]}#application/dashboard/Application%20Dashboard?instance=${consoleResponse.body["id"]}`; + const consoleUrl = buildConsoleUrl(consoleResponse.body["streams_console"], consoleResponse.body["id"]); this.submitJobPrompt(consoleUrl, outputDir, this.submitAppObservable.bind(this), artifacts); @@ -233,15 +273,29 @@ export class SplBuilder { ).subscribe( next => {}, err => { - console.log("build and submit via Console error\n", err); - this.messageHandler.handleError(err); - this.checkKnownErrors(err); + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); + } else { + errorNotification = this.messageHandler.handleError(err); + } + this.checkKnownErrors(err, errorNotification, this.buildAndSubmitJob.bind(this), input); }, - consoleResult => console.log("submit via Console result\n", consoleResult), + complete => { + console.log("buildAndSubmitJob observable complete"); + try { + if (input.filename && fs.existsSync(input.filename)) { + fs.unlinkSync(input.filename); + } + } catch (err) { + this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); + } + } ); } submit(streamingAnalyticsCredentials, input) { + console.log("submit(); input:",arguments); this.serviceCredentials = SplBuilder.parseServiceCredentials(streamingAnalyticsCredentials); const self = this; if (this.serviceCredentials.apikey && this.serviceCredentials.v2_rest_url) { @@ -257,7 +311,7 @@ export class SplBuilder { map(consoleResult => { const [ submitInput, consoleResponse ] = consoleResult; if (consoleResponse.body["streams_console"] && consoleResponse.body["id"]) { - const consoleUrl = `${consoleResponse.body["streams_console"]}#application/dashboard/Application%20Dashboard?instance=${consoleResponse.body["id"]}`; + const consoleUrl = buildConsoleUrl(consoleResponse.body["streams_console"], consoleResponse.body["id"]); this.submitJobPrompt(consoleUrl, outputDir, this.submitSabObservable.bind(this), input); @@ -270,31 +324,50 @@ export class SplBuilder { ).subscribe( next => {}, err => { - console.log("submit job error\n", err); - this.messageHandler.handleError(err); - this.checkKnownErrors(err); + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); + } else { + errorNotification = this.messageHandler.handleError(err); + } + this.checkKnownErrors(err, errorNotification, this.submit.bind(this), [streamingAnalyticsCredentials, input]); }, - consoleResult => console.log("submit result\n", consoleResult), + complete => console.log("submit .sab observable complete"), ); } else { - this.messageHandler.handleError("Unable to determine Streaming Analytics service credentials."); - this.messageHandler.handleCredentialsMissing(); + const errorNotification = this.messageHandler.handleError("Unable to determine Streaming Analytics service credentials."); + this.messageHandler.handleCredentialsMissing(errorNotification); throw new Error("Error parsing VCAP_SERVICES environment variable"); } } submitJobPrompt(consoleUrl, outputDir, submissionObservableFunc, submissionObservableInput) { + console.log("submitJobPrompt(); input:",arguments); + let submissionTarget = "the application(s)"; + if (typeof(this.useMakefile) === "boolean") { + if(this.useMakefile) { + submissionTarget = "the application(s) for the Makefile"; + } else if (this.fqn) { + submissionTarget = this.fqn; + } + } else { + if (submissionObservableInput.filename) { + submissionTarget = submissionObservableInput.filename.split(path.sep).pop(); + } + } - // Submission dialog - const dialogMessage = "Job submission"; - const dialogDetail = "Submit the application(s) to your instance with default configuration " + + // Submission notification + let submissionNotification = null; + const dialogMessage = `Job submission - ${this.useMakefile ? this.makefilePath : submissionTarget}`; + const dialogDetail = `Submit ${submissionTarget} to your service with default configuration ` + "or use the Streaming Analytics Console to customize the submission time configuration."; const dialogButtons = [ { label: "Submit", callbackFn: () => { - this.messageHandler.handleSubmitProgressMessage("Submitting application to Streaming Analytics instance..."); + console.log("submitButtonCallback"); + this.messageHandler.handleInfo("Submitting application to Streaming Analytics service..."); submissionObservableFunc(submissionObservableInput).pipe( mergeMap(submitResult => { @@ -303,17 +376,17 @@ export class SplBuilder { label: "Open Streaming Analytics Console", callbackFn: () => this.openUrlHandler(consoleUrl) } - ] + ]; // when build+submit from makefile/spl file, potentially multiple objects coming back if (Array.isArray(submitResult)) { submitResult.forEach(obj => { if (obj.body) { - this.messageHandler.handleSubmitSuccess(obj.body, notificationButtons); + this.messageHandler.handleSuccess(`Job ${obj.body.name} is ${obj.body.health}`, {notificationButtons: notificationButtons}); } }); } else { if (submitResult.body) { - this.messageHandler.handleSubmitSuccess(submitResult.body, notificationButtons); + this.messageHandler.handleSuccess(`Job ${submitResult.body.name} is ${submitResult.body.health}`, {notificationButtons: notificationButtons}); } } return of(submitResult); @@ -321,78 +394,65 @@ export class SplBuilder { ).subscribe( next => {}, err => { - console.log("default job submit error\n", err); - this.messageHandler.handleError(err); - this.checkKnownErrors(err); + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); + } else { + errorNotification = this.messageHandler.handleError(err); + } + console.log("submitPrompt error caught, submissionObservableFunc:",submissionObservableFunc, "submissionObservableInput:",submissionObservableInput); + this.checkKnownErrors(err, errorNotification, this.submitJobPrompt.bind(this), [consoleUrl, outputDir, submissionObservableFunc, submissionObservableInput]); }, - consoleResult => console.log("submit result\n", consoleResult), + complete => console.log("job submission observable complete"), ); + this.messageHandler.dismissNotification(submissionNotification); } }, { - label: "Submit via Console", + label: "Submit via Streaming Analytics Console", callbackFn: () => { - const submitMsg = () => { - this.messageHandler.handleSuccess( - "Submit via Console", - `Use the Streaming Analytics Console to submit.`, - true, - true, - [ - { - label: "Copy output path", - callbackFn: () => ncp.copy(outputDir) - }, - { - label: "Open Streaming Analytics Console", - callbackFn: () => this.openUrlHandler(consoleUrl) - } - ] - ); - }; if (submissionObservableInput.filename && submissionObservableInput.filename.toLowerCase().endsWith(".sab")) { // sab is local already - submitMsg(); + this.openUrlHandler(consoleUrl); } else { // need to download bundles first - this.messageHandler.handleSubmitProgressMessage("Downloading application bundles for submission via Streaming analytics Console..."); + this.messageHandler.handleInfo("Downloading application bundles for submission via Streaming Analytics Console..."); this.downloadBundlesObservable(submissionObservableInput).pipe( map(downloadOutput => ( [ submissionObservableInput, downloadOutput ])), mergeMap(downloadResult => this.performBundleDownloads(downloadResult, null, outputDir)), ).subscribe( next => {}, err => { - console.log("Error downloading bundles for Console submit\n", err); - this.messageHandler.handleError(err); - this.checkKnownErrors(err); + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); + } else { + errorNotification = this.messageHandler.handleError(err); + } + this.checkKnownErrors(err, errorNotification); }, - submitMsg(), + complete => this.openUrlHandler(consoleUrl) ); } + this.messageHandler.dismissNotification(submissionNotification); } }, - { - label: "Cancel", - callbackFn: null - } ]; - this.messageHandler.showDialog(dialogMessage, dialogDetail, dialogButtons); - this.messageHandler.handleBuildProgressMessage(`Streaming Analytics Console URL: ${consoleUrl}`); - + submissionNotification = this.messageHandler.handleInfo(dialogMessage,{detail: dialogDetail, notificationAutoDismiss: false, notificationButtons: dialogButtons}); } /** * poll build status for a specific build * @param input - * @param messageHandler IDE specific message handler callback object */ pollBuildStatus(input) { let prevBuildOutput = []; - this.messageHandler.handleBuildProgressMessage("Building...", true); + let buildMessage = `Building ${this.useMakefile? this.makefilePath : this.fqn}...`; + this.messageHandler.handleInfo(buildMessage); return this.getBuildStatusObservable(input) .pipe( map((buildStatusResponse) => ({...input, ...buildStatusResponse.body})), @@ -404,7 +464,7 @@ export class SplBuilder { tap(s => { if (this._pollHandleMessage % 3 === 0) { const newOutput = this.getNewBuildOutput(s.output, prevBuildOutput); - this.messageHandler.handleBuildProgressMessage(newOutput, true); + this.messageHandler.handleInfo(buildMessage, {detail: this.messageHandler.getLoggableMessage(newOutput)}); prevBuildOutput = s.output; } this._pollHandleMessage++; @@ -436,38 +496,137 @@ export class SplBuilder { buildStatusIsComplete(input, prevBuildOutput) { if (input.status === "failed") { + const failMessage = `Build failed - ${this.useMakefile ? this.makefilePath : this.fqn}`; this.lintHandler.lint(input); const newOutput = this.getNewBuildOutput(input.output, prevBuildOutput); - this.messageHandler.handleBuildFailure(newOutput); + this.messageHandler.handleError(failMessage, {detail: this.messageHandler.getLoggableMessage(newOutput)}); return true; } else if (input.status === "built") { + const successMessage = `Build succeeded - ${this.useMakefile ? this.makefilePath : this.fqn}`; this.lintHandler.lint(input); const newOutput = this.getNewBuildOutput(input.output, prevBuildOutput); - this.messageHandler.handleBuildSuccess(newOutput); + this.messageHandler.handleSuccess(successMessage, {detail: this.messageHandler.getLoggableMessage(newOutput)}); return true; } else { return false; } } - checkKnownErrors(err) { + checkKnownErrors(err, errorNotification, retryCallbackFunction = null, retryInput = null) { if (typeof(err) === "string") { if (err.includes("CDISB4090E")) { - // additional notification with button to open bluemix dashboard so the user can verify their + // additional notification with button to open IBM Cloud dashboard so the user can verify their // service is started. - this.messageHandler.handleError( - "Verify that the streaming analytics service is started and able to handle requests.", - [{label: "Open IBM Cloud Dashboard", - callbackFn: ()=>{this.openUrlHandler("https://console.bluemix.net/dashboard/apps")} - }]); + const n = this.messageHandler.handleError( + "Verify that the Streaming Analytics service is started and able to handle requests.", + { notificationButtons: [ + { + label: "Open IBM Cloud Dashboard", + callbackFn: ()=>{this.openUrlHandler(ibmCloudDashboardUrl)} + }, + { + label: "Start service and retry", + callbackFn: ()=> this.startServiceAndRetry(retryCallbackFunction, retryInput, [errorNotification, n]) + } + ]} + ); + } + } + } + + startServiceAndRetry(retryCallbackFunction, retryInput, notifications) { + if (Array.isArray(notifications)) { + notifications.map(a => this.messageHandler.dismissNotification(a)); + } + + const startingNotification = this.messageHandler.handleInfo("Streaming Analytics service is starting...", {notificationAutoDismiss: false}); + let startSuccessNotification = null; + let serviceState = null; + const poll = interval(8000); + + poll.pipe( + takeUntil(this.startServiceObservable().pipe( + map(a => { + if (a && a.body && a.body.state){ + serviceState = a.body.state + } + })) + ), + ).subscribe( + next => {}, + err => { + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); + } else { + errorNotification = this.messageHandler.handleError(err); + } + this.checkKnownErrors(err, errorNotification, retryCallbackFunction, retryInput); + }, + startServiceResult => { + this.messageHandler.dismissNotification(startingNotification); + if (serviceState === "STARTED") { + console.log("serviceRestartedSuccess",arguments); + console.log("retryCallbackFunction:",retryCallbackFunction); + console.log("retryCallbackInput:",retryInput); + this.messageHandler.handleSuccess("Streaming Analytics service started", {detail: "Service has been started. Retrying Build Service request..."}); + if (typeof(retryCallbackFunction) === "function" && retryInput) { + if (Array.isArray(retryInput)) { + retryCallbackFunction.apply(this, retryInput); + } else { + retryCallbackFunction(retryInput); + } + } + } else { + this.messageHandler.handleError("Error starting service"); } + console.log("startService observable complete"); + }); + } + + openStreamingAnalyticsConsole(streamingAnalyticsCredentials) { + this.serviceCredentials = SplBuilder.parseServiceCredentials(streamingAnalyticsCredentials); + if (this.serviceCredentials.apikey && this.serviceCredentials.v2_rest_url) { + + this.getAccessTokenObservable().pipe( + mergeMap(response => { + this.accessToken = response.body.access_token; + return this.getConsoleUrlObservable(); + }), + map(response => { + if (response.body["streams_console"] && response.body["id"]) { + const consoleUrl = buildConsoleUrl(response.body["streams_console"], response.body["id"]); + this.openUrlHandler(consoleUrl); + } else { + this.messageHandler.handleError("Cannot retrieve Streaming Analytics Console URL"); + } + }) + ).subscribe( + next => {}, + err => { + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); + } else { + errorNotification = this.messageHandler.handleError(err); + } + this.checkKnownErrors(err); + }, + complete => { + console.log("get Console URL observable complete"); + } + ); } } + openCloudDashboard() { + this.openUrlHandler(ibmCloudDashboardUrl); + } + getAccessTokenObservable() { const iamTokenRequestOptions = { method: "POST", - url: "https://iam.bluemix.net/identity/token", + url: "https://iam.cloud.ibm.com/identity/token", json: true, headers: { Accept: "application/json", @@ -481,6 +640,24 @@ export class SplBuilder { return SplBuilder.createObservableRequest(iamTokenRequestOptions); } + startServiceObservable() { + console.log("startServiceObservable entry"); + const startServiceRequestOptions = { + method: "PATCH", + url: this.serviceCredentials.v2_rest_url, + instance_id: `${this.serviceCredentials.v2_rest_url.split("/").pop()}`, + json: true, + headers: { + "Authorization": `Bearer ${this.accessToken}`, + "Content-Type": "application/json" + }, + body: { + "state": "STARTED" + } + }; + return SplBuilder.createObservableRequest(startServiceRequestOptions); + } + getBuildStatusObservable(input) { console.log("pollBuildStatusObservable input:", input); const buildStatusRequestOptions = { @@ -504,6 +681,9 @@ export class SplBuilder { method: "POST", url: `${this.serviceCredentials.v2_rest_url}/builds`, json: true, + qs: { + originator: this.originatorString + }, headers: { "Authorization": `Bearer ${this.accessToken}`, "Content-Type": "application/json" @@ -512,7 +692,7 @@ export class SplBuilder { file: { value: fs.createReadStream(input.filename), options: { - filename: "___bundle.zip", + filename: input.filename.split(path.sep).pop(), contentType: "application/zip" } } @@ -639,7 +819,19 @@ export class SplBuilder { fs.unlinkSync(outputFile); } fs.writeFileSync(outputFile, downloadOutput[index].body); - this.messageHandler.handleSuccess(`Application ${artifact.name} bundle downloaded to output directory`, outputFile, true, true); + const notificationButtons = [ + { + label: "Copy output path", + callbackFn: () => ncp.copy(outputDir) + } + ]; + this.messageHandler.handleSuccess( + `Application ${artifact.name} bundle downloaded to output directory`, + { + detail: outputFile, + notificationButtons: notificationButtons + } + ); return of(outputDir); } catch (err) { throw new Error(`Error downloading application .sab bundle\n${err}`); @@ -667,15 +859,26 @@ export class SplBuilder { * */ static getToolkits(toolkitRootDir) { - let validToolkitPaths = null; + let validToolkitPaths = []; if (toolkitRootDir && toolkitRootDir.trim() !== "") { - if (fs.existsSync(toolkitRootDir)) { - let toolkitRootContents = fs.readdirSync(toolkitRootDir); - validToolkitPaths = toolkitRootContents - .filter(item => fs.lstatSync(`${toolkitRootDir}${path.sep}${item}`).isDirectory()) - .filter(dir => fs.readdirSync(`${toolkitRootDir}${path.sep}${dir}`).filter(tkDirItem => tkDirItem === "toolkit.xml").length > 0) - .map(tk => ({ tk: tk, tkPath: `${toolkitRootDir}${path.sep}${tk}` })); + let toolkitRoots = []; + + if (toolkitRootDir.includes(",") || toolkitRootDir.includes(";")) { + toolkitRoots.push(...toolkitRootDir.split(/[,;]/)); + } else { + toolkitRoots.push(toolkitRootDir); } + + toolkitRoots.forEach(toolkitRoot => { + if (fs.existsSync(toolkitRoot)) { + let toolkitRootContents = fs.readdirSync(toolkitRoot); + validToolkitPaths.push(...toolkitRootContents + .filter(item => fs.lstatSync(`${toolkitRoot}${path.sep}${item}`).isDirectory()) + .filter(dir => fs.readdirSync(`${toolkitRoot}${path.sep}${dir}`).filter(tkDirItem => tkDirItem === "toolkit.xml").length > 0) + .map(tk => ({ tk: tk, tkPath: `${toolkitRoot}${path.sep}${tk}` })) + ); + } + }); } return validToolkitPaths; } diff --git a/lib/spl-build.js b/lib/spl-build.js index 3e87b62..5ba312c 100644 --- a/lib/spl-build.js +++ b/lib/spl-build.js @@ -15,6 +15,8 @@ import { LintHandler } from "./LintHandler"; import { SplBuilder } from "./spl-build-common"; import { MainCompositePickerView } from "./views/MainCompositePickerView"; +import { version } from "../package.json"; + const CONF_TOOLKITS_PATH = "ide-ibmstreams.toolkitsPath"; const CONF_STREAMING_ANALYTICS_CREDENTIALS = "build-ibmstreams.streamingAnalyticsCredentials"; @@ -60,6 +62,8 @@ export default { "spl-build:build-make-submit": () => this.buildMake(SplBuilder.BUILD_ACTION.SUBMIT), "spl-build:build-make-download": () => this.buildMake(SplBuilder.BUILD_ACTION.DOWNLOAD), "spl-build:submit": () => this.submit(), + "spl-build:open-console": () => this.openConsole(), + "spl-build:open-IBM-cloud-dashboard": () => this.openCloudDashboard() } ) ); @@ -218,7 +222,7 @@ export default { this.subscriptions.add(this.consoleService); this.messageHandler = new MessageHandler(this.consoleService); this.lintHandler = new LintHandler(this.linterService, SplBuilder.SPL_MSG_REGEX, this.appRoot); - this.splBuilder = new SplBuilder(this.messageHandler, this.lintHandler, this.openUrlHandler); + this.splBuilder = new SplBuilder(this.messageHandler, this.lintHandler, this.openUrlHandler, {originator: "atom", version: version, type: "make"}); atom.workspace.open("atom://nuclide/console"); @@ -267,7 +271,7 @@ export default { this.consoleService = this.consumeConsoleService({id: fqn, name: fqn}); this.messageHandler = new MessageHandler(this.consoleService); this.lintHandler = new LintHandler(this.linterService, SplBuilder.SPL_MSG_REGEX, this.appRoot); - this.splBuilder = new SplBuilder(this.messageHandler, this.lintHandler, this.openUrlHandler); + this.splBuilder = new SplBuilder(this.messageHandler, this.lintHandler, this.openUrlHandler, {originator: "atom", version: version, type: "spl"}); atom.workspace.open("atom://nuclide/console"); @@ -320,6 +324,23 @@ export default { this.splBuilder.submit(this.streamingAnalyticsCredentials, {filename: selectedFilePath}); + }, + + openConsole() { + this.streamingAnalyticsCredentials = atom.config.get(CONF_STREAMING_ANALYTICS_CREDENTIALS); + this.consoleService = this.consumeConsoleService({id: name, name: name}); + this.messageHandler = new MessageHandler(this.consoleService); + this.lintHandler = new LintHandler(this.linterService, SplBuilder.SPL_MSG_REGEX, this.appRoot); + this.splBuilder = new SplBuilder(this.messageHandler, this.lintHandler, this.openUrlHandler); + this.splBuilder.openStreamingAnalyticsConsole(this.streamingAnalyticsCredentials); + }, + + openCloudDashboard() { + this.streamingAnalyticsCredentials = atom.config.get(CONF_STREAMING_ANALYTICS_CREDENTIALS); + this.consoleService = this.consumeConsoleService({id: name, name: name}); + this.messageHandler = new MessageHandler(this.consoleService); + this.splBuilder = new SplBuilder(this.messageHandler, null, this.openUrlHandler); + this.splBuilder.openCloudDashboard(); } }; diff --git a/package.json b/package.json index e94ac50..f124ca6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "build-ibmstreams", "main": "./lib/spl-build", - "version": "0.2.0", + "version": "0.3.0", "description": "IBM Streams build package [Beta]", "keywords": [], "repository": "https://github.com/IBMStreams/build-ibmstreams",