diff --git a/packages/cli/__tests__/zosfiles/__unit__/search/ds/Datasets.handler.unit.test.ts b/packages/cli/__tests__/zosfiles/__unit__/search/ds/Datasets.handler.unit.test.ts index f9026ae836..d0444e9434 100644 --- a/packages/cli/__tests__/zosfiles/__unit__/search/ds/Datasets.handler.unit.test.ts +++ b/packages/cli/__tests__/zosfiles/__unit__/search/ds/Datasets.handler.unit.test.ts @@ -29,7 +29,7 @@ describe("Search Datasets handler", () => { let logMessage = ""; let fakeSession = null; - // Mock the submit JCL function + // Mock the search datasets function Search.dataSets = jest.fn(async (session) => { fakeSession = session; return { @@ -37,8 +37,8 @@ describe("Search Datasets handler", () => { commandResponse: "Found \"test\" in 2 data sets and PDS members", apiResponse: [ { - dsname: "TEST1.DS", - memname: "TESTMEM", + dsn: "TEST1.DS", + member: "TESTMEM", matchList: [ { line: 1, @@ -48,8 +48,8 @@ describe("Search Datasets handler", () => { ] }, { - dsname: "TEST2.DS", - memname: undefined, + dsn: "TEST2.DS", + member: undefined, matchList: [ { line: 1, @@ -139,8 +139,8 @@ describe("Search Datasets handler", () => { commandResponse: "Found \"test\" in 2 data sets and PDS members", apiResponse: [ { - dsname: "TEST1.DS", - memname: "TESTMEM", + dsn: "TEST1.DS", + member: "TESTMEM", matchList: [ { line: 1, @@ -150,8 +150,8 @@ describe("Search Datasets handler", () => { ] }, { - dsname: "TEST2.DS", - memname: undefined, + dsn: "TEST2.DS", + member: undefined, matchList: [ { line: 1, @@ -242,8 +242,8 @@ describe("Search Datasets handler", () => { commandResponse: "Found \"test\" in 2 data sets and PDS members", apiResponse: [ { - dsname: "TEST1.DS", - memname: "TESTMEM", + dsn: "TEST1.DS", + member: "TESTMEM", matchList: [ { line: 1, @@ -253,8 +253,8 @@ describe("Search Datasets handler", () => { ] }, { - dsname: "TEST2.DS", - memname: undefined, + dsn: "TEST2.DS", + member: undefined, matchList: [ { line: 1, @@ -345,8 +345,8 @@ describe("Search Datasets handler", () => { commandResponse: "Found \"test\" in 2 data sets and PDS members", apiResponse: [ { - dsname: "TEST1.DS", - memname: "TESTMEM", + dsn: "TEST1.DS", + member: "TESTMEM", matchList: [ { line: 1, @@ -356,8 +356,8 @@ describe("Search Datasets handler", () => { ] }, { - dsname: "TEST2.DS", - memname: undefined, + dsn: "TEST2.DS", + member: undefined, matchList: [ { line: 1, @@ -448,8 +448,8 @@ describe("Search Datasets handler", () => { commandResponse: "Found \"test\" in 2 data sets and PDS members", apiResponse: [ { - dsname: "TEST1.DS", - memname: "TESTMEM", + dsn: "TEST1.DS", + member: "TESTMEM", matchList: [ { line: 1, @@ -459,8 +459,8 @@ describe("Search Datasets handler", () => { ] }, { - dsname: "TEST2.DS", - memname: undefined, + dsn: "TEST2.DS", + member: undefined, matchList: [ { line: 1, @@ -551,8 +551,8 @@ describe("Search Datasets handler", () => { commandResponse: "Found \"test\" in 2 data sets and PDS members", apiResponse: [ { - dsname: "TEST1.DS", - memname: "TESTMEM", + dsn: "TEST1.DS", + member: "TESTMEM", matchList: [ { line: 1, @@ -562,8 +562,8 @@ describe("Search Datasets handler", () => { ] }, { - dsname: "TEST2.DS", - memname: undefined, + dsn: "TEST2.DS", + member: undefined, matchList: [ { line: 1, diff --git a/packages/cli/__tests__/zosfiles/__unit__/search/ds/__snapshots__/Datasets.handler.unit.test.ts.snap b/packages/cli/__tests__/zosfiles/__unit__/search/ds/__snapshots__/Datasets.handler.unit.test.ts.snap index 16cfd76fb2..496cfc0b0d 100644 --- a/packages/cli/__tests__/zosfiles/__unit__/search/ds/__snapshots__/Datasets.handler.unit.test.ts.snap +++ b/packages/cli/__tests__/zosfiles/__unit__/search/ds/__snapshots__/Datasets.handler.unit.test.ts.snap @@ -4,7 +4,7 @@ exports[`Search Datasets handler process method should search a data set if requ Object { "apiResponse": Array [ Object { - "dsname": "TEST1.DS", + "dsn": "TEST1.DS", "matchList": Array [ Object { "column": 1, @@ -12,10 +12,10 @@ Object { "line": 1, }, ], - "memname": "TESTMEM", + "member": "TESTMEM", }, Object { - "dsname": "TEST2.DS", + "dsn": "TEST2.DS", "matchList": Array [ Object { "column": 1, @@ -23,7 +23,7 @@ Object { "line": 1, }, ], - "memname": undefined, + "member": undefined, }, ], "commandResponse": "Found \\"test\\" in 2 data sets and PDS members", @@ -42,7 +42,7 @@ exports[`Search Datasets handler process method should search a data set if requ Object { "apiResponse": Array [ Object { - "dsname": "TEST1.DS", + "dsn": "TEST1.DS", "matchList": Array [ Object { "column": 1, @@ -50,10 +50,10 @@ Object { "line": 1, }, ], - "memname": "TESTMEM", + "member": "TESTMEM", }, Object { - "dsname": "TEST2.DS", + "dsn": "TEST2.DS", "matchList": Array [ Object { "column": 1, @@ -61,7 +61,7 @@ Object { "line": 1, }, ], - "memname": undefined, + "member": undefined, }, ], "commandResponse": "Found \\"test\\" in 2 data sets and PDS members", @@ -80,7 +80,7 @@ exports[`Search Datasets handler process method should search a data set if requ Object { "apiResponse": Array [ Object { - "dsname": "TEST1.DS", + "dsn": "TEST1.DS", "matchList": Array [ Object { "column": 1, @@ -88,10 +88,10 @@ Object { "line": 1, }, ], - "memname": "TESTMEM", + "member": "TESTMEM", }, Object { - "dsname": "TEST2.DS", + "dsn": "TEST2.DS", "matchList": Array [ Object { "column": 1, @@ -99,7 +99,7 @@ Object { "line": 1, }, ], - "memname": undefined, + "member": undefined, }, ], "commandResponse": "Found \\"test\\" in 2 data sets and PDS members", @@ -118,7 +118,7 @@ exports[`Search Datasets handler process method should search a data set if requ Object { "apiResponse": Array [ Object { - "dsname": "TEST1.DS", + "dsn": "TEST1.DS", "matchList": Array [ Object { "column": 1, @@ -126,10 +126,10 @@ Object { "line": 1, }, ], - "memname": "TESTMEM", + "member": "TESTMEM", }, Object { - "dsname": "TEST2.DS", + "dsn": "TEST2.DS", "matchList": Array [ Object { "column": 1, @@ -137,7 +137,7 @@ Object { "line": 1, }, ], - "memname": undefined, + "member": undefined, }, ], "commandResponse": "Found \\"test\\" in 2 data sets and PDS members", @@ -156,7 +156,7 @@ exports[`Search Datasets handler process method should search a data set if requ Object { "apiResponse": Array [ Object { - "dsname": "TEST1.DS", + "dsn": "TEST1.DS", "matchList": Array [ Object { "column": 1, @@ -164,10 +164,10 @@ Object { "line": 1, }, ], - "memname": "TESTMEM", + "member": "TESTMEM", }, Object { - "dsname": "TEST2.DS", + "dsn": "TEST2.DS", "matchList": Array [ Object { "column": 1, @@ -175,7 +175,7 @@ Object { "line": 1, }, ], - "memname": undefined, + "member": undefined, }, ], "commandResponse": "Found \\"test\\" in 2 data sets and PDS members", @@ -194,7 +194,7 @@ exports[`Search Datasets handler process method should search a data set if requ Object { "apiResponse": Array [ Object { - "dsname": "TEST1.DS", + "dsn": "TEST1.DS", "matchList": Array [ Object { "column": 1, @@ -202,10 +202,10 @@ Object { "line": 1, }, ], - "memname": "TESTMEM", + "member": "TESTMEM", }, Object { - "dsname": "TEST2.DS", + "dsn": "TEST2.DS", "matchList": Array [ Object { "column": 1, @@ -213,7 +213,7 @@ Object { "line": 1, }, ], - "memname": undefined, + "member": undefined, }, ], "commandResponse": "Found \\"test\\" in 2 data sets and PDS members", diff --git a/packages/zosfiles/CHANGELOG.md b/packages/zosfiles/CHANGELOG.md index 264c7f60c0..d6ea06dc9d 100644 --- a/packages/zosfiles/CHANGELOG.md +++ b/packages/zosfiles/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the Zowe z/OS files SDK package will be documented in this file. +## Recent Changes + +- Enhancement: Allows extenders of the Search functionality to pass a function `abortSearch` on `searchOptions` to abort a search. [#2370](https://github.com/zowe/zowe-cli/pull/2370) + ## `8.8.3` - BugFix: Resolved issue where special characters could be corrupted when downloading a large file. [#2366](https://github.com/zowe/zowe-cli/pull/2366) diff --git a/packages/zosfiles/__tests__/__system__/methods/search/Search.system.test.ts b/packages/zosfiles/__tests__/__system__/methods/search/Search.system.test.ts index f982c81787..c4ea458f1f 100644 --- a/packages/zosfiles/__tests__/__system__/methods/search/Search.system.test.ts +++ b/packages/zosfiles/__tests__/__system__/methods/search/Search.system.test.ts @@ -9,11 +9,11 @@ * */ -import { Session } from "@zowe/imperative"; +import { AbstractSession, Session } from "@zowe/imperative"; import { TestEnvironment } from "../../../../../../__tests__/__src__/environment/TestEnvironment"; import { ITestPropertiesSchema } from "../../../../../../__tests__/__src__/properties/ITestPropertiesSchema"; import { getUniqueDatasetName } from "../../../../../../__tests__/__src__/TestUtils"; -import { Create, Upload, Delete, Search, CreateDataSetTypeEnum, ISearchOptions, IZosFilesResponse } from "../../../../src"; +import { Create, Upload, Delete, Search, CreateDataSetTypeEnum, ISearchOptions, IZosFilesResponse, Get, IGetOptions } from "../../../../src"; import { ITestEnvironment } from "../../../../../../__tests__/__src__/environment/ITestEnvironment"; let REAL_SESSION: Session; @@ -93,6 +93,7 @@ describe("Search", () => { for (const dsn of [...goodDsNames, ...badDsNames, ...pdsNames]) { await Delete.dataSet(REAL_SESSION, dsn); } + jest.restoreAllMocks(); }); beforeEach(() => { @@ -104,7 +105,8 @@ describe("Search", () => { mainframeSearch: undefined, progressTask: undefined, maxConcurrentRequests: undefined, - timeout: undefined + timeout: undefined, + abortSearch: undefined }; expectedApiResponse = [ @@ -116,6 +118,8 @@ describe("Search", () => { {dsn: `${dsnPrefix}.SEQ4`, matchList: [{line: 1, column: 39, contents: goodTestString}]}, {dsn: `${dsnPrefix}.SEQ5`, matchList: [{line: 1, column: 39, contents: goodTestString}]}, ]; + + jest.restoreAllMocks(); }); it("should search and find the correct data sets", async () => { @@ -236,6 +240,36 @@ describe("Search", () => { expect(response.errorMessage).toContain("The following data set(s) failed to be searched:"); }); + it("should abort when requested", async () => { + let count = 0; + let abort = false; + function abortFn () { return abort; } + const realGet = jest.requireActual("../../../../src/methods/get/Get"); + searchOptions.abortSearch = abortFn; + + const getDataSetSpy = jest.spyOn(Get, "dataSet"); + getDataSetSpy.mockImplementation((session: AbstractSession, dataSetName: string, options: IGetOptions) => { + count++; + if (count > 3) { + abort = true; + } + return realGet.dataSet(session, dataSetName, options); + }); + + const response = await Search.dataSets(REAL_SESSION, searchOptions); + + /** + * Since this test is timeout based, we cannot make many assumptions about what will or will not be found. + * The safest assumption is that something may or may not be found, but we will not find everything + * in under one second. + */ + expect(response.success).toEqual(false); + expect(response.commandResponse).toContain(`cancelled`); + expect(response.commandResponse).toContain(`Found "${searchString}" in`); + expect(response.commandResponse).toContain(`data sets and PDS members`); + expect(response.errorMessage).toContain("The following data set(s) failed to be searched:"); + }); + it("should fail without a pattern to search for", async () => { searchOptions.pattern = undefined; let error: any; diff --git a/packages/zosfiles/__tests__/__unit__/methods/search/Search.unit.test.ts b/packages/zosfiles/__tests__/__unit__/methods/search/Search.unit.test.ts index de4bb9f4db..8266d5a357 100644 --- a/packages/zosfiles/__tests__/__unit__/methods/search/Search.unit.test.ts +++ b/packages/zosfiles/__tests__/__unit__/methods/search/Search.unit.test.ts @@ -38,6 +38,8 @@ describe("Search", () => { progressTask: undefined, maxConcurrentRequests: 1, timeout: undefined, + continueSearch: undefined, + abortSearch: undefined }; let searchItems: ISearchItem[] = [ {dsn: "TEST1.DS", member: undefined, matchList: undefined}, @@ -95,7 +97,8 @@ describe("Search", () => { progressTask: undefined, maxConcurrentRequests: 1, timeout: undefined, - continueSearch: undefined + continueSearch: undefined, + abortSearch: undefined }; searchItems = [ @@ -128,8 +131,8 @@ describe("Search", () => { function delay(ms: number) { jest.advanceTimersByTime(ms); } function regenerateMockImplementations() { - searchOnMainframeSpy.mockImplementation(async (session, searchOptions, searchItems: ISearchItem[]) => { - if ((Search as any).timerExpired != true) { + searchOnMainframeSpy.mockImplementation(async (session, searchOptions: ISearchOptions, searchItems: ISearchItem[]) => { + if ((Search as any).timerExpired != true && !(searchOptions.abortSearch && searchOptions.abortSearch())) { return { responses: searchItems, failures: [] @@ -143,8 +146,8 @@ describe("Search", () => { return {responses: [], failures}; } }); - searchLocalSpy.mockImplementation(async (session, searchOptions, searchItems: ISearchItem[]) => { - if ((Search as any).timerExpired != true) { + searchLocalSpy.mockImplementation(async (session, searchOptions: ISearchOptions, searchItems: ISearchItem[]) => { + if ((Search as any).timerExpired != true && !(searchOptions.abortSearch && searchOptions.abortSearch())) { const searchItemArray: ISearchItem[] = []; for (const searchItem of searchItems) { const localSearchItem: ISearchItem = searchItem; @@ -586,6 +589,107 @@ describe("Search", () => { expect(response.commandResponse).toContain("The search was cancelled."); }); + it("Should handle an abort that returns true 1", async () => { + testDataString = "TESTDATA IS AT THE BEGINNING OF THE STRING"; + expectedCol = 1; + expectedLine = 1; + regenerateMockImplementations(); + searchOptions.abortSearch = function fakeAbort() { + return true; + }; + + const response = await Search.dataSets(dummySession, searchOptions); + + expect(listDataSetsMatchingPatternSpy).toHaveBeenCalledTimes(1); + expect(listDataSetsMatchingPatternSpy).toHaveBeenCalledWith(dummySession, ["TEST*"], {maxConcurrentRequests: 1}); + expect(listAllMembersSpy).toHaveBeenCalledTimes(1); + expect(listAllMembersSpy).toHaveBeenCalledWith(dummySession, "TEST3.PDS", {}); + expect(searchOnMainframeSpy).toHaveBeenCalledTimes(1); + expect(searchLocalSpy).toHaveBeenCalledTimes(1); + + expect(response.errorMessage).toEqual("The following data set(s) failed to be searched: " + + "\nTEST1.DS\nTEST2.DS\nTEST3.PDS(MEMBER1)\nTEST3.PDS(MEMBER2)\nTEST3.PDS(MEMBER3)\n"); + expect(response.success).toEqual(false); + expect(response.apiResponse).toEqual([]); + expect(response.commandResponse).toContain("The search was cancelled."); + expect(response.commandResponse).toContain("Found \"TESTDATA\" in 0 data sets and PDS members."); + }); + + it("Should handle an abort that returns true 2", async () => { + testDataString = "TESTDATA IS AT THE BEGINNING OF THE STRING"; + expectedCol = 1; + expectedLine = 1; + regenerateMockImplementations(); + searchOptions.abortSearch = function fakeAbort() { + return true; + }; + searchOptions.progressTask = { + stageName: TaskStage.NOT_STARTED, + percentComplete: 0, + statusMessage: undefined + }; + + const response = await Search.dataSets(dummySession, searchOptions); + + expect(listDataSetsMatchingPatternSpy).toHaveBeenCalledTimes(1); + expect(listDataSetsMatchingPatternSpy).toHaveBeenCalledWith(dummySession, ["TEST*"], {maxConcurrentRequests: 1}); + expect(listAllMembersSpy).toHaveBeenCalledTimes(1); + expect(listAllMembersSpy).toHaveBeenCalledWith(dummySession, "TEST3.PDS", {}); + expect(searchOnMainframeSpy).toHaveBeenCalledTimes(1); + expect(searchLocalSpy).toHaveBeenCalledTimes(1); + + expect(response.errorMessage).toEqual("The following data set(s) failed to be searched: " + + "\nTEST1.DS\nTEST2.DS\nTEST3.PDS(MEMBER1)\nTEST3.PDS(MEMBER2)\nTEST3.PDS(MEMBER3)\n"); + expect(response.success).toEqual(false); + expect(response.apiResponse).toEqual([]); + expect(response.commandResponse).toContain("The search was cancelled."); + expect(response.commandResponse).toContain("Found \"TESTDATA\" in 0 data sets and PDS members."); + + expect(searchOptions.progressTask.percentComplete).toEqual(100); + expect(searchOptions.progressTask.stageName).toEqual(TaskStage.FAILED); + expect(searchOptions.progressTask.statusMessage).toEqual("Operation cancelled"); + }); + + it("Should handle an abort that returns false", async () => { + testDataString = "TESTDATA IS AT THE BEGINNING OF THE STRING"; + expectedCol = 1; + expectedLine = 1; + regenerateMockImplementations(); + searchOptions.abortSearch = function fakeAbort() { + return false; + }; + + const response = await Search.dataSets(dummySession, searchOptions); + + expect(listDataSetsMatchingPatternSpy).toHaveBeenCalledTimes(1); + expect(listDataSetsMatchingPatternSpy).toHaveBeenCalledWith(dummySession, ["TEST*"], {maxConcurrentRequests: 1}); + expect(listAllMembersSpy).toHaveBeenCalledTimes(1); + expect(listAllMembersSpy).toHaveBeenCalledWith(dummySession, "TEST3.PDS", {}); + expect(searchOnMainframeSpy).toHaveBeenCalledTimes(1); + expect(searchLocalSpy).toHaveBeenCalledTimes(1); + + expect(response.errorMessage).not.toBeDefined(); + expect(response.success).toEqual(true); + expect(response.apiResponse).toEqual([ + {dsn: "TEST1.DS", member: undefined, matchList: [{column: expectedCol, line: expectedLine, contents: testDataString}]}, + {dsn: "TEST2.DS", member: undefined, matchList: [{column: expectedCol, line: expectedLine, contents: testDataString}]}, + {dsn: "TEST3.PDS", member: "MEMBER1", matchList: [{column: expectedCol, line: expectedLine, contents: testDataString}]}, + {dsn: "TEST3.PDS", member: "MEMBER2", matchList: [{column: expectedCol, line: expectedLine, contents: testDataString}]}, + {dsn: "TEST3.PDS", member: "MEMBER3", matchList: [{column: expectedCol, line: expectedLine, contents: testDataString}]} + ]); + expect(response.commandResponse).toContain("Found \"TESTDATA\" in 5 data sets and PDS members"); + expect(response.commandResponse).toContain("Data Set \"TEST1.DS\":\nLine: " + + expectedLine + ", Column: " + expectedCol + ", Contents: " + testDataString); + expect(response.commandResponse).toContain("Data Set \"TEST2.DS\":\nLine: " + + expectedLine + ", Column: " + expectedCol + ", Contents: " + testDataString); + expect(response.commandResponse).toContain("Data Set \"TEST3.PDS\" | Member \"MEMBER1\":\nLine: " + + expectedLine + ", Column: " + expectedCol + ", Contents: " + testDataString); + expect(response.commandResponse).toContain("Data Set \"TEST3.PDS\" | Member \"MEMBER2\":\nLine: " + + expectedLine + ", Column: " + expectedCol + ", Contents: " + testDataString); + expect(response.commandResponse).toContain("Data Set \"TEST3.PDS\" | Member \"MEMBER3\":\nLine: " + + expectedLine + ", Column: " + expectedCol + ", Contents: " + testDataString); + }); + it("Should handle a migrated data set", async () => { listDataSetsMatchingPatternSpy.mockImplementation(async (session, patterns, options) => { return { @@ -1104,6 +1208,18 @@ describe("Search", () => { }); }); + it("Should return failures if aborted", async () => { + searchOptions.abortSearch = function fakeAbort() { return true; }; + + const response = await (Search as any).searchOnMainframe(dummySession, searchOptions, searchItems); + + expect(getDataSetSpy).toHaveBeenCalledTimes(0); + expect(response).toEqual({ + responses: [], + failures: ["TEST1.DS", "TEST2.DS", "TEST3.PDS(MEMBER1)", "TEST3.PDS(MEMBER2)", "TEST3.PDS(MEMBER3)"] + }); + }); + it("Should handle a data set get failure", async () => { getDataSetSpy.mockImplementation(async (session, dsn, options) => { return Buffer.from(testDataString); @@ -1331,6 +1447,18 @@ describe("Search", () => { }); }); + it("Should return failures if aborted", async () => { + searchOptions.abortSearch = function fakeAbort() { return true; }; + + const response = await (Search as any).searchLocal(dummySession, searchOptions, searchItems); + + expect(getDataSetSpy).toHaveBeenCalledTimes(0); + expect(response).toEqual({ + responses: [], + failures: ["TEST1.DS", "TEST2.DS", "TEST3.PDS(MEMBER1)", "TEST3.PDS(MEMBER2)", "TEST3.PDS(MEMBER3)"] + }); + }); + it("Should handle a data set get failure", async () => { getDataSetSpy.mockImplementation(async (session, dsn, options) => { return Buffer.from(testDataString); diff --git a/packages/zosfiles/src/methods/search/Search.ts b/packages/zosfiles/src/methods/search/Search.ts index d4b5d20f85..f1431e77f4 100644 --- a/packages/zosfiles/src/methods/search/Search.ts +++ b/packages/zosfiles/src/methods/search/Search.ts @@ -17,12 +17,12 @@ import { Get } from "../get"; import { ISearchMatchLocation } from "./doc/ISearchMatchLocation"; import { asyncPool } from "@zowe/core-for-zowe-sdk"; import { ISearchOptions } from "./doc/ISearchOptions"; -import { IZosFilesResponse } from "../../doc/IZosFilesResponse"; import { IDataSet } from "../../doc/IDataSet"; import { cloneDeep } from "lodash"; +import { ISearchResponse } from "./doc/ISearchResponse"; // This interface isn't used outside of the private functions, so just keeping it here. -interface ISearchResponse { +interface IInternalSearchResponse { responses: ISearchItem[], failures: string[] } @@ -48,7 +48,7 @@ export class Search { * @throws {Error} When the {@link ZosmfRestClient} throws an error */ - public static async dataSets(session: AbstractSession, searchOptions: ISearchOptions): Promise { + public static async dataSets(session: AbstractSession, searchOptions: ISearchOptions): Promise { ImperativeExpect.toBeDefinedAndNonBlank(searchOptions.pattern, "pattern"); ImperativeExpect.toBeDefinedAndNonBlank(searchOptions.searchString, "searchString"); @@ -146,6 +146,10 @@ export class Search { searchOptions.progressTask.stageName = TaskStage.FAILED; searchOptions.progressTask.percentComplete = 100; searchOptions.progressTask.statusMessage = "Operation timed out"; + } else if (searchOptions.abortSearch?.()) { + searchOptions.progressTask.stageName = TaskStage.FAILED; + searchOptions.progressTask.percentComplete = 100; + searchOptions.progressTask.statusMessage = "Operation cancelled"; } else { searchOptions.progressTask.stageName = TaskStage.COMPLETE; searchOptions.progressTask.percentComplete = 100; @@ -170,13 +174,18 @@ export class Search { const chalk = TextUtils.chalk; - const apiResponse: IZosFilesResponse = { + const apiResponse: ISearchResponse = { success: failedDatasets.length <= 0, commandResponse: "Found \"" + chalk.yellow(origSearchQuery) + "\" in " + chalk.yellow(matchResponses.length) + " data sets and PDS members", apiResponse: matchResponses }; + if (searchOptions.abortSearch?.()) { + // Notify the user the search was cancelled, and give the results from before the cancellation. + apiResponse.commandResponse = "The search was cancelled.\n" + apiResponse.commandResponse; + } + if (matchResponses.length >= 1) { apiResponse.commandResponse += ":\n"; for (const entry of matchResponses) { @@ -232,14 +241,19 @@ export class Search { * @throws {ImperativeError} when a download fails, or timeout is exceeded. */ private static async searchOnMainframe(session: AbstractSession, searchOptions: ISearchOptions, searchItems: ISearchItem[]): - Promise { + Promise { const matches: ISearchItem[] = []; const failures: string[] = []; const total = searchItems.length; let complete = 0; + let searchAborted: boolean = searchOptions.abortSearch?.(); const createSearchPromise = async (searchItem: ISearchItem) => { - if (!this.timerExpired) { + if (!this.timerExpired && !searchAborted) { + if (searchOptions.abortSearch?.()) { + searchAborted = true; + } + // Update the progress bar if (searchOptions.progressTask) { // eslint-disable-next-line @typescript-eslint/no-magic-numbers @@ -295,13 +309,20 @@ export class Search { * * @throws {ImperativeError} when a download fails, or timeout is exceeded. */ - private static async searchLocal(session: AbstractSession, searchOptions: ISearchOptions, searchItems: ISearchItem[]): Promise { + private static async searchLocal(session: AbstractSession, searchOptions: ISearchOptions, searchItems: ISearchItem[]): + Promise { const matchedItems: ISearchItem[] = []; const failures: string[] = []; const total = searchItems.length; let complete = 0; + let searchAborted: boolean = searchOptions.abortSearch?.(); + const createFindPromise = async (searchItem: ISearchItem) => { - if (!this.timerExpired) { + if (!this.timerExpired && !searchAborted) { + if (searchOptions.abortSearch?.()) { + searchAborted = true; + } + // Handle the progress bars if (searchOptions.progressTask) { if (searchOptions.mainframeSearch) { diff --git a/packages/zosfiles/src/methods/search/doc/ISearchOptions.ts b/packages/zosfiles/src/methods/search/doc/ISearchOptions.ts index e937d34260..b554b976f8 100644 --- a/packages/zosfiles/src/methods/search/doc/ISearchOptions.ts +++ b/packages/zosfiles/src/methods/search/doc/ISearchOptions.ts @@ -45,4 +45,9 @@ export interface ISearchOptions { /* A function that, if provided, is called with a list of data sets and members that are about to be searched. */ /* If true, continue search. If false, terminate search. */ continueSearch?: (dataSets: IDataSet[]) => Promise | boolean; + + /* A function that gets called to validate whether or not to abort if a timeout isn't specified. */ + /* If abortSearch returns true, then the search should terminate immediately with the current available results. */ + /* This prevents searches from continuing to run in the background in the case that a user wishes to cancel a search (i.e. in VS Code) */ + abortSearch?: () => boolean; } \ No newline at end of file diff --git a/packages/zosfiles/src/methods/search/doc/ISearchResponse.ts b/packages/zosfiles/src/methods/search/doc/ISearchResponse.ts new file mode 100644 index 0000000000..d57235a67e --- /dev/null +++ b/packages/zosfiles/src/methods/search/doc/ISearchResponse.ts @@ -0,0 +1,17 @@ +/* +* 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 { IZosFilesResponse } from "../../../doc/IZosFilesResponse"; +import { ISearchItem } from "./ISearchItem"; + +export interface ISearchResponse extends IZosFilesResponse { + apiResponse?: ISearchItem[]; +} \ No newline at end of file diff --git a/packages/zosfiles/src/methods/search/index.ts b/packages/zosfiles/src/methods/search/index.ts index bec37537e0..51725620d3 100644 --- a/packages/zosfiles/src/methods/search/index.ts +++ b/packages/zosfiles/src/methods/search/index.ts @@ -12,5 +12,6 @@ export * from "./doc/ISearchMatchLocation"; export * from "./doc/ISearchItem"; export * from "./doc/ISearchOptions"; +export * from "./doc/ISearchResponse"; export * from "./Search";