diff --git a/packages/imperative/CHANGELOG.md b/packages/imperative/CHANGELOG.md index 5351b72337..d697010671 100644 --- a/packages/imperative/CHANGELOG.md +++ b/packages/imperative/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the Imperative package will be documented in this file. +## Recent Changes + +- Enhancement: Add client-side event handling capabilities. [#1987](https://github.com/zowe/zowe-cli/pull/1987) + ## `8.0.0-next.202405061946` - Enhancement: Consolidated the Zowe client log files into the same directory. [#2116](https://github.com/zowe/zowe-cli/issues/2116) diff --git a/packages/imperative/__tests__/__integration__/imperative/__tests__/__integration__/cli/test/cli.imperative-test-cli.test.logging.integration.test.ts b/packages/imperative/__tests__/__integration__/imperative/__tests__/__integration__/cli/test/cli.imperative-test-cli.test.logging.integration.test.ts index e341e6c192..e86ee2d981 100644 --- a/packages/imperative/__tests__/__integration__/imperative/__tests__/__integration__/cli/test/cli.imperative-test-cli.test.logging.integration.test.ts +++ b/packages/imperative/__tests__/__integration__/imperative/__tests__/__integration__/cli/test/cli.imperative-test-cli.test.logging.integration.test.ts @@ -264,7 +264,10 @@ describe("imperative-test-cli test logging command", () => { // Set the ENV var for the script const response = runCliScript(__dirname + "/__scripts__/test_logging_cmd.sh", - TEST_ENVIRONMENT.workingDir, [], { IMPERATIVE_TEST_CLI_IMPERATIVE_LOG_LEVEL: "OFF" }); + TEST_ENVIRONMENT.workingDir, [], { + IMPERATIVE_TEST_CLI_IMPERATIVE_LOG_LEVEL: "OFF", + IMPERATIVE_TEST_CLI_APP_LOG_LEVEL: "OFF" + }); expect(response.stderr.toString()).toBe(""); expect(response.status).toBe(0); expect(response.stdout.toString()).toMatchSnapshot(); diff --git a/packages/imperative/src/cmd/__tests__/CommandProcessor.unit.test.ts b/packages/imperative/src/cmd/__tests__/CommandProcessor.unit.test.ts index aaaa738106..104dddc311 100644 --- a/packages/imperative/src/cmd/__tests__/CommandProcessor.unit.test.ts +++ b/packages/imperative/src/cmd/__tests__/CommandProcessor.unit.test.ts @@ -29,6 +29,7 @@ import { join } from "path"; jest.mock("../src/syntax/SyntaxValidator"); jest.mock("../src/utils/SharedOptions"); jest.mock("../../utilities/src/ImperativeConfig"); +jest.mock("../../events/src/ImperativeEventEmitter"); // Persist the original definitions of process.write const ORIGINAL_STDOUT_WRITE = process.stdout.write; diff --git a/packages/imperative/src/config/__tests__/Config.api.unit.test.ts b/packages/imperative/src/config/__tests__/Config.api.unit.test.ts index af72547cbb..8130b143c0 100644 --- a/packages/imperative/src/config/__tests__/Config.api.unit.test.ts +++ b/packages/imperative/src/config/__tests__/Config.api.unit.test.ts @@ -19,6 +19,8 @@ import { IConfig } from "../src/doc/IConfig"; import { IConfigLayer } from "../src/doc/IConfigLayer"; import { IConfigProfile } from "../src/doc/IConfigProfile"; +jest.mock("../../events/src/ImperativeEventEmitter"); + const MY_APP = "my_app"; const mergeConfig: IConfig = { diff --git a/packages/imperative/src/config/__tests__/Config.secure.unit.test.ts b/packages/imperative/src/config/__tests__/Config.secure.unit.test.ts index b189683cf7..619b0fb087 100644 --- a/packages/imperative/src/config/__tests__/Config.secure.unit.test.ts +++ b/packages/imperative/src/config/__tests__/Config.secure.unit.test.ts @@ -18,6 +18,7 @@ import { Config } from "../src/Config"; import { IConfig } from "../src/doc/IConfig"; import { IConfigSecure } from "../src/doc/IConfigSecure"; import { IConfigVault } from "../src/doc/IConfigVault"; +import { ImperativeEventEmitter } from "../../events"; const MY_APP = "my_app"; @@ -46,6 +47,9 @@ describe("Config secure tests", () => { }); beforeEach(() => { + jest.spyOn(ImperativeEventEmitter, "initialize").mockImplementation(); + Object.defineProperty(ImperativeEventEmitter, "instance", { value: { emitEvent: jest.fn() }}); + mockSecureLoad = jest.fn(); mockSecureSave = jest.fn(); mockVault = { diff --git a/packages/imperative/src/config/__tests__/Config.unit.test.ts b/packages/imperative/src/config/__tests__/Config.unit.test.ts index 708e480243..abfb7480d5 100644 --- a/packages/imperative/src/config/__tests__/Config.unit.test.ts +++ b/packages/imperative/src/config/__tests__/Config.unit.test.ts @@ -19,6 +19,9 @@ import { ConfigConstants } from "../src/ConfigConstants"; import * as JSONC from "comment-json"; import { ConfigLayers, ConfigSecure } from "../src/api"; + +jest.mock("../../events/src/ImperativeEventEmitter"); + const MY_APP = "my_app"; describe("Config tests", () => { diff --git a/packages/imperative/src/config/__tests__/ConfigAutoStore.unit.test.ts b/packages/imperative/src/config/__tests__/ConfigAutoStore.unit.test.ts index a861671f60..73a22e7908 100644 --- a/packages/imperative/src/config/__tests__/ConfigAutoStore.unit.test.ts +++ b/packages/imperative/src/config/__tests__/ConfigAutoStore.unit.test.ts @@ -10,6 +10,8 @@ */ jest.mock("../../logger/src/LoggerUtils"); +jest.mock("../../events/src/ImperativeEventEmitter"); + import { AbstractAuthHandler } from "../../imperative"; import { SessConstants } from "../../rest"; import { ImperativeConfig } from "../../utilities"; diff --git a/packages/imperative/src/config/__tests__/ProfileInfo.TeamConfig.unit.test.ts b/packages/imperative/src/config/__tests__/ProfileInfo.TeamConfig.unit.test.ts index 237b393427..e8db53d543 100644 --- a/packages/imperative/src/config/__tests__/ProfileInfo.TeamConfig.unit.test.ts +++ b/packages/imperative/src/config/__tests__/ProfileInfo.TeamConfig.unit.test.ts @@ -35,6 +35,8 @@ import { IExtendersJsonOpts } from "../src/doc/IExtenderOpts"; import { ConfigSchema } from "../src/ConfigSchema"; import { Logger } from "../.."; +jest.mock("../../events/src/ImperativeEventEmitter"); + const testAppNm = "ProfInfoApp"; const testEnvPrefix = testAppNm.toUpperCase(); const profileTypes = ["zosmf", "tso", "base", "dummy"]; diff --git a/packages/imperative/src/config/src/Config.ts b/packages/imperative/src/config/src/Config.ts index 57585f9823..decda488ff 100644 --- a/packages/imperative/src/config/src/Config.ts +++ b/packages/imperative/src/config/src/Config.ts @@ -31,6 +31,8 @@ import { ConfigUtils } from "./ConfigUtils"; import { IConfigSchemaInfo } from "./doc/IConfigSchema"; import { JsUtils } from "../../utilities/src/JsUtils"; import { IConfigMergeOpts } from "./doc/IConfigMergeOpts"; +import { ImperativeEventEmitter } from "../../events"; +import { Logger } from "../../logger"; /** * Enum used by Config class to maintain order of config layers @@ -153,6 +155,8 @@ export class Config { myNewConfig.mVault = opts.vault; myNewConfig.mSecure = {}; + ImperativeEventEmitter.initialize(app, { logger:Logger.getAppLogger() }); + // Populate configuration file layers await myNewConfig.reload(opts); diff --git a/packages/imperative/src/config/src/ConfigUtils.ts b/packages/imperative/src/config/src/ConfigUtils.ts index 84dcbe7ae6..583676a0f8 100644 --- a/packages/imperative/src/config/src/ConfigUtils.ts +++ b/packages/imperative/src/config/src/ConfigUtils.ts @@ -9,13 +9,18 @@ * */ -import { normalize as pathNormalize } from "path"; +import { homedir as osHomedir } from "os"; +import { normalize as pathNormalize, join as pathJoin } from "path"; import { existsSync as fsExistsSync } from "fs"; -import { CredentialManagerFactory } from "../../security"; +import { CredentialManagerFactory } from "../../security/src/CredentialManagerFactory"; import { ICommandArguments } from "../../cmd"; import { ImperativeConfig } from "../../utilities"; import { ImperativeError } from "../../error"; +import { LoggerManager } from "../../logger/src/LoggerManager"; +import { LoggingConfigurer } from "../../imperative/src/LoggingConfigurer"; +import { Logger } from "../../logger/src/Logger"; +import { EnvironmentalVariableSettings } from "../../imperative/src/env/EnvironmentalVariableSettings"; export class ConfigUtils { /** @@ -114,4 +119,43 @@ export class ConfigUtils { additionalDetails: details }); } + + + // _______________________________________________________________________ + /** + * Perform a rudimentary initialization of some Imperative utilities. + * We must do this because VSCode apps do not typically call imperative.init. + * @internal + */ + public static initImpUtils(appName: string) { + // create a rudimentary ImperativeConfig if it has not been initialized + if (ImperativeConfig.instance.loadedConfig == null) { + let homeDir: string = null; + const envVarPrefix = appName.toUpperCase(); + const envVarNm = envVarPrefix + EnvironmentalVariableSettings.CLI_HOME_SUFFIX; + if (process.env[envVarNm] === undefined) { + // use OS home directory + homeDir = pathJoin(osHomedir(), "." + appName.toLowerCase()); + } else { + // use the available environment variable + homeDir = pathNormalize(process.env[envVarNm]); + } + ImperativeConfig.instance.loadedConfig = { + name: appName, + defaultHome: homeDir, + envVariablePrefix: envVarPrefix + }; + ImperativeConfig.instance.rootCommandName = appName; + } + + // initialize logging + if (LoggerManager.instance.isLoggerInit === false) { + const loggingConfig = LoggingConfigurer.configureLogger( + ImperativeConfig.instance.cliHome, ImperativeConfig.instance.loadedConfig + ); + Logger.initLogger(loggingConfig); + } + return Logger.getImperativeLogger(); + } + } diff --git a/packages/imperative/src/config/src/ProfileInfo.ts b/packages/imperative/src/config/src/ProfileInfo.ts index 9479743e20..278524d0d4 100644 --- a/packages/imperative/src/config/src/ProfileInfo.ts +++ b/packages/imperative/src/config/src/ProfileInfo.ts @@ -37,12 +37,9 @@ import { ICommandProfileProperty, ICommandArguments } from "../../cmd"; import { IProfileLoaded, IProfileProperty, IProfileSchema } from "../../profiles"; // for imperative operations -import { EnvironmentalVariableSettings } from "../../imperative/src/env/EnvironmentalVariableSettings"; -import { LoggingConfigurer } from "../../imperative/src/LoggingConfigurer"; import { CliUtils, ImperativeConfig } from "../../utilities"; import { ImperativeExpect } from "../../expect"; import { Logger, LoggerUtils } from "../../logger"; -import { LoggerManager } from "../../logger/src/LoggerManager"; import { IOptionsForAddConnProps, ISession, Session, SessConstants, ConnectionPropsForSessCfg } from "../../rest"; @@ -180,7 +177,7 @@ export class ProfileInfo { this.mCredentials = new ProfileCredentials(this, profInfoOpts); // do enough Imperative stuff to let imperative utilities work - this.initImpUtils(); + this.mImpLogger = ConfigUtils.initImpUtils(this.mAppName); } /** @@ -969,42 +966,6 @@ export class ProfileInfo { } } - // _______________________________________________________________________ - /** - * Perform a rudimentary initialization of some Imperative utilities. - * We must do this because VSCode apps do not typically call imperative.init. - */ - private initImpUtils() { - // create a rudimentary ImperativeConfig if it has not been initialized - if (ImperativeConfig.instance.loadedConfig == null) { - let homeDir: string = null; - const envVarPrefix = this.mAppName.toUpperCase(); - const envVarNm = envVarPrefix + EnvironmentalVariableSettings.CLI_HOME_SUFFIX; - if (process.env[envVarNm] === undefined) { - // use OS home directory - homeDir = path.join(os.homedir(), "." + this.mAppName.toLowerCase()); - } else { - // use the available environment variable - homeDir = path.normalize(process.env[envVarNm]); - } - ImperativeConfig.instance.loadedConfig = { - name: this.mAppName, - defaultHome: homeDir, - envVariablePrefix: envVarPrefix - }; - ImperativeConfig.instance.rootCommandName = this.mAppName; - } - - // initialize logging - if (LoggerManager.instance.isLoggerInit === false) { - const loggingConfig = LoggingConfigurer.configureLogger( - ImperativeConfig.instance.cliHome, ImperativeConfig.instance.loadedConfig - ); - Logger.initLogger(loggingConfig); - } - this.mImpLogger = Logger.getImperativeLogger(); - } - /** * Load any profile schema objects found on disk and cache them. For team * config, we check each config layer and load its schema JSON if there is diff --git a/packages/imperative/src/config/src/api/ConfigSecure.ts b/packages/imperative/src/config/src/api/ConfigSecure.ts index ad887c99cc..765a8d430a 100644 --- a/packages/imperative/src/config/src/api/ConfigSecure.ts +++ b/packages/imperative/src/config/src/api/ConfigSecure.ts @@ -20,6 +20,8 @@ import { ConfigConstants } from "../ConfigConstants"; import { IConfigProfile } from "../doc/IConfigProfile"; import { CredentialManagerFactory } from "../../../security"; import { ConfigUtils } from "../ConfigUtils"; +import { ImperativeEventEmitter } from "../../../events/src/ImperativeEventEmitter"; +import { ImperativeUserEvents } from "../../../events/src/ImperativeEventConstants"; /** * API Class for manipulating config layers. @@ -130,6 +132,7 @@ export class ConfigSecure extends ConfigApi { */ public async directSave() { await this.mConfig.mVault.save(ConfigConstants.SECURE_ACCT, JSONC.stringify(this.mConfig.mSecure)); + ImperativeEventEmitter.instance.emitEvent(ImperativeUserEvents.ON_VAULT_CHANGED); } // _______________________________________________________________________ diff --git a/packages/imperative/src/events/__tests__/__integration__/ImperativeEventEmitter.integration.test.ts b/packages/imperative/src/events/__tests__/__integration__/ImperativeEventEmitter.integration.test.ts new file mode 100644 index 0000000000..2b4dd4041e --- /dev/null +++ b/packages/imperative/src/events/__tests__/__integration__/ImperativeEventEmitter.integration.test.ts @@ -0,0 +1,101 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { IImperativeEventJson, ImperativeEventEmitter, ImperativeSharedEvents } from "../../.."; +import { ITestEnvironment } from "../../../../__tests__/__src__/environment/doc/response/ITestEnvironment"; +import { TestLogger } from "../../../../__tests__/src/TestLogger"; +import * as TestUtil from "../../../../__tests__/src/TestUtil"; +import { SetupTestEnvironment } from "../../../../__tests__/__src__/environment/SetupTestEnvironment"; +import * as fs from "fs"; +import * as path from "path"; + +let TEST_ENVIRONMENT: ITestEnvironment; +const iee = ImperativeEventEmitter; +const iee_s = ImperativeSharedEvents; +let cwd = ''; + +describe("Event Emitter", () => { + const mainModule = process.mainModule; + const testLogger = TestLogger.getTestLogger(); + + beforeAll(async () => { + (process.mainModule as any) = { + filename: __filename + }; + + TEST_ENVIRONMENT = await SetupTestEnvironment.createTestEnv({ + cliHomeEnvVar: "ZOWE_CLI_HOME", + testName: "event_emitter" + }); + cwd = TEST_ENVIRONMENT.workingDir; + }); + + beforeEach(() => { + iee.initialize("zowe", { logger: testLogger }); + }); + + afterEach(() => { + iee.teardown(); + }); + + afterAll(() => { + process.mainModule = mainModule; + TestUtil.rimraf(cwd); + }); + + const doesEventFileExists = (eventType: string) => { + const eventDir = iee.instance.getEventDir(eventType); + if (!fs.existsSync(eventDir)) return false; + if (fs.existsSync(path.join(eventDir, eventType))) return true; + return false; + }; + + describe("Shared Events", () => { + it("should create an event file upon first subscription if the file does not exist", () => { + const theEvent = iee_s.ON_CREDENTIAL_MANAGER_CHANGED; + + expect(doesEventFileExists(theEvent)).toBeFalsy(); + + const subSpy = jest.fn(); + iee.instance.subscribe(theEvent, subSpy); + + expect(subSpy).not.toHaveBeenCalled(); + expect(doesEventFileExists(theEvent)).toBeTruthy(); + + expect(iee.instance.getEventContents(theEvent)).toBeFalsy(); + + iee.instance.emitEvent(theEvent); + + (iee.instance as any).subscriptions.get(theEvent)[1][0](); // simulate FSWatcher called + + expect(doesEventFileExists(theEvent)).toBeTruthy(); + const eventDetails: IImperativeEventJson = JSON.parse(iee.instance.getEventContents(theEvent)); + expect(eventDetails.type).toEqual(theEvent); + expect(eventDetails.user).toBeFalsy(); + + expect(subSpy).toHaveBeenCalled(); + }); + it("should trigger subscriptions for all instances watching for onCredentialManagerChanged", () => { }); + it("should not affect subscriptions from another instance when unsubscribing from events", () => { }); + }); + + describe("User Events", () => { + it("should create an event file upon first subscription if the file does not exist", () => { }); + it("should trigger subscriptions for all instances watching for onVaultChanged", () => { }); + it("should not affect subscriptions from another instance when unsubscribing from events", () => { }); + }); + + describe("Custom Events", () => { + it("should create an event file upon first subscription if the file does not exist", () => { }); + it("should trigger subscriptions for all instances watching for onMyCustomEvent", () => { }); + it("should not affect subscriptions from another instance when unsubscribing from events", () => { }); + }); +}); diff --git a/packages/imperative/src/events/__tests__/__unit__/ImperativeEventEmitter.unit.test.ts b/packages/imperative/src/events/__tests__/__unit__/ImperativeEventEmitter.unit.test.ts new file mode 100644 index 0000000000..4421d8b41b --- /dev/null +++ b/packages/imperative/src/events/__tests__/__unit__/ImperativeEventEmitter.unit.test.ts @@ -0,0 +1,290 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import * as fs from "fs"; +import { join } from "path"; +import { homedir } from "os"; +import { Logger } from "../../../logger/src/Logger"; +import { ImperativeEventEmitter, ImperativeSharedEvents, ImperativeUserEvents } from "../.."; + +jest.mock("fs"); + +describe("Event Emitter", () => { + const iee = ImperativeEventEmitter; + const sharedDir = join(__dirname, ".zowe", ".events"); + const userDir = join(homedir(), ".zowe", ".events"); + let spyFsWriteFileSync: jest.SpyInstance; + let allCallbacks: Function[]; + let removeAllListeners: jest.SpyInstance; + const closeWatcher = jest.fn(); + + beforeEach(() => { + jest.restoreAllMocks(); + (iee as any).initialized = undefined; + process.env["ZOWE_CLI_HOME"] = join(__dirname, ".zowe"); + jest.spyOn(fs, "existsSync").mockImplementation(jest.fn()); + jest.spyOn(fs, "mkdirSync").mockImplementation(jest.fn()); + jest.spyOn(fs, "openSync").mockImplementation(jest.fn()); + jest.spyOn(fs, "closeSync").mockImplementation(jest.fn()); + jest.spyOn(fs, "openSync").mockImplementation(jest.fn()); + spyFsWriteFileSync = jest.spyOn(fs, "writeFileSync").mockImplementation(jest.fn()); + allCallbacks = []; + removeAllListeners = jest.fn().mockReturnValue({ close: closeWatcher }); + jest.spyOn(fs, "watch").mockImplementation((_event: string | any, cb: Function | any) => { + allCallbacks.push(cb); + return { close: jest.fn(), removeAllListeners } as any; + }); + }); + + describe("Base structure and emission", () => { + it("should only allow for one instance of the event emitter", () => { + jest.spyOn(Logger, "getImperativeLogger").mockReturnValue("the logger" as any); + iee.initialize("test"); + let caughtError: any; + try { + iee.initialize("dummy"); + } catch (err) { + caughtError = err; + } + expect(caughtError).toBeDefined(); + expect(caughtError.message).toContain("Only one instance"); + expect(iee.instance.appName).toEqual("test"); + expect(iee.instance.logger).toEqual("the logger"); + }); + + it("should determine the type of event", () => { + iee.initialize("test"); + expect(iee.instance.isUserEvent("dummy")).toBe(false); + expect(iee.instance.isUserEvent(ImperativeUserEvents.ON_VAULT_CHANGED)).toBe(true); + expect(iee.instance.isSharedEvent("dummy")).toBe(false); + expect(iee.instance.isSharedEvent(ImperativeSharedEvents.ON_CREDENTIAL_MANAGER_CHANGED)).toBe(true); + + expect(iee.instance.isCustomEvent(ImperativeUserEvents.ON_VAULT_CHANGED)).toBe(false); + expect(iee.instance.isCustomEvent(ImperativeSharedEvents.ON_CREDENTIAL_MANAGER_CHANGED)).toBe(false); + expect(iee.instance.isCustomEvent("dummy")).toBe(true); + }); + + it("should determine the correct directory based on the event", () => { + iee.initialize("test"); + expect(iee.instance.getEventDir("dummy")).toEqual(sharedDir); + expect(iee.instance.getEventDir(ImperativeUserEvents.ON_VAULT_CHANGED)).toEqual(userDir); + expect(iee.instance.getEventDir(ImperativeSharedEvents.ON_CREDENTIAL_MANAGER_CHANGED)).toEqual(sharedDir); + delete process.env["ZOWE_CLI_HOME"]; + }); + + it("should not allow all kinds of events to be emitted", () => { + iee.initialize("zowe"); + expect(iee.instance.appName).toEqual("zowe"); + + const processError = (eventType: string, msg: string, isCustomEvent = true) => { + let caughtError: any; + try { + iee.instance[(isCustomEvent ? "emitCustomEvent" : "emitEvent")](eventType as any); + } catch (err) { + caughtError = err; + } + expect(caughtError).toBeDefined(); + expect(caughtError.message).toContain(msg); + }; + + const aMsg = "Unable to determine the type of event."; + const bMsg = "Operation not allowed. Event is considered protected"; + + // Application developers shouldn't be able to emit custom events from emitEvent, even though it is an internal method + processError("dummy", aMsg, false); + processError(ImperativeUserEvents.ON_VAULT_CHANGED, bMsg); + processError(ImperativeSharedEvents.ON_CREDENTIAL_MANAGER_CHANGED, bMsg); + }); + + it("should write to a file with all required properties in IImperativeEventJson to the correct location", () => { + iee.initialize("zowe"); + expect(iee.instance.appName).toEqual("zowe"); + + const processEvent = (theEvent: any, isUser: boolean, isCustomEvent = false) => { + // Emit the event + iee.instance[(isCustomEvent ? "emitCustomEvent" : "emitEvent")](theEvent); + + const dir = isUser ? userDir : sharedDir; + expect(fs.existsSync).toHaveBeenCalledWith(dir); + expect(fs.mkdirSync).toHaveBeenCalledWith(dir); + expect(spyFsWriteFileSync.mock.calls[0][0]).toEqual(join(dir, theEvent)); + expect(JSON.parse(spyFsWriteFileSync.mock.calls[0][1])).toMatchObject({ + type: theEvent, + user: isUser, + loc: dir, + }); + spyFsWriteFileSync.mockClear(); + }; + + processEvent(ImperativeUserEvents.ON_VAULT_CHANGED, true); + processEvent(ImperativeSharedEvents.ON_CREDENTIAL_MANAGER_CHANGED, false); + processEvent("onSuperCustomEvent", false, true); + }); + + it("should fail to emit, subscribe or unsubscribe if the emitter has not been initialized", () => { + const getError = (shouldThrow: any) => { + let caughtError: any; + try { + shouldThrow(); + } catch (err) { + caughtError = err; + } + return caughtError ?? { message: "THIS METHOD DID NOT THROW AN ERROR" }; + }; + + const cbs = [ + // Emitting should fail if IEE is not initialized + () => { iee.instance.emitEvent("dummy" as any); }, + () => { iee.instance.emitEvent(ImperativeUserEvents.ON_VAULT_CHANGED); }, + () => { iee.instance.emitEvent(ImperativeSharedEvents.ON_CREDENTIAL_MANAGER_CHANGED); }, + () => { iee.instance.emitCustomEvent("dummy"); }, + () => { iee.instance.emitCustomEvent(ImperativeUserEvents.ON_VAULT_CHANGED); }, + () => { iee.instance.emitCustomEvent(ImperativeSharedEvents.ON_CREDENTIAL_MANAGER_CHANGED); }, + + // Subscribing should fail if IEE is not initialized + () => { iee.instance.subscribe("dummy", jest.fn()); }, + () => { iee.instance.subscribe(ImperativeUserEvents.ON_VAULT_CHANGED, jest.fn()); }, + () => { iee.instance.subscribe(ImperativeSharedEvents.ON_CREDENTIAL_MANAGER_CHANGED, jest.fn()); }, + () => { iee.instance.unsubscribe("dummy"); }, + () => { iee.instance.unsubscribe(ImperativeUserEvents.ON_VAULT_CHANGED); }, + () => { iee.instance.unsubscribe(ImperativeSharedEvents.ON_CREDENTIAL_MANAGER_CHANGED); }, + ]; + cbs.forEach(cb => { + expect((getError(cb)).message).toContain("You must initialize the instance"); + }); + }); + + it("should surface errors if unable to create event files or directories", () => { + iee.initialize("zowe"); + + jest.spyOn(fs, "mkdirSync").mockImplementationOnce(() => { throw "DIR"; }); + + const theEvent = ImperativeUserEvents.ON_VAULT_CHANGED; + try { + iee.instance.subscribe(theEvent, jest.fn()); + } catch (err) { + expect(err.message).toContain("Unable to create '.events' directory."); + } + expect(fs.existsSync).toHaveBeenCalledWith(userDir); + expect(fs.mkdirSync).toHaveBeenCalledWith(userDir); + + jest.spyOn(fs, "closeSync").mockImplementation(() => { throw "FILE"; }); + + try { + iee.instance.subscribe(theEvent, jest.fn()); + } catch (err) { + expect(err.message).toContain("Unable to create event file."); + } + expect(fs.existsSync).toHaveBeenCalledWith(join(userDir, theEvent)); + expect(fs.openSync).toHaveBeenCalledWith(join(userDir, theEvent), "w"); + expect(fs.closeSync).toHaveBeenCalled(); + }); + + it("should subscribe even when the onEventFile or the events directory do not exist", () => { + iee.initialize("zowe"); + expect(iee.instance.appName).toEqual("zowe"); + + const processSubcription = (theEvent: any, isUser: boolean) => { + const dir = isUser ? userDir : sharedDir; + const cbSpy = jest.fn(); + iee.instance.subscribe(theEvent, cbSpy); + + // Ensure the directory is created + expect(fs.existsSync).toHaveBeenCalledWith(dir); + expect(fs.mkdirSync).toHaveBeenCalledWith(dir); + + // Ensure the file is created + expect(fs.existsSync).toHaveBeenCalledWith(join(dir, theEvent)); + expect(fs.openSync).toHaveBeenCalledWith(join(dir, theEvent), "w"); + expect(fs.closeSync).toHaveBeenCalled(); + }; + + processSubcription(ImperativeUserEvents.ON_VAULT_CHANGED, true); + processSubcription(ImperativeSharedEvents.ON_CREDENTIAL_MANAGER_CHANGED, false); + }); + + it("should trigger all callbacks when subscribed event is emitted", () => { + jest.spyOn(ImperativeEventEmitter.prototype, "emitEvent").mockImplementation((theEvent: any) => { + (iee.instance as any).subscriptions.get(theEvent)[1].forEach((cb: any) => cb()); + }); + jest.spyOn(fs, "readFileSync").mockReturnValue("{\"time\":\"123456\"}"); + + iee.initialize("zowe"); + expect(iee.instance.appName).toEqual("zowe"); + + const processEmission = (theEvent: any, isCustomEvent = false) => { + const cbSpy = jest.fn().mockReturnValue("test"); + const numberOfCalls = Math.floor(Math.random() * 20); + let i = numberOfCalls; + while(i-- > 0) { + iee.instance.subscribe(theEvent, cbSpy); + } + + iee.instance[(isCustomEvent ? "emitCustomEvent" : "emitEvent")](theEvent); + expect(cbSpy).toHaveBeenCalledTimes(numberOfCalls); + }; + + processEmission(ImperativeUserEvents.ON_VAULT_CHANGED); + processEmission(ImperativeSharedEvents.ON_CREDENTIAL_MANAGER_CHANGED); + }); + + it("should unsubscribe from events successfully", () => { + iee.initialize("zowe"); + + const dummyMap = { + has: () => (true), + delete: jest.fn(), + get: () => ([{ removeAllListeners }, jest.fn()]) + }; + // Mocked map of subscriptions + (iee.instance as any).subscriptions = dummyMap; + (iee.instance as any).eventTimes = dummyMap; + + iee.instance.unsubscribe("dummy"); + expect(closeWatcher).toHaveBeenCalled(); + }); + + it("should teardown the Event Emitter instance successfully", () => { + expect((iee as any).initialized).toBeFalsy(); + iee.teardown(); + expect((iee as any).initialized).toBeFalsy(); + + iee.initialize("zowe", {logger: jest.fn() as any}); + expect((iee as any).initialized).toBeTruthy(); + + const dummyMap = { + has: () => (true), + delete: jest.fn(), + keys: () => ["dummy"], + get: () => ([{ removeAllListeners }, jest.fn()]) + }; + // Mocked map of subscriptions + (iee.instance as any).subscriptions = dummyMap; + (iee.instance as any).eventTimes = dummyMap; + + iee.teardown(); + expect(closeWatcher).toHaveBeenCalled(); + expect((iee as any).initialized).toBeFalsy(); + }); + + it("should retrieve event contents successfully", () => { + jest.spyOn(fs, "existsSync").mockReturnValueOnce(false); + iee.initialize("zowe"); + let contents = iee.instance.getEventContents(ImperativeUserEvents.ON_VAULT_CHANGED); + expect(contents).toEqual(""); + + jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); + jest.spyOn(fs, "readFileSync").mockReturnValueOnce("dummy"); + contents = iee.instance.getEventContents(ImperativeUserEvents.ON_VAULT_CHANGED); + expect(contents).toEqual("dummy"); + }); + }); +}); diff --git a/packages/imperative/src/events/index.ts b/packages/imperative/src/events/index.ts new file mode 100644 index 0000000000..8da687cf65 --- /dev/null +++ b/packages/imperative/src/events/index.ts @@ -0,0 +1,15 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +export * from "./src/doc"; +export * from "./src/ImperativeEvent"; +export * from "./src/ImperativeEventConstants"; +export * from "./src/ImperativeEventEmitter"; diff --git a/packages/imperative/src/events/src/ImperativeEvent.ts b/packages/imperative/src/events/src/ImperativeEvent.ts new file mode 100644 index 0000000000..b06f046560 --- /dev/null +++ b/packages/imperative/src/events/src/ImperativeEvent.ts @@ -0,0 +1,126 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { randomUUID } from "crypto"; +import { IImperativeEventJson, IImperativeEventParms } from "./doc"; +import { ImperativeEventType } from "./ImperativeEventConstants"; + +/** + * + * @export + * @class ImperativeEvent + */ +export class ImperativeEvent { + /** + * The ID of the event + * @private + * @type {string} + * @memberof ImperativeEvent + */ + private mEventID: string; + + /** + * The application ID that caused this event + * @private + * @type {string} + * @memberof ImperativeEvent + */ + private mAppID: string; + + /** + * The time of the event created with new Date().toISOString() (ISO String) + * @private + * @type {string} + * @memberof ImperativeEvent + */ + private mEventTime: string; + + /** + * The location of the event + * @private + * @type {string} + * @memberof ImperativeEvent + */ + private mEventLoc: string; + + /** + * The type of event that occurred + * @private + * @type {string} + * @memberof ImperativeEvent + */ + private mEventType: ImperativeEventType | string; + + /** + * Indicator of user-specific (if true) or shared (if false) events + * @private + * @type {boolean} + * @memberof ImperativeEvent + */ + private isUserEvent: boolean; + + /** + * toString overload to be called automatically on string concatenation + * @returns string representation of the imperative event + */ + public toString = (): string => { + return `Type: ${this.type} \t| Time: ${this.time} \t| App: ${this.appName} \t| ID: ${this.id}`; + }; + + /** + * toJson helper method to be called for emitting or logging imperative events + * @returns JSON representation of the imperative event + */ + public toJson = (): IImperativeEventJson => { + return { + time: this.time, + type: this.type, + source: this.appName, + id: this.id, + loc: this.location, + user: this.isUserEvent, + }; + }; + + constructor(parms: IImperativeEventParms) { + this.mEventTime = new Date().toISOString(); + this.mEventID = randomUUID(); + this.mAppID = parms.appName; + this.mEventType = parms.eventName; + this.isUserEvent = parms.isUser; + parms.logger.debug("ImperativeEvent: " + this); + } + + public set location(location: string) { + // TODO: (edge-case) Test whether we need to re-assign the location (multiple times) of an already initialized event + this.mEventLoc ||= location; + } + + public get location(): string { + return this.mEventLoc; + } + + public get time(): string { + return this.mEventTime; + } + + public get type(): ImperativeEventType | string { + return this.mEventType; + } + + public get appName(): string { + return this.mAppID; + } + + public get id() : string { + return this.mEventID; + } +} diff --git a/packages/imperative/src/events/src/ImperativeEventConstants.ts b/packages/imperative/src/events/src/ImperativeEventConstants.ts new file mode 100644 index 0000000000..0fd35d70af --- /dev/null +++ b/packages/imperative/src/events/src/ImperativeEventConstants.ts @@ -0,0 +1,50 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +export enum ImperativeUserEvents { + ON_VAULT_CHANGED = "onVaultChanged" +} +export enum ImperativeSharedEvents { + ON_CREDENTIAL_MANAGER_CHANGED = "onCredentialManagerChanged" +} + +export type ImperativeEventType = ImperativeUserEvents | ImperativeSharedEvents; + +/** + * TODO: + * The following list of event types will only be implemented upon request + * + * Shared events: + * Global: + * - $ZOWE_CLI_HOME/.events/onConfigChanged + * - $ZOWE_CLI_HOME/.events/onSchemaChanged + * Project: + * - $ZOWE_CLI_HOME/.events//onConfigChanged + * - $ZOWE_CLI_HOME/.events//onSchemaChanged + * + * User events: + * Global: + * - ~/.zowe/.events/onUserConfigChanged + * Project: + * - ~/.zowe/.events//onUserConfigChanged + * + * Custom events: + * Shared: + * Global: + * - $ZOWE_CLI_HOME/.events// + * Project: + * - $ZOWE_CLI_HOME/.events/// + * User: + * Global: + * - ~/.zowe/.events// + * Project: + * - ~/.zowe/.events/// + */ \ No newline at end of file diff --git a/packages/imperative/src/events/src/ImperativeEventEmitter.ts b/packages/imperative/src/events/src/ImperativeEventEmitter.ts new file mode 100644 index 0000000000..e8ca554397 --- /dev/null +++ b/packages/imperative/src/events/src/ImperativeEventEmitter.ts @@ -0,0 +1,288 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import * as fs from "fs"; +import { homedir } from "os"; +import { join } from "path"; +import { ImperativeConfig } from "../../utilities/src/ImperativeConfig"; +import { ImperativeError } from "../../error/src/ImperativeError"; +import { ImperativeEventType, ImperativeUserEvents, ImperativeSharedEvents } from "./ImperativeEventConstants"; +import { ImperativeEvent } from "./ImperativeEvent"; +import { Logger } from "../../logger/src/Logger"; +import { LoggerManager } from "../../logger/src/LoggerManager"; +import { IImperativeRegisteredAction, IImperativeEventEmitterOpts, IImperativeEventJson, ImperativeEventCallback } from "./doc"; +import { ConfigUtils } from "../../config/src/ConfigUtils"; + +export class ImperativeEventEmitter { + private static mInstance: ImperativeEventEmitter; + private static initialized = false; + private subscriptions: Map; + private eventTimes: Map; + public appName: string; + public logger: Logger; + + public static initialize(appName?: string, options?: IImperativeEventEmitterOpts) { + if (this.initialized) { + throw new ImperativeError({msg: "Only one instance of the Imperative Event Emitter is allowed"}); + } + this.initialized = true; + + if (ImperativeConfig.instance.loadedConfig == null || LoggerManager.instance.isLoggerInit === false) { + ConfigUtils.initImpUtils("zowe"); + } + + this.instance.appName = appName; + this.instance.logger = options?.logger ?? Logger.getImperativeLogger(); + } + + public static teardown() { + if (!this.initialized) { + return; + } + + for (const sub of [...this.instance.subscriptions.keys()]) { + this.instance.unsubscribe(sub); + } + + this.initialized = false; + } + + public static get instance(): ImperativeEventEmitter { + if (this.mInstance == null) { + this.mInstance = new ImperativeEventEmitter(); + this.mInstance.subscriptions = new Map(); + this.mInstance.eventTimes = new Map(); + } + return this.mInstance; + } + + /** + * Check to see if the Imperative Event Emitter instance has been initialized + */ + private ensureClassInitialized() { + if (!ImperativeEventEmitter.initialized) { + throw new ImperativeError({msg: "You must initialize the instance before using any of its methods."}); + } + } + + /** + * Check to see if the directory exists, otherwise, create it : ) + * @param directoryPath Zowe or User dir where we will write the events + */ + private ensureEventsDirExists(directoryPath: string) { + try { + if (!fs.existsSync(directoryPath)) { + fs.mkdirSync(directoryPath); + } + } catch (err) { + throw new ImperativeError({ msg: `Unable to create '.events' directory. Path: ${directoryPath}`, causeErrors: err }); + } + } + + /** + * Check to see if the file path exists, otherwise, create it : ) + * @param filePath Zowe or User path where we will write the events + */ + private ensureEventFileExists(filePath: string) { + try { + if (!fs.existsSync(filePath)) { + fs.closeSync(fs.openSync(filePath, 'w')); + } + } catch (err) { + throw new ImperativeError({ msg: `Unable to create event file. Path: ${filePath}`, causeErrors: err }); + } + } + + /** + * Helper method to initialize the event + * @param eventName The type of event to initialize + * @returns The initialized ImperativeEvent + */ + private initEvent(eventName: ImperativeEventType | string): ImperativeEvent { + this.ensureClassInitialized(); + return new ImperativeEvent({ appName: this.appName, eventName, isUser: this.isUserEvent(eventName), logger: this.logger }); + } + + /** + * Helper method to write contents out to disk + * @param location directory to write the file (i.e. emit the event) + * @param event the event to be written/emitted + * @internal We do not want developers writing events directly, they should use the `emit...` methods + */ + private writeEvent(location: string, event: ImperativeEvent) { + event.location = location; + this.ensureEventsDirExists(location); + fs.writeFileSync(join(location, event.type), JSON.stringify(event.toJson(), null, 2)); + } + + /** + * Helper method to create watchers based on event strings and list of callbacks + * @param eventName type of event to which we will create a watcher for + * @param callbacks list of all callbacks for this watcher + * @returns The FSWatcher instance created + */ + private setupWatcher(eventName: string, callbacks: ImperativeEventCallback[] = []): fs.FSWatcher { + const dir = this.getEventDir(eventName); + this.ensureEventsDirExists(dir); + this.ensureEventFileExists(join(dir, eventName)); + + const watcher = fs.watch(join(dir, eventName), (event: "rename" | "change") => { + // Node.JS triggers this event 3 times + const eventContents = this.getEventContents(eventName); + const eventTime = eventContents.length === 0 ? "" : (JSON.parse(eventContents) as IImperativeEventJson).time; + + if (this.eventTimes.get(eventName) !== eventTime) { + this.logger.debug(`ImperativeEventEmitter: Event "${event}" emitted: ${eventName}`); + // Promise.all(callbacks) + callbacks.forEach(cb => cb()); + this.eventTimes.set(eventName, eventTime); + } + }); + this.subscriptions.set(eventName, [watcher, callbacks]); + return watcher; + } + + /** + * Check to see if the given event is a User event + * @param eventName A string representing the type of event + * @returns True if it is a user event, false otherwise + */ + public isUserEvent(eventName: string): eventName is ImperativeEventType { + return Object.values(ImperativeUserEvents).includes(eventName); + } + + /** + * Check to see if the given event is a shared event + * @param eventName A string representing the type of event + * @returns True if it is a shared event, false otherwise + */ + public isSharedEvent(eventName: string): eventName is ImperativeEventType { + return Object.values(ImperativeSharedEvents).includes(eventName); + } + + /** + * Check to see if the given event is a Custom event + * @param eventName A string representing the type of event + * @returns True if it is not a zowe or a user event, false otherwise + * @internal Not implemented in the MVP + */ + public isCustomEvent(eventName: string): eventName is ImperativeEventType { + return !this.isUserEvent(eventName) && !this.isSharedEvent(eventName); + } + + /** + * ZOWE HOME directory to search for system wide ImperativeEvents like `configChanged` + */ + private getSharedEventDir(): string { + return join(ImperativeConfig.instance.cliHome, ".events"); + } + + /** + * USER HOME directory to search for user specific ImperativeEvents like `vaultChanged` + */ + private getUserEventDir(): string { + return join(homedir(), ".zowe", ".events"); + } + + /** + * Obtain the directory of the event + * @param eventName The type of event to be emitted + * @returns The directory to where this event will be emitted + */ + public getEventDir(eventName: string): string { + if (this.isUserEvent(eventName)) { + return this.getUserEventDir(); + } else if (this.isSharedEvent(eventName)) { + return this.getSharedEventDir(); + } + + return this.getSharedEventDir(); + } + + /** + * Obtain the contents of the event + * @param eventName The type of event to retrieve contents from + * @returns The contents of the event + * @internal + */ + public getEventContents(eventName: string): string { + const eventLoc = join(this.getEventDir(eventName), eventName); + if (!fs.existsSync(eventLoc)) return ""; + return fs.readFileSync(eventLoc).toString(); + } + + /** + * Simple method to write the events to disk + * @param eventName The type of event to write + * @internal We do not want to make this function accessible to any application developers + */ + public emitEvent(eventName: ImperativeEventType) { + const theEvent = this.initEvent(eventName); + + if (this.isCustomEvent(eventName)) { + throw new ImperativeError({ msg: `Unable to determine the type of event. Event: ${eventName}` }); + } + + this.writeEvent(this.getEventDir(eventName), theEvent); + } + + /** + * Simple method to write the events to disk + * @param eventName The type of event to write + * @internal We won't support custom events as part of the MVP + */ + public emitCustomEvent(eventName: string) { //, isUserSpecific: boolean = false) { + const theEvent = this.initEvent(eventName); + + if (!this.isCustomEvent(eventName)) { + throw new ImperativeError({ msg: `Operation not allowed. Event is considered protected. Event: ${eventName}` }); + } + + this.writeEvent(this.getSharedEventDir(), theEvent); + } + + /** + * Method to register your custom actions based on when the given event is emitted + * @param eventName Type of event to register + * @param callback Action to be registered to the given event + */ + public subscribe(eventName: string, callback: ImperativeEventCallback): IImperativeRegisteredAction { + this.ensureClassInitialized(); + + let watcher: fs.FSWatcher; + if (this.subscriptions.get(eventName) != null) { + const [watcherToClose, callbacks] = this.subscriptions.get(eventName); + watcherToClose.removeAllListeners(eventName).close(); + + watcher = this.setupWatcher(eventName, [...callbacks, callback]); + } else { + watcher = this.setupWatcher(eventName, [callback]); + } + return { close: watcher.close }; + } + + /** + * Method to unsubscribe from custom and regular events + * @param eventName Type of registered event + */ + public unsubscribe(eventName: string): void { + this.ensureClassInitialized(); + + if (this.subscriptions.has(eventName)) { + const [watcherToClose, _callbacks] = this.subscriptions.get(eventName); + watcherToClose.removeAllListeners(eventName).close(); + this.subscriptions.delete(eventName); + } + if (this.eventTimes.has(eventName)) { + this.eventTimes.delete(eventName); + } + } +} \ No newline at end of file diff --git a/packages/imperative/src/events/src/doc/IImperativeEventEmitterOpts.ts b/packages/imperative/src/events/src/doc/IImperativeEventEmitterOpts.ts new file mode 100644 index 0000000000..2f9a20c223 --- /dev/null +++ b/packages/imperative/src/events/src/doc/IImperativeEventEmitterOpts.ts @@ -0,0 +1,26 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { Logger } from "../../../logger"; + +/** + * Imperative standard event emitter options + * @export + * @interface IImperativeEventEmitterOpts + */ +export interface IImperativeEventEmitterOpts { + /** + * The logger to use when logging the imperative event that occurred + * @type {Logger} + * @memberof IImperativeEventEmitterOpts + */ + logger?: Logger; +} diff --git a/packages/imperative/src/events/src/doc/IImperativeEventJson.ts b/packages/imperative/src/events/src/doc/IImperativeEventJson.ts new file mode 100644 index 0000000000..b433c49ec7 --- /dev/null +++ b/packages/imperative/src/events/src/doc/IImperativeEventJson.ts @@ -0,0 +1,44 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +/** + * Imperative Event JSON representation + * @export + * @interface IImperativeEventJson + */ +export interface IImperativeEventJson { + /** + * The time in which the event occurred + */ + time: string; + /** + * The type of event that occurred + */ + type: string; + /** + * The application name that triggered the event + */ + source: string; + /** + * The ID of the event that occurred + */ + id?: string; + /** + * The location in which the event was emitted (User vs Shared) + */ + loc?: string; + /** + * The indicator of user-specific (if true) or shared (if false) events + */ + user?: boolean; +} + +export type ImperativeEventCallback = () => void | PromiseLike; diff --git a/packages/imperative/src/events/src/doc/IImperativeEventParms.ts b/packages/imperative/src/events/src/doc/IImperativeEventParms.ts new file mode 100644 index 0000000000..3a9568b694 --- /dev/null +++ b/packages/imperative/src/events/src/doc/IImperativeEventParms.ts @@ -0,0 +1,45 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { Logger } from "../../../logger"; +import { ImperativeEventType } from "../ImperativeEventConstants"; + +/** + * Imperative Standard Event + * @export + * @interface IImperativeEventParms + */ +export interface IImperativeEventParms { + /** + * The name of the application to be used to generate a unique ID for the event + * @type {string} + * @memberof IImperativeEventParms + */ + appName: string; + /** + * The type of imperative event that occurred + * @type {ImperativeEventType} + * @memberof IImperativeEventParms + */ + eventName: ImperativeEventType | string; + /** + * Specifies whether this is a user event or not + * @type {ImperativeEventType} + * @memberof IImperativeEventParms + */ + isUser: boolean; + /** + * The logger to use when logging the imperative event that occurred + * @type {Logger} + * @memberof IImperativeEventParms + */ + logger: Logger; +} diff --git a/packages/imperative/src/events/src/doc/IImperativeRegisteredAction.ts b/packages/imperative/src/events/src/doc/IImperativeRegisteredAction.ts new file mode 100644 index 0000000000..3f06b3f821 --- /dev/null +++ b/packages/imperative/src/events/src/doc/IImperativeRegisteredAction.ts @@ -0,0 +1,23 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +/** + * Imperative Registered Action + * @export + * @interface IImperativeRegisteredAction + */ +export interface IImperativeRegisteredAction { + /** + * The method to dispose of the registered action + * @memberof IImperativeRegisteredAction + */ + close(): void; +} diff --git a/packages/imperative/src/events/src/doc/index.ts b/packages/imperative/src/events/src/doc/index.ts new file mode 100644 index 0000000000..22eb9c922e --- /dev/null +++ b/packages/imperative/src/events/src/doc/index.ts @@ -0,0 +1,15 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +export * from "./IImperativeEventEmitterOpts"; +export * from "./IImperativeEventParms"; +export * from "./IImperativeRegisteredAction"; +export * from "./IImperativeEventJson"; diff --git a/packages/imperative/src/imperative/__tests__/config/cmd/import/import.handler.unit.test.ts b/packages/imperative/src/imperative/__tests__/config/cmd/import/import.handler.unit.test.ts index 671bab25e8..17bac7fa95 100644 --- a/packages/imperative/src/imperative/__tests__/config/cmd/import/import.handler.unit.test.ts +++ b/packages/imperative/src/imperative/__tests__/config/cmd/import/import.handler.unit.test.ts @@ -23,6 +23,7 @@ import { expectedConfigObject, expectedSchemaObject } from "../../../../../../__tests__/__integration__/imperative/__tests__/__integration__/cli/config/__resources__/expectedObjects"; jest.mock("fs"); +jest.mock("../../../../../events/src/ImperativeEventEmitter"); const expectedConfigText = JSONC.stringify(expectedConfigObject, null, ConfigConstants.INDENT); const expectedConfigObjectWithoutSchema = lodash.omit(expectedConfigObject, "$schema"); diff --git a/packages/imperative/src/imperative/__tests__/config/cmd/init/init.handler.unit.test.ts b/packages/imperative/src/imperative/__tests__/config/cmd/init/init.handler.unit.test.ts index 83249761c7..d54d6f24a6 100644 --- a/packages/imperative/src/imperative/__tests__/config/cmd/init/init.handler.unit.test.ts +++ b/packages/imperative/src/imperative/__tests__/config/cmd/init/init.handler.unit.test.ts @@ -26,6 +26,7 @@ import { setupConfigToLoad } from "../../../../../../__tests__/src/TestUtil"; import { OverridesLoader } from "../../../../src/OverridesLoader"; jest.mock("fs"); +jest.mock("../../../../../events/src/ImperativeEventEmitter"); const getIHandlerParametersObject = (): IHandlerParameters => { const x: any = { diff --git a/packages/imperative/src/imperative/__tests__/config/cmd/secure/secure.handler.unit.test.ts b/packages/imperative/src/imperative/__tests__/config/cmd/secure/secure.handler.unit.test.ts index eaf068137e..89c4aa879c 100644 --- a/packages/imperative/src/imperative/__tests__/config/cmd/secure/secure.handler.unit.test.ts +++ b/packages/imperative/src/imperative/__tests__/config/cmd/secure/secure.handler.unit.test.ts @@ -9,7 +9,7 @@ * */ -import { IHandlerParameters, Logger } from "../../../../.."; +import { IHandlerParameters, ImperativeEventEmitter, Logger } from "../../../../.."; import { Config } from "../../../../../config/src/Config"; import { IConfig, IConfigOpts, IConfigProfile } from "../../../../../config"; import { ImperativeConfig } from "../../../../../utilities"; @@ -27,6 +27,8 @@ import * as fs from "fs"; import { SessConstants } from "../../../../../rest"; import { setupConfigToLoad } from "../../../../../../__tests__/src/TestUtil"; +jest.mock("../../../../../events/src/ImperativeEventEmitter"); + let readPromptSpy: any; const getIHandlerParametersObject = (): IHandlerParameters => { const x: any = { @@ -101,6 +103,7 @@ describe("Configuration Secure command handler", () => { }; beforeAll( async() => { + Object.defineProperty(ImperativeEventEmitter, "instance", { value: { emitEvent: jest.fn() }, configurable: true}); keytarGetPasswordSpy = jest.spyOn(keytar, "getPassword"); keytarSetPasswordSpy = jest.spyOn(keytar, "setPassword"); keytarDeletePasswordSpy = jest.spyOn(keytar, "deletePassword"); diff --git a/packages/imperative/src/imperative/__tests__/config/cmd/set/set.handler.unit.test.ts b/packages/imperative/src/imperative/__tests__/config/cmd/set/set.handler.unit.test.ts index d060bcb3f4..08613f6777 100644 --- a/packages/imperative/src/imperative/__tests__/config/cmd/set/set.handler.unit.test.ts +++ b/packages/imperative/src/imperative/__tests__/config/cmd/set/set.handler.unit.test.ts @@ -9,7 +9,7 @@ * */ -import { IHandlerParameters } from "../../../../.."; +import { IHandlerParameters, ImperativeEventEmitter } from "../../../../.."; import { Config } from "../../../../../config/src/Config"; import { IConfigOpts } from "../../../../../config"; import { ImperativeConfig } from "../../../../../utilities"; @@ -26,6 +26,9 @@ import * as lodash from "lodash"; import * as fs from "fs"; import { setupConfigToLoad } from "../../../../../../__tests__/src/TestUtil"; +jest.mock("../../../../../events/src/ImperativeEventEmitter"); + + const getIHandlerParametersObject = (): IHandlerParameters => { const x: any = { response: { @@ -95,6 +98,7 @@ describe("Configuration Set command handler", () => { }; beforeAll( async() => { + Object.defineProperty(ImperativeEventEmitter, "instance", { value: { emitEvent: jest.fn() }, configurable: true}); keytarGetPasswordSpy = jest.spyOn(keytar, "getPassword"); keytarSetPasswordSpy = jest.spyOn(keytar, "setPassword"); keytarDeletePasswordSpy = jest.spyOn(keytar, "deletePassword"); diff --git a/packages/imperative/src/imperative/src/auth/__tests__/BaseAuthHandler.config.unit.test.ts b/packages/imperative/src/imperative/src/auth/__tests__/BaseAuthHandler.config.unit.test.ts index 5ba1bd2e25..cf13d6e108 100644 --- a/packages/imperative/src/imperative/src/auth/__tests__/BaseAuthHandler.config.unit.test.ts +++ b/packages/imperative/src/imperative/src/auth/__tests__/BaseAuthHandler.config.unit.test.ts @@ -10,6 +10,8 @@ */ jest.mock("../../../../logger/src/LoggerUtils"); +jest.mock("../../../../events/src/ImperativeEventEmitter"); + import * as fs from "fs"; import * as path from "path"; import * as lodash from "lodash"; @@ -20,7 +22,7 @@ import { Config } from "../../../../config"; import { IConfigSecure } from "../../../../config/src/doc/IConfigSecure"; import FakeAuthHandler from "./__data__/FakeAuthHandler"; import { CredentialManagerFactory } from "../../../../security"; -import { ImperativeError } from "../../../.."; +import { ImperativeError, ImperativeEventEmitter } from "../../../.."; const MY_APP = "my_app"; @@ -36,6 +38,7 @@ describe("BaseAuthHandler config", () => { let fakeConfig: Config; beforeAll(() => { + Object.defineProperty(ImperativeEventEmitter, "instance", { value: { emitEvent: jest.fn() }}); Object.defineProperty(CredentialManagerFactory, "initialized", { get: () => true }); Object.defineProperty(ImperativeConfig, "instance", { get: () => ({ diff --git a/packages/imperative/src/index.ts b/packages/imperative/src/index.ts index 334969ff98..08493bc401 100644 --- a/packages/imperative/src/index.ts +++ b/packages/imperative/src/index.ts @@ -13,6 +13,7 @@ export * from "./cmd"; export * from "./config"; export * from "./console"; export * from "./constants"; +export * from "./events"; export * from "./error"; export * from "./expect"; export * from "./imperative"; diff --git a/packages/imperative/src/rest/__tests__/session/ConnectionPropsForSessCfg.unit.test.ts b/packages/imperative/src/rest/__tests__/session/ConnectionPropsForSessCfg.unit.test.ts index c17fa61088..717be13b89 100644 --- a/packages/imperative/src/rest/__tests__/session/ConnectionPropsForSessCfg.unit.test.ts +++ b/packages/imperative/src/rest/__tests__/session/ConnectionPropsForSessCfg.unit.test.ts @@ -10,6 +10,8 @@ */ jest.mock("../../../logger/src/LoggerUtils"); +jest.mock("../../../events/src/ImperativeEventEmitter"); + import { ConnectionPropsForSessCfg } from "../../src/session/ConnectionPropsForSessCfg"; import { CliUtils } from "../../../utilities/src/CliUtils"; import { ImperativeError } from "../../../error"; diff --git a/packages/imperative/src/security/__tests__/CredentialManagerOverride.unit.test.ts b/packages/imperative/src/security/__tests__/CredentialManagerOverride.unit.test.ts index 2433cc66bb..0a7a6f71d1 100644 --- a/packages/imperative/src/security/__tests__/CredentialManagerOverride.unit.test.ts +++ b/packages/imperative/src/security/__tests__/CredentialManagerOverride.unit.test.ts @@ -17,6 +17,10 @@ import { ICredentialManagerNameMap } from "../src/doc/ICredentialManagerNameMap" import { ImperativeConfig } from "../../utilities"; import { ImperativeError } from "../../error"; import { ISettingsFile } from "../../settings/src/doc/ISettingsFile"; +import { ImperativeEventEmitter } from "../../events"; + + +jest.mock("../../events/src/ImperativeEventEmitter"); describe("CredentialManagerOverride", () => { let mockImpConfig: any; @@ -28,6 +32,7 @@ describe("CredentialManagerOverride", () => { cliHome: __dirname }; jest.spyOn(ImperativeConfig, "instance", "get").mockReturnValue(mockImpConfig); + Object.defineProperty(ImperativeEventEmitter, "instance", { value: { emitEvent: jest.fn() }, configurable: true}); expectedSettings = { fileName: path.join(mockImpConfig.cliHome, "settings", "imperative.json"), diff --git a/packages/imperative/src/security/src/CredentialManagerOverride.ts b/packages/imperative/src/security/src/CredentialManagerOverride.ts index 60459512be..ca66b4a64e 100644 --- a/packages/imperative/src/security/src/CredentialManagerOverride.ts +++ b/packages/imperative/src/security/src/CredentialManagerOverride.ts @@ -16,6 +16,7 @@ import { ICredentialManagerNameMap } from "./doc/ICredentialManagerNameMap"; import { ImperativeConfig } from "../../utilities"; import { ImperativeError } from "../../error"; import { ISettingsFile } from "../../settings/src/doc/ISettingsFile"; +import { ImperativeEventEmitter, ImperativeSharedEvents } from "../../events"; /** * This class provides access to the known set of credential manager overrides @@ -132,6 +133,7 @@ export class CredentialManagerOverride { settings.json.overrides.CredentialManager = newCredMgrName; try { writeJsonSync(settings.fileName, settings.json, {spaces: 2}); + ImperativeEventEmitter.instance.emitEvent(ImperativeSharedEvents.ON_CREDENTIAL_MANAGER_CHANGED); } catch (error) { throw new ImperativeError({ msg: "Unable to write settings file = " + settings.fileName + @@ -186,6 +188,7 @@ export class CredentialManagerOverride { settings.json.overrides.CredentialManager = this.DEFAULT_CRED_MGR_NAME; try { writeJsonSync(settings.fileName, settings.json, {spaces: 2}); + ImperativeEventEmitter.instance.emitEvent(ImperativeSharedEvents.ON_CREDENTIAL_MANAGER_CHANGED); } catch (error) { throw new ImperativeError({ msg: "Unable to write settings file = " + settings.fileName + diff --git a/packages/imperative/tsconfig.json b/packages/imperative/tsconfig.json index fdfdb95a1d..511d8c8e52 100644 --- a/packages/imperative/tsconfig.json +++ b/packages/imperative/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./lib" + "outDir": "./lib", + "stripInternal": true }, "include": [ "./src"