From e82ae2322b6e9be2a00ef1c097bf99568e11743a Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 1 May 2025 14:47:35 -0400 Subject: [PATCH 01/24] Better naming for fetching user ASP Signed-off-by: worksofliam --- src/api/IBMi.ts | 12 ++++++++---- src/api/IBMiContent.ts | 2 +- src/api/configuration/config/ConnectionManager.ts | 1 + src/api/configuration/config/types.ts | 1 + src/api/tests/suites/asp.test.ts | 4 ++-- src/commands/actions.ts | 2 +- src/ui/views/objectBrowser.ts | 2 +- src/webviews/settings/index.ts | 2 +- 8 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 8d8617afd..de2dd9d32 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -1491,18 +1491,22 @@ export default class IBMi { asp = this.iAspInfo.find(asp => asp.id === by); } - if (asp) { - return asp; - } + return asp; } getIAspName(by: string|number): string|undefined { return this.getIAspDetail(by)?.name; } - getCurrentIAspName() { + getCurrentUserIAspName() { return this.currentAsp; } + + getConfiguredIAsp() { + const selected = this.config?.chosenAsp; + return selected ? this.getIAspDetail(selected) : this.getIAspDetail(0); + } + async lookupLibraryIAsp(library: string): Promise { let foundNumber = this.libraryAsps.get(library); diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index e9f12bd15..a8e4819d1 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -877,7 +877,7 @@ export default class IBMiContent { // Escape names for shell const pathList = files .map(file => { - const asp = file.asp || this.ibmi.getCurrentIAspName(); + const asp = file.asp || this.ibmi.getCurrentUserIAspName(); if (asp && asp.length > 0) { return [ Tools.qualifyPath(inAmerican(file.library), inAmerican(file.name), inAmerican(member), asp, true), diff --git a/src/api/configuration/config/ConnectionManager.ts b/src/api/configuration/config/ConnectionManager.ts index e5b72ee70..c0cc302b0 100644 --- a/src/api/configuration/config/ConnectionManager.ts +++ b/src/api/configuration/config/ConnectionManager.ts @@ -18,6 +18,7 @@ function initialize(parameters: Partial): ConnectionConfig { ifsShortcuts: parameters.ifsShortcuts || [], /** Default auto sorting of shortcuts to off */ autoSortIFSShortcuts: parameters.autoSortIFSShortcuts || false, + chosenAsp: parameters.chosenAsp || `*SYSBAS`, homeDirectory: parameters.homeDirectory || `.`, /** Undefined means not created, so default to on */ tempLibrary: parameters.tempLibrary || `ILEDITOR`, diff --git a/src/api/configuration/config/types.ts b/src/api/configuration/config/types.ts index a65676e60..157b97413 100644 --- a/src/api/configuration/config/types.ts +++ b/src/api/configuration/config/types.ts @@ -10,6 +10,7 @@ export interface ConnectionConfig extends ConnectionProfile { connectionProfiles: ConnectionProfile[]; commandProfiles: CommandProfile[]; autoSortIFSShortcuts: boolean; + chosenAsp: string; tempLibrary: string; tempDir: string; sourceFileCCSID: string; diff --git a/src/api/tests/suites/asp.test.ts b/src/api/tests/suites/asp.test.ts index 25c7da4df..7c40dec9d 100644 --- a/src/api/tests/suites/asp.test.ts +++ b/src/api/tests/suites/asp.test.ts @@ -12,14 +12,14 @@ function checkAsps(connection: IBMi) { const asps = connection.getAllIAsps(); if (asps.length === 0) return false; - const currentAsp = connection.getCurrentIAspName(); + const currentAsp = connection.getCurrentUserIAspName(); if (!currentAsp) return false; return true; } async function ensureLibExists(connection: IBMi) { - const detail = connection.getIAspDetail(connection.getCurrentIAspName()!)!; + const detail = connection.getIAspDetail(connection.getCurrentUserIAspName()!)!; const res = await connection.runCommand({ command: `CRTLIB LIB(${LIBNAME}) ASPDEV(${detail.name})` }); if (res.code) { assert.strictEqual(res.code, 0, res.stderr || res.stdout); diff --git a/src/commands/actions.ts b/src/commands/actions.ts index 008357249..1fc199730 100644 --- a/src/commands/actions.ts +++ b/src/commands/actions.ts @@ -112,7 +112,7 @@ export function registerActionsCommands(instance: Instance): Disposable[] { } break; case `streamfile`: - detail.asp = connection.getCurrentIAspName(); + detail.asp = connection.getCurrentUserIAspName(); detail.lib = config.currentLibrary; break; } diff --git a/src/ui/views/objectBrowser.ts b/src/ui/views/objectBrowser.ts index d9d207327..904ae0787 100644 --- a/src/ui/views/objectBrowser.ts +++ b/src/ui/views/objectBrowser.ts @@ -981,7 +981,7 @@ Do you want to replace it?`, item.name), { modal: true }, skipAllLabel, overwrit const pathParts = parameters.path.split(`/`); if (pathParts[1] !== `*ALL`) { - const selectedAsp = connection.getCurrentIAspName(); + const selectedAsp = connection.getCurrentUserIAspName(); const aspText = (selectedAsp ? vscode.l10n.t(`(in ASP {0})`, selectedAsp) : ``); const list = IBMi.GlobalStorage.getPreviousSearchTerms(); diff --git a/src/webviews/settings/index.ts b/src/webviews/settings/index.ts index e94edb558..52dde9718 100644 --- a/src/webviews/settings/index.ts +++ b/src/webviews/settings/index.ts @@ -78,7 +78,7 @@ export class SettingsUI { const sourceTab = new Section(); sourceTab - .addInput(`sourceASP`, `Source ASP`, `Current ASP is based on the user profile job description and cannot be changed here.`, { default: connection?.getCurrentIAspName() || `*SYSBAS`, readonly: true }) + .addInput(`sourceASP`, `Source ASP`, `Current ASP is based on the user profile job description and cannot be changed here.`, { default: connection?.getCurrentUserIAspName() || `*SYSBAS`, readonly: true }) .addInput(`sourceFileCCSID`, `Source file CCSID`, `The CCSID of source files on your system. You should only change this setting from *FILE if you have a source file that is 65535 - otherwise use *FILE. Note that this config is used to fetch all members. If you have any source files using 65535, you have bigger problems.`, { default: config.sourceFileCCSID, minlength: 1, maxlength: 5 }) .addHorizontalRule() .addCheckbox(`enableSourceDates`, `Enable Source Dates`, `When enabled, source dates will be retained and updated when editing source members. Requires restart when changed.`, config.enableSourceDates) From c53e896c8225ca93f62f8c85d01c490e0765f451 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 1 May 2025 15:07:31 -0400 Subject: [PATCH 02/24] Use method to get configured ASP Signed-off-by: worksofliam --- src/api/CompileTools.ts | 46 ++++++++++++++++++++++++++-------- src/api/IBMi.ts | 13 +++++++--- src/api/IBMiContent.ts | 2 +- src/commands/actions.ts | 2 +- src/ui/views/objectBrowser.ts | 2 +- src/webviews/settings/index.ts | 20 ++++++++++++++- 6 files changed, 66 insertions(+), 19 deletions(-) diff --git a/src/api/CompileTools.ts b/src/api/CompileTools.ts index 4c1f0c3bc..63fd4078a 100644 --- a/src/api/CompileTools.ts +++ b/src/api/CompileTools.ts @@ -142,17 +142,41 @@ export namespace CompileTools { case `ile`: default: - // escape $ and # in commands - commandResult = await connection.sendQsh({ - command: [ - ...options.noLibList? [] : buildLiblistCommands(connection, ileSetup), - ...commands.map(command => - `${`system "${IBMi.escapeForShell(command)}"`}`, - ) - ].join(` && `), - directory: cwd, - ...callbacks - }); + + if (connection.usingBash()) { + const chosenAsp = connection.getConfiguredIAsp(); + const aspName = chosenAsp ? chosenAsp.name : `*NONE`; + + const setAspGrp = connection.getContent().toCl(`SETASPGRP`, { + ASPGRP: aspName, + CURLIB: ileSetup.currentLibrary, + USRLIBL: buildLibraryList(ileSetup).join(` `), + }); + + commandResult = await connection.sendCommand({ + command: [ + `getjobid`, + `cl "${setAspGrp}"`, + ...commands.map(command => + `${`cl "${IBMi.escapeForShell(command)}"`}`, + ) + ].join(` && `), + directory: cwd, + ...callbacks + }); + } else { + commandResult = await connection.sendQsh({ + command: [ + ...options.noLibList? [] : buildLiblistCommands(connection, ileSetup), + ...commands.map(command => + `${`system "${IBMi.escapeForShell(command)}"`}`, + ) + ].join(` && `), + directory: cwd, + ...callbacks + }); + } + break; } diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index de2dd9d32..d1bb91ce1 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -1337,13 +1337,17 @@ export default class IBMi { // CHGJOB not required here. It will use the job CCSID, or the runtime CCSID. let input = Tools.fixSQL(`${possibleChangeCommand}${statements}`, true); let returningAsCsv: WrapResult | undefined; - let command = `${IBMi.locale} system "call QSYS/QZDFMDB2 PARM('-d' '-i' '-t')"` + + const chosenAsp = this.getConfiguredIAsp(); + const rdbParameter = chosenAsp ? `'-r' '${chosenAsp.rdbName}'` : ``; + + let command = `${IBMi.locale} system "call QSYS/QZDFMDB2 PARM('-d' '-i' '-t' ${rdbParameter})"` let useCsv = options.forceSafe; // Use custom QSH if available if (this.canUseCqsh) { const customQsh = this.getComponent(CustomQSh.ID)!; - command = `${IBMi.locale} ${customQsh.installPath} -c "system \\"call QSYS/QZDFMDB2 PARM('-d' '-i' '-t')\\""`; + command = `${IBMi.locale} ${customQsh.installPath} -c "system \\"call QSYS/QZDFMDB2 PARM('-d' '-i' '-t' ${rdbParameter})\\""`; } if (this.requiresTranslation) { @@ -1502,9 +1506,10 @@ export default class IBMi { return this.currentAsp; } - getConfiguredIAsp() { + getConfiguredIAsp(): AspInfo|undefined { const selected = this.config?.chosenAsp; - return selected ? this.getIAspDetail(selected) : this.getIAspDetail(0); + + return selected ? this.getIAspDetail(selected) : undefined; } async lookupLibraryIAsp(library: string): Promise { diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index a8e4819d1..1ef592cee 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -877,7 +877,7 @@ export default class IBMiContent { // Escape names for shell const pathList = files .map(file => { - const asp = file.asp || this.ibmi.getCurrentUserIAspName(); + const asp = file.asp || this.ibmi.getConfiguredIAsp()?.name; if (asp && asp.length > 0) { return [ Tools.qualifyPath(inAmerican(file.library), inAmerican(file.name), inAmerican(member), asp, true), diff --git a/src/commands/actions.ts b/src/commands/actions.ts index 1fc199730..fdcdbd474 100644 --- a/src/commands/actions.ts +++ b/src/commands/actions.ts @@ -112,7 +112,7 @@ export function registerActionsCommands(instance: Instance): Disposable[] { } break; case `streamfile`: - detail.asp = connection.getCurrentUserIAspName(); + detail.asp = connection.getConfiguredIAsp()?.name; detail.lib = config.currentLibrary; break; } diff --git a/src/ui/views/objectBrowser.ts b/src/ui/views/objectBrowser.ts index 904ae0787..eeec99980 100644 --- a/src/ui/views/objectBrowser.ts +++ b/src/ui/views/objectBrowser.ts @@ -981,7 +981,7 @@ Do you want to replace it?`, item.name), { modal: true }, skipAllLabel, overwrit const pathParts = parameters.path.split(`/`); if (pathParts[1] !== `*ALL`) { - const selectedAsp = connection.getCurrentUserIAspName(); + const selectedAsp = connection.getConfiguredIAsp()?.name; const aspText = (selectedAsp ? vscode.l10n.t(`(in ASP {0})`, selectedAsp) : ``); const list = IBMi.GlobalStorage.getPreviousSearchTerms(); diff --git a/src/webviews/settings/index.ts b/src/webviews/settings/index.ts index 52dde9718..9b2ca06d9 100644 --- a/src/webviews/settings/index.ts +++ b/src/webviews/settings/index.ts @@ -77,8 +77,26 @@ export class SettingsUI { .addCheckbox(`autoClearTempData`, `Clear temporary data automatically`, `Automatically clear temporary data in the chosen temporary library when it's done with and on startup. Deletes all *FILE objects that start with O_ in the chosen temporary library.`, config.autoClearTempData); const sourceTab = new Section(); + + if (connection) { + const asps = connection.getAllIAsps().map(asp => ({ + selected: asp.name === config.sourceASP, + value: asp.name, + description: `iASP ${asp.name}`, + text: asp.name + })); + + asps.push({ + selected: config.sourceASP === `*SYSBAS` || !asps.some(asp => asp.value === config.sourceASP), + value: `*SYSBAS`, + description: `System ASP`, + text: `*SYSBAS` + }); + + sourceTab.addSelect(`chosenAsp`, `Chosen ASP`, asps, `iASP that should be used when navigating file systems and executing commands. iASP configured on your user profile is ${connection.getCurrentUserIAspName() || `*SYSBAS`}.`); + } + sourceTab - .addInput(`sourceASP`, `Source ASP`, `Current ASP is based on the user profile job description and cannot be changed here.`, { default: connection?.getCurrentUserIAspName() || `*SYSBAS`, readonly: true }) .addInput(`sourceFileCCSID`, `Source file CCSID`, `The CCSID of source files on your system. You should only change this setting from *FILE if you have a source file that is 65535 - otherwise use *FILE. Note that this config is used to fetch all members. If you have any source files using 65535, you have bigger problems.`, { default: config.sourceFileCCSID, minlength: 1, maxlength: 5 }) .addHorizontalRule() .addCheckbox(`enableSourceDates`, `Enable Source Dates`, `When enabled, source dates will be retained and updated when editing source members. Requires restart when changed.`, config.enableSourceDates) From 8bc2b3af9902ca27061445e9087ce138b08f1f5a Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 1 May 2025 15:48:55 -0400 Subject: [PATCH 03/24] Ability to configure iASP Signed-off-by: worksofliam --- src/api/IBMi.ts | 6 ++++++ src/api/Tools.ts | 1 + src/webviews/settings/index.ts | 35 +++++++++++++++++----------------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index d1bb91ce1..7475fb5d7 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -783,6 +783,12 @@ export default class IBMi { this.currentAsp = await this.getUserProfileAsp(); + // Set the default ASP to the current ASP if it is not set. + const chosenAspIsConfigured = this.getConfiguredIAsp(); + if (!chosenAspIsConfigured) { + this.config.chosenAsp = this.currentAsp || `*SYSBAS`; + } + // Fetch conversion values? if (quickConnect() && cachedServerSettings?.jobCcsid !== null && cachedServerSettings?.userDefaultCCSID && cachedServerSettings?.qccsid) { this.qccsid = cachedServerSettings.qccsid; diff --git a/src/api/Tools.ts b/src/api/Tools.ts index 3df788656..983c1eb88 100644 --- a/src/api/Tools.ts +++ b/src/api/Tools.ts @@ -33,6 +33,7 @@ export namespace Tools { const data = output.split(`\n`).filter(line => { const trimmed = line.trim(); return trimmed !== `DB2>` && + !trimmed.startsWith('WARNING') && // TODO: Ignore warnings (TODO: specifically for multi-threaded, be careful because of locale changes) !trimmed.startsWith(`DB20`) && // Notice messages !/COMMAND .+ COMPLETED WITH EXIT STATUS \d+/.test(trimmed) && // @CL command execution output trimmed !== `?>`; diff --git a/src/webviews/settings/index.ts b/src/webviews/settings/index.ts index 9b2ca06d9..0d1e1434a 100644 --- a/src/webviews/settings/index.ts +++ b/src/webviews/settings/index.ts @@ -79,21 +79,25 @@ export class SettingsUI { const sourceTab = new Section(); if (connection) { - const asps = connection.getAllIAsps().map(asp => ({ - selected: asp.name === config.sourceASP, - value: asp.name, - description: `iASP ${asp.name}`, - text: asp.name - })); - - asps.push({ - selected: config.sourceASP === `*SYSBAS` || !asps.some(asp => asp.value === config.sourceASP), - value: `*SYSBAS`, - description: `System ASP`, - text: `*SYSBAS` - }); + if (connection.usingBash()) { + const asps = connection.getAllIAsps().map(asp => ({ + selected: asp.name === config.chosenAsp, + value: asp.name, + description: asp.name, + text: `${asp.name} (independent ASP ${asp.id})` + })); + + asps.push({ + selected: config.chosenAsp === `*SYSBAS` || !asps.some(asp => asp.value === config.chosenAsp), + value: `*SYSBAS`, + description: `*SYSBAS`, + text: `*SYSBAS` + }); - sourceTab.addSelect(`chosenAsp`, `Chosen ASP`, asps, `iASP that should be used when navigating file systems and executing commands. iASP configured on your user profile is ${connection.getCurrentUserIAspName() || `*SYSBAS`}.`); + sourceTab.addSelect(`chosenAsp`, `ASP / Database`, asps, `iASP that should be used when navigating file systems and executing commands. The iASP configured on your user profile is ${connection.getCurrentUserIAspName() || `*SYSBAS`} and changing this setting will override that while using Code for IBM i.`); + } else { + sourceTab.addInput(`chosenAsp`, `ASP / Database`, `iASP that will be used when navigating file systems and executing commands. This cannot be overridden because bash is not your default shell.`, { default: connection.getCurrentUserIAspName() || `*SYSBAS`, readonly: true }); + } } sourceTab @@ -304,9 +308,6 @@ export class SettingsUI { //In case we need to play with the data switch (key) { - case `sourceASP`: - data[key] = null; - break; case `hideCompileErrors`: data[key] = String(data[key]).split(`,`) .map(item => item.toUpperCase().trim()) From ac11af26d529c5ddeb6320c99bc77c52c0464944 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 1 May 2025 16:10:36 -0400 Subject: [PATCH 04/24] test cases to prove ASPs work as expected Signed-off-by: worksofliam --- src/api/tests/suites/asp.test.ts | 52 ++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/src/api/tests/suites/asp.test.ts b/src/api/tests/suites/asp.test.ts index 7c40dec9d..1d49dc391 100644 --- a/src/api/tests/suites/asp.test.ts +++ b/src/api/tests/suites/asp.test.ts @@ -8,24 +8,31 @@ const LIBNAME = `VSCODELIBT`; const SPFNAME = `VSCODESPFT`; const MBRNAME = `VSCODEMBRT`; +/** + * Check the system has at least one iASP available, + * and the user profile is set to use *SYSBAS + */ function checkAsps(connection: IBMi) { const asps = connection.getAllIAsps(); if (asps.length === 0) return false; const currentAsp = connection.getCurrentUserIAspName(); - if (!currentAsp) return false; + if (currentAsp !== undefined) return false; return true; } -async function ensureLibExists(connection: IBMi) { - const detail = connection.getIAspDetail(connection.getCurrentUserIAspName()!)!; - const res = await connection.runCommand({ command: `CRTLIB LIB(${LIBNAME}) ASPDEV(${detail.name})` }); +async function ensureLibExists(connection: IBMi, aspName: string) { + const res = await connection.runCommand({ command: `CRTLIB LIB(${LIBNAME}) ASPDEV(${aspName})` }); if (res.code) { assert.strictEqual(res.code, 0, res.stderr || res.stdout); } } +async function setToAsp(connection: IBMi, name?: string) { + connection.getConfig().chosenAsp = name || `*SYSBAS`; +} + async function createTempRpgle(connection: IBMi) { const content = connection.getContent(); @@ -35,7 +42,7 @@ async function createTempRpgle(connection: IBMi) { }); await connection.runCommand({ - command: `ADDPFM FILE(${LIBNAME}/${SPFNAME}) MBR(${MBRNAME}) `, + command: `ADDPFM FILE(${LIBNAME}/${SPFNAME}) MBR(${MBRNAME})`, environment: `ile` }); @@ -49,8 +56,14 @@ describe(`iASP tests`, { concurrent: true }, () => { let skipAsp = false; beforeAll(async () => { connection = await newConnection(); + setToAsp(connection); + if (checkAsps(connection)) { - await ensureLibExists(connection); + const useiAsp = connection.getAllIAsps()[0].name; + + await ensureLibExists(connection, useiAsp); + + setToAsp(connection, useiAsp); await createTempRpgle(connection); } else { console.log(`Skipping iASP tests, no ASPs found.`); @@ -59,6 +72,8 @@ describe(`iASP tests`, { concurrent: true }, () => { }, CONNECTION_TIMEOUT) afterAll(async () => { + setToAsp(connection, connection.getAllIAsps()[0].name); + await connection.runCommand({ command: `DLTLIB LIB(${LIBNAME})` }); disposeConnection(connection); }); @@ -67,21 +82,40 @@ describe(`iASP tests`, { concurrent: true }, () => { if (skipAsp) { t.skip(); } + + setToAsp(connection, connection.getAllIAsps()[0].name); + }); + + it('CHKOBJ works with ASP set and unset', async () => { + expect(connection.getConfiguredIAsp()).toBeDefined(); + + const aspObjectExists = await connection.getContent()?.checkObject({library: LIBNAME, name: SPFNAME, type: `*FILE`}); + expect(aspObjectExists).toBeTruthy() + + setToAsp(connection); // Reset to *SYSBAS + const aspObjectNotFound = await connection.getContent()?.checkObject({library: LIBNAME, name: SPFNAME, type: `*FILE`}); + expect(aspObjectNotFound).toBeFalsy() }); it('Read members in ASP and base', async () => { + expect(connection.getConfiguredIAsp()).toBeDefined(); + const aspMbrContents = await connection.getContent()?.downloadMemberContent(LIBNAME, SPFNAME, MBRNAME); assert.ok(aspMbrContents); }); it('can find ASP members via search', async () => { + expect(connection.getConfiguredIAsp()).toBeDefined(); + const searchResults = await Search.searchMembers(connection, LIBNAME, SPFNAME, `hello world`, `*`); expect(searchResults.hits.length).toBeGreaterThan(0); // TODO: additional expects }); it('can resolve member info from ASP', async () => { + expect(connection.getConfiguredIAsp()).toBeDefined(); + const resolved = await connection.getContent().memberResolve(MBRNAME, [ { library: `QSYS`, name: `QSYSINC` }, { library: LIBNAME, name: SPFNAME } @@ -90,4 +124,10 @@ describe(`iASP tests`, { concurrent: true }, () => { expect(resolved).toBeDefined(); //TODO: additional expects }); + + it('can change ASP', async () => { + setToAsp(connection); // Reset to *SYSBAS + + expect(connection.getConfiguredIAsp()).toBeUndefined(); + }); }); From 88fa4130c71a97254506c488c2c32f0a6f11765e Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 1 May 2025 16:16:45 -0400 Subject: [PATCH 05/24] Add check in afterAll Signed-off-by: worksofliam --- src/api/tests/suites/asp.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/api/tests/suites/asp.test.ts b/src/api/tests/suites/asp.test.ts index 1d49dc391..a5a985b74 100644 --- a/src/api/tests/suites/asp.test.ts +++ b/src/api/tests/suites/asp.test.ts @@ -72,10 +72,12 @@ describe(`iASP tests`, { concurrent: true }, () => { }, CONNECTION_TIMEOUT) afterAll(async () => { - setToAsp(connection, connection.getAllIAsps()[0].name); + if (checkAsps(connection)) { + setToAsp(connection, connection.getAllIAsps()[0].name); - await connection.runCommand({ command: `DLTLIB LIB(${LIBNAME})` }); - disposeConnection(connection); + await connection.runCommand({ command: `DLTLIB LIB(${LIBNAME})` }); + disposeConnection(connection); + } }); beforeEach((t) => { From 2cb9afaccd9c8b632c4778fadd85ee8977961c57 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 1 May 2025 16:17:38 -0400 Subject: [PATCH 06/24] Fix to library list order when bash is used Signed-off-by: worksofliam --- src/api/CompileTools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/CompileTools.ts b/src/api/CompileTools.ts index 63fd4078a..8caa30be7 100644 --- a/src/api/CompileTools.ts +++ b/src/api/CompileTools.ts @@ -150,7 +150,7 @@ export namespace CompileTools { const setAspGrp = connection.getContent().toCl(`SETASPGRP`, { ASPGRP: aspName, CURLIB: ileSetup.currentLibrary, - USRLIBL: buildLibraryList(ileSetup).join(` `), + USRLIBL: ileSetup.libraryList.join(` `), }); commandResult = await connection.sendCommand({ From 08fefb622eceac5169a06bd2d1549d8127646a31 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Fri, 2 May 2025 08:59:18 -0400 Subject: [PATCH 07/24] Minor changes to default ASP config Signed-off-by: worksofliam --- src/api/IBMi.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 7475fb5d7..3d15ad038 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -105,7 +105,7 @@ export default class IBMi { * the root of the IFS, thus why we store it. */ private iAspInfo: AspInfo[] = []; - private currentAsp: string|undefined; + private userProfileIAsp: string|undefined; private libraryAsps = new Map(); /** @@ -620,7 +620,7 @@ export default class IBMi { //check users default shell if (!commandShellResult.stderr) { - let usesBash = this.shell === IBMi.bashShellPath; + const usesBash = this.usingBash(); if (!usesBash) { // make sure chsh is installed if (this.remoteFeatures[`chsh`]) { @@ -781,12 +781,18 @@ export default class IBMi { message: `Fetching current iASP information.` }); - this.currentAsp = await this.getUserProfileAsp(); + this.userProfileIAsp = await this.getUserProfileAsp(); - // Set the default ASP to the current ASP if it is not set. - const chosenAspIsConfigured = this.getConfiguredIAsp(); - if (!chosenAspIsConfigured) { - this.config.chosenAsp = this.currentAsp || `*SYSBAS`; + if (this.usingBash()) { + // Set the default ASP to the current ASP if it is not set. + const chosenAspIsConfigured = this.getConfiguredIAsp(); + if (!chosenAspIsConfigured) { + this.config.chosenAsp = this.userProfileIAsp || `*SYSBAS`; + } + } else { + // If the user is not using bash, then we set the chosen ASP to the user profile iASP. + // since we can't change the ASP without using bash (since we use 'cl') + this.config.chosenAsp = this.userProfileIAsp || `*SYSBAS`; } // Fetch conversion values? @@ -1509,7 +1515,7 @@ export default class IBMi { } getCurrentUserIAspName() { - return this.currentAsp; + return this.userProfileIAsp; } getConfiguredIAsp(): AspInfo|undefined { From a48945aa5f050580f889c9f9fcb9f9ce30d0e3aa Mon Sep 17 00:00:00 2001 From: worksofliam Date: Fri, 2 May 2025 09:47:40 -0400 Subject: [PATCH 08/24] Fix ASP library API to support multiple ASPs Signed-off-by: worksofliam --- src/api/IBMi.ts | 65 +++++++++++++++++++++++++++++++----------- src/api/IBMiContent.ts | 14 ++++++++- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 3d15ad038..f2b56a853 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -106,7 +106,7 @@ export default class IBMi { */ private iAspInfo: AspInfo[] = []; private userProfileIAsp: string|undefined; - private libraryAsps = new Map(); + private libraryAsps = new Map(); /** * @deprecated Will be replaced with {@link IBMi.getAllIAsps} in v3.0.0 @@ -1524,30 +1524,63 @@ export default class IBMi { return selected ? this.getIAspDetail(selected) : undefined; } + /** + * Will return the name of the iASP in which the given library resides. + * If the library is not in an iASP, it will return undefined. + * If multiple iASPS contain a library with this name, then either: + * - If the user has configured an iASP for this library, it will return that iASP name. + * - If the user has not configured an iASP for this library, it will return undefined. + */ async lookupLibraryIAsp(library: string): Promise { - let foundNumber = this.libraryAsps.get(library); - - if (!foundNumber) { - const [row] = await this.runSQL(`SELECT IASP_NUMBER FROM TABLE(QSYS2.LIBRARY_INFO('${this.sysNameInAmerican(library)}'))`); - const iaspNumber = Number(row?.IASP_NUMBER); - if (iaspNumber >= 0) { - this.libraryAsps.set(library, iaspNumber); - foundNumber = iaspNumber; + let foundAsps = this.libraryAsps.get(library) || []; + + if (foundAsps.length === 0) { + const rows = await this.runSQL(`SELECT IASP_NUMBER from table(qsys2.object_statistics('*ALLAVL', '*LIB')) x where x.objname = '${this.sysNameInAmerican(library)}'`); + + for (const row of rows) { + if (row?.IASP_NUMBER !== undefined && row?.IASP_NUMBER !== null) { + const aspNumber = Number(row.IASP_NUMBER); + + if (aspNumber > 0) { + foundAsps.push(aspNumber); + } + break; + } } - } - if (foundNumber) { - return this.getIAspName(foundNumber); + if (foundAsps.length > 0) { + // Cache the found ASPs for this library + this.libraryAsps.set(library, foundAsps); + } } + + return this.getLibraryIAsp(library); } - getLibraryIAsp(library: string) { - const found = this.libraryAsps.get(library); - if (found && found >= 0) { - return this.getIAspName(found); + getLibraryIAsp(library: string): string|undefined { + const foundAsps = this.libraryAsps.get(library) || []; + + if (foundAsps.length === 1) { + // If there is only one ASP, return it + return this.getIAspName(foundAsps[0]); + } + else if (foundAsps.length > 1) { + const userConfigured = this.getConfiguredIAsp(); + if (userConfigured && foundAsps.includes(userConfigured.id)) { + // If the user has configured an iASP for this library, return it + return userConfigured.name; + } else { + return undefined; // Multiple ASPs found, but no user configured iASP + } } } + getLibraryIAsps(library: string): string[] { + const foundAsps = this.libraryAsps.get(library) || []; + + return foundAsps.map(asp => this.getIAspName(asp)).filter(name => name !== undefined); + } + /** * @deprecated Use {@link IBMiContent.uploadFiles} instead. */ diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 1ef592cee..7bebd2058 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -514,7 +514,19 @@ export default class IBMiContent { if (localLibrary !== `QSYS`) { if (!await this.checkObject({ library: "QSYS", name: localLibrary, type: "*LIB" })) { - throw new Error(`Library ${localLibrary} does not exist.`); + const configuredAsp = this.ibmi.getConfig().chosenAsp; + const existsInLibrary = await this.ibmi.lookupLibraryIAsp(localLibrary); + const existsInMultiple = this.ibmi.getLibraryIAsps(localLibrary); + + if (existsInMultiple.length >= 2) { + throw new Error(`Library ${localLibrary} exists in multiple IASPs: ${existsInMultiple.join(', ')}. Please specify the IASP in the connection settings.`); + } + else if (existsInLibrary && existsInLibrary !== configuredAsp) { + throw new Error(`Library ${localLibrary} is in IASP ${existsInLibrary}, but the current connection is not configured to use that IASP.`); + + } else { + throw new Error(`Library ${localLibrary} does not exist.`); + } } } From 035f40cc1474d1e9b563e6063c2d3fd2e38164df Mon Sep 17 00:00:00 2001 From: worksofliam Date: Fri, 2 May 2025 09:52:47 -0400 Subject: [PATCH 09/24] Remove useless call to getjobid Signed-off-by: worksofliam --- src/api/CompileTools.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/CompileTools.ts b/src/api/CompileTools.ts index 8caa30be7..c996e9d4e 100644 --- a/src/api/CompileTools.ts +++ b/src/api/CompileTools.ts @@ -155,7 +155,6 @@ export namespace CompileTools { commandResult = await connection.sendCommand({ command: [ - `getjobid`, `cl "${setAspGrp}"`, ...commands.map(command => `${`cl "${IBMi.escapeForShell(command)}"`}`, From 82c85e64c2897bca558e873e3f728c9970029af9 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Fri, 2 May 2025 11:29:17 -0400 Subject: [PATCH 10/24] Add additional check to make sure the configured ASP is usable Signed-off-by: worksofliam --- src/api/IBMi.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index f2b56a853..eba0f330a 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -757,7 +757,7 @@ export default class IBMi { //This is mostly a nice to have. We grab the ASP info so user's do //not have to provide the ASP in the settings. try { - const resultSet = await this.runSQL(`SELECT * FROM QSYS2.ASP_INFO`); + const resultSet = await this.runSQL(`select * from qsys2.asp_info where asp_state in ('ACTIVE', 'AVAILABLE', 'VARIED ON')`); resultSet.forEach(row => { // Does not ever include SYSBAS/SYSTEM, only iASPs if (row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME !== `null`) { @@ -786,7 +786,15 @@ export default class IBMi { if (this.usingBash()) { // Set the default ASP to the current ASP if it is not set. const chosenAspIsConfigured = this.getConfiguredIAsp(); - if (!chosenAspIsConfigured) { + if (chosenAspIsConfigured) { + // Double check that the configured ASP is usable. + const configuredIsUsable = await this.canUseConfiguredAsp(); + + if (!configuredIsUsable) { + this.config.chosenAsp = `*SYSBAS`; + } + + } else { this.config.chosenAsp = this.userProfileIAsp || `*SYSBAS`; } } else { @@ -1341,7 +1349,7 @@ export default class IBMi { * @param statements * @returns a Result set */ - async runSQL(statements: string, options: { fakeBindings?: (string | number)[], forceSafe?: boolean } = {}): Promise { + async runSQL(statements: string, options: { fakeBindings?: (string | number)[], forceSafe?: boolean, ignoreDatabase?: boolean } = {}): Promise { const { 'QZDFMDB2.PGM': QZDFMDB2 } = this.remoteFeatures; const possibleChangeCommand = (this.userCcsidInvalid ? `@CHGJOB CCSID(${this.getCcsid()});\n` : ''); @@ -1351,7 +1359,7 @@ export default class IBMi { let returningAsCsv: WrapResult | undefined; const chosenAsp = this.getConfiguredIAsp(); - const rdbParameter = chosenAsp ? `'-r' '${chosenAsp.rdbName}'` : ``; + const rdbParameter = chosenAsp && options.ignoreDatabase !== true ? `'-r' '${chosenAsp.rdbName}'` : ``; let command = `${IBMi.locale} system "call QSYS/QZDFMDB2 PARM('-d' '-i' '-t' ${rdbParameter})"` let useCsv = options.forceSafe; @@ -1482,7 +1490,7 @@ export default class IBMi { } private async getUserProfileAsp(): Promise { - const [currentRdb] = await this.runSQL(`values current_server`); + const [currentRdb] = await this.runSQL(`values current_server`, {ignoreDatabase: true}); if (currentRdb) { const key = Object.keys(currentRdb)[0]; @@ -1495,6 +1503,17 @@ export default class IBMi { } } + private async canUseConfiguredAsp(): Promise { + try { + const [row] = await this.runSQL(`values current_server`); + return true; + } catch (e) { + // If we can't run the SQL, then we can't use the configured ASP. + console.log(e); + return false; + } + } + getAllIAsps() { return this.iAspInfo; } From 4f3c248a2a9bb7b21e7e169733be2adbd42c8ee0 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Fri, 2 May 2025 11:29:35 -0400 Subject: [PATCH 11/24] Show loading indicator when opening connection settings Signed-off-by: worksofliam --- src/instantiate.ts | 22 +++++++++++++++------- src/webviews/settings/index.ts | 7 ++++++- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/instantiate.ts b/src/instantiate.ts index aad556adb..eefc6fbc1 100644 --- a/src/instantiate.ts +++ b/src/instantiate.ts @@ -26,10 +26,6 @@ disconnectBarItem.tooltip = `Disconnect from system.`; disconnectBarItem.text = `$(debug-disconnect)`; const connectedBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10); -connectedBarItem.command = { - command: `code-for-ibmi.showAdditionalSettings`, - title: `Show connection settings` -}; export async function safeDisconnect(): Promise { let doDisconnect = true; @@ -69,7 +65,7 @@ export async function loadAllofExtension(context: vscode.ExtensionContext) { ...registerConnectionCommands(context, instance), - onCodeForIBMiConfigurationChange("connectionSettings", updateConnectedBar), + onCodeForIBMiConfigurationChange("connectionSettings", () => updateConnectedBar()), ...registerOpenCommands(instance), @@ -101,11 +97,23 @@ export async function loadAllofExtension(context: vscode.ExtensionContext) { } } -async function updateConnectedBar() { +export async function updateConnectedBar(options: { loading?: boolean } = {}) { const connection = instance.getConnection(); if (connection) { const config = connection.getConfig(); - connectedBarItem.text = `$(${config.readOnlyMode ? "lock" : "settings-gear"}) ${config.name}`; + + let icon = `$(${config.readOnlyMode ? "lock" : "server"})`; + + if (options.loading) { + icon = `$(${config.readOnlyMode ? "loading~spin" : "loading~spin"})`; + } else { + connectedBarItem.command = { + command: `code-for-ibmi.showAdditionalSettings`, + title: `Show connection settings` + }; + } + + connectedBarItem.text = `${icon} ${config.name}`; const debugRunning = await isDebugEngineRunning(); connectedBarItem.tooltip = new vscode.MarkdownString([ diff --git a/src/webviews/settings/index.ts b/src/webviews/settings/index.ts index 0d1e1434a..51f7c8620 100644 --- a/src/webviews/settings/index.ts +++ b/src/webviews/settings/index.ts @@ -7,7 +7,7 @@ import { deleteStoredPassword, getStoredPassword, setStoredPassword } from "../. import { isManaged } from "../../debug"; import * as certificates from "../../debug/certificates"; import { isSEPSupported } from "../../debug/server"; -import { instance } from "../../instantiate"; +import { instance, updateConnectedBar } from "../../instantiate"; import { ConnectionConfig, ConnectionData, Server } from '../../typings'; import { VscodeTools } from "../../ui/Tools"; import { ComplexTab, CustomUI, Section } from "../CustomUI"; @@ -40,6 +40,8 @@ export class SettingsUI { const connection = instance.getConnection(); const passwordAuthorisedExtensions = instance.getStorage()?.getAuthorisedExtensions() || []; + updateConnectedBar({loading: true}); + let config: ConnectionConfig; if (connectionSettings && server) { @@ -274,6 +276,9 @@ export class SettingsUI { .addHorizontalRule() .addButtons({ id: `save`, label: `Save settings`, requiresValidation: true }); + + updateConnectedBar(); + await VscodeTools.withContext(EDITING_CONTEXT, async () => { const page = await ui.loadPage(`Settings: ${config.name}`); if (page) { From 1b4009ff6d32fca5b8b3a307a4cf256593fac382 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 8 May 2025 14:14:56 -0400 Subject: [PATCH 12/24] Test case for getLibraryList Signed-off-by: worksofliam --- src/api/IBMiContent.ts | 2 +- src/api/tests/suites/asp.test.ts | 25 ++++++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 7bebd2058..a067cfef6 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -396,7 +396,7 @@ export default class IBMiContent { os.OBJOWNER AS OWNER, os.OBJDEFINER AS CREATED_BY from table( SYSTOOLS.SPLIT( INPUT_LIST => '${libraries.toString()}', DELIMITER => ',' ) ) libs, - table( QSYS2.OBJECT_STATISTICS( OBJECT_SCHEMA => 'QSYS', OBJTYPELIST => '*LIB', OBJECT_NAME => libs.ELEMENT ) ) os + table( QSYS2.OBJECT_STATISTICS( OBJECT_SCHEMA => '*ALLUSR', OBJTYPELIST => '*LIB', OBJECT_NAME => libs.ELEMENT ) ) os `; const results = await this.ibmi.runSQL(statement); diff --git a/src/api/tests/suites/asp.test.ts b/src/api/tests/suites/asp.test.ts index a5a985b74..16d1bf466 100644 --- a/src/api/tests/suites/asp.test.ts +++ b/src/api/tests/suites/asp.test.ts @@ -24,9 +24,6 @@ function checkAsps(connection: IBMi) { async function ensureLibExists(connection: IBMi, aspName: string) { const res = await connection.runCommand({ command: `CRTLIB LIB(${LIBNAME}) ASPDEV(${aspName})` }); - if (res.code) { - assert.strictEqual(res.code, 0, res.stderr || res.stdout); - } } async function setToAsp(connection: IBMi, name?: string) { @@ -92,11 +89,11 @@ describe(`iASP tests`, { concurrent: true }, () => { expect(connection.getConfiguredIAsp()).toBeDefined(); const aspObjectExists = await connection.getContent()?.checkObject({library: LIBNAME, name: SPFNAME, type: `*FILE`}); - expect(aspObjectExists).toBeTruthy() + expect(aspObjectExists).toBeTruthy(); setToAsp(connection); // Reset to *SYSBAS const aspObjectNotFound = await connection.getContent()?.checkObject({library: LIBNAME, name: SPFNAME, type: `*FILE`}); - expect(aspObjectNotFound).toBeFalsy() + expect(aspObjectNotFound).toBeFalsy(); }); it('Read members in ASP and base', async () => { @@ -127,6 +124,24 @@ describe(`iASP tests`, { concurrent: true }, () => { //TODO: additional expects }); + it('can get library info', async () => { + expect(connection.getConfiguredIAsp()).toBeDefined(); + + const librariesA = await connection.getContent().getLibraryList([`QSYS2`, LIBNAME]); + expect(librariesA.length).toBe(2); + expect(librariesA.some(lib => lib.name === `QSYS2`)).toBeTruthy(); + expect(librariesA.some(lib => lib.name === LIBNAME)).toBeTruthy(); + + setToAsp(connection); // Reset to *SYSBAS + const librariesB = await connection.getContent().getLibraryList([`QSYS2`, LIBNAME]); + expect(librariesB.length).toBe(2); + expect(librariesB.some(lib => lib.name === `QSYS2`)).toBeTruthy(); + + const notFound = librariesB.find(lib => lib.name === LIBNAME); + expect(notFound).toBeTruthy(); + expect(notFound!.text).toBe(`*** NOT FOUND ***`); + }); + it('can change ASP', async () => { setToAsp(connection); // Reset to *SYSBAS From bcec68e3a24a78787da93facb37aaf710ed52cca Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 8 May 2025 14:25:20 -0400 Subject: [PATCH 13/24] Fix to validateLibraryList to support ASP Signed-off-by: worksofliam --- src/api/IBMiContent.ts | 42 ++++++++++++++++++++++++++------ src/api/tests/suites/asp.test.ts | 12 +++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index a067cfef6..c2d0b0fa3 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -449,6 +449,9 @@ export default class IBMiContent { * @returns Bad libraries */ async validateLibraryList(newLibl: string[]): Promise { + // TODO: consider replacing this with getLibraryList + // since it does the same thing (validates a list of libraries) + let badLibs: string[] = []; newLibl = newLibl @@ -468,14 +471,39 @@ export default class IBMiContent { const sanitized = Tools.sanitizeObjNamesForPase(newLibl); - const result = await this.ibmi.sendQsh({ - command: [ - `liblist -d ` + Tools.sanitizeObjNamesForPase(this.ibmi.defaultUserLibraries).join(` `), - ...sanitized.map(lib => `liblist -a ` + lib) - ].join(`; `) - }); + let commands: string[] = []; + + if (this.ibmi.usingBash()) { + // If we are using bash, we can set the ASP group with the cl builtin. + const configuredAsp = this.ibmi.getConfiguredIAsp(); + if (configuredAsp) { + const setAspGrp = this.ibmi.getContent().toCl(`SETASPGRP`, { + ASPGRP: configuredAsp.name, + }); + commands.push(`cl "${setAspGrp}"`); + } + } + + commands.push(...[ + `liblist -d ` + Tools.sanitizeObjNamesForPase(this.ibmi.defaultUserLibraries).join(` `), + ...sanitized.map(lib => `liblist -a ` + lib) + ]); + + let result: CommandResult; + + if (this.ibmi.usingBash()) { + result = await this.ibmi.sendCommand({ + command: commands.join(`; `), + directory: `.` + }); + } else { + result = await this.ibmi.sendQsh({ + command: commands.join(`; `), + directory: `.` + }); + } - if (result.stderr) { + if (result && result.stderr) { const lines = result.stderr.split(`\n`); lines.forEach(line => { diff --git a/src/api/tests/suites/asp.test.ts b/src/api/tests/suites/asp.test.ts index 16d1bf466..ffd41f0b1 100644 --- a/src/api/tests/suites/asp.test.ts +++ b/src/api/tests/suites/asp.test.ts @@ -142,6 +142,18 @@ describe(`iASP tests`, { concurrent: true }, () => { expect(notFound!.text).toBe(`*** NOT FOUND ***`); }); + it('can validate libraries in ASP', async () => { + expect(connection.getConfiguredIAsp()).toBeDefined(); + + const badLibsA = await connection.getContent().validateLibraryList([`QSYS2`, LIBNAME]); + expect(badLibsA.length).toBe(0); + + setToAsp(connection); // Reset to *SYSBAS + const badLibsB = await connection.getContent().validateLibraryList([`QSYS2`, LIBNAME]); + expect(badLibsB.length).toBe(1); + expect(badLibsB[0]).toBe(LIBNAME); + }) + it('can change ASP', async () => { setToAsp(connection); // Reset to *SYSBAS From 5dd27bf7b1709b16bcee824fd1c2804f9c4c7f89 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 8 May 2025 14:35:24 -0400 Subject: [PATCH 14/24] Add large timeout for search tests Signed-off-by: worksofliam --- src/api/tests/suites/search.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/tests/suites/search.test.ts b/src/api/tests/suites/search.test.ts index dc62fedd4..9bf4070bb 100644 --- a/src/api/tests/suites/search.test.ts +++ b/src/api/tests/suites/search.test.ts @@ -6,7 +6,7 @@ import { Tools } from '../../Tools'; import { SearchResults } from '../../types'; import { CONNECTION_TIMEOUT, disposeConnection, newConnection } from '../connection'; -describe('Search Tests', { concurrent: true }, () => { +describe('Search Tests', { concurrent: true, timeout: 100000 }, () => { let connection: IBMi beforeAll(async () => { connection = await newConnection(); From b31f2884f84d9c9dcbf11d2a7350870dc814533a Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 8 May 2025 14:40:42 -0400 Subject: [PATCH 15/24] Increase timeouts Signed-off-by: worksofliam --- src/api/tests/suites/connection.test.ts | 2 +- src/api/tests/suites/content.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/tests/suites/connection.test.ts b/src/api/tests/suites/connection.test.ts index 456dd38aa..c8e0496b9 100644 --- a/src/api/tests/suites/connection.test.ts +++ b/src/api/tests/suites/connection.test.ts @@ -6,7 +6,7 @@ import IBMi from '../../IBMi'; import { getJavaHome } from '../../configuration/DebugConfiguration'; import { CompileTools } from '../../CompileTools'; -describe(`connection tests`, {concurrent: true}, () => { +describe(`connection tests`, {concurrent: true, timeout: 100000}, () => { let connection: IBMi beforeAll(async () => { connection = await newConnection(); diff --git a/src/api/tests/suites/content.test.ts b/src/api/tests/suites/content.test.ts index 8353e96e8..3d54d0839 100644 --- a/src/api/tests/suites/content.test.ts +++ b/src/api/tests/suites/content.test.ts @@ -8,7 +8,7 @@ import { Tools } from '../../Tools'; import { CONNECTION_TIMEOUT, disposeConnection, newConnection } from '../connection'; import { ModuleExport, ProgramExportImportInfo } from '../../types'; -describe('Content Tests', {concurrent: true}, () => { +describe('Content Tests', {concurrent: true, timeout: 100000}, () => { let connection: IBMi beforeAll(async () => { connection = await newConnection(); From fd6280c26ba3d554ced795df8ad9bd7575451f44 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Fri, 9 May 2025 12:53:25 -0400 Subject: [PATCH 16/24] Improvements to connection times Signed-off-by: worksofliam --- src/api/IBMi.ts | 2 +- src/api/tests/suites/asp.test.ts | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index eba0f330a..52abc7c02 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -1554,7 +1554,7 @@ export default class IBMi { let foundAsps = this.libraryAsps.get(library) || []; if (foundAsps.length === 0) { - const rows = await this.runSQL(`SELECT IASP_NUMBER from table(qsys2.object_statistics('*ALLAVL', '*LIB')) x where x.objname = '${this.sysNameInAmerican(library)}'`); + const rows = await this.runSQL(`SELECT IASP_NUMBER from table(qsys2.object_statistics('*ALLSIMPLE', '*LIB')) where objname = '${this.sysNameInAmerican(library)}'`); for (const row of rows) { if (row?.IASP_NUMBER !== undefined && row?.IASP_NUMBER !== null) { diff --git a/src/api/tests/suites/asp.test.ts b/src/api/tests/suites/asp.test.ts index ffd41f0b1..3a8f268ba 100644 --- a/src/api/tests/suites/asp.test.ts +++ b/src/api/tests/suites/asp.test.ts @@ -121,10 +121,19 @@ describe(`iASP tests`, { concurrent: true }, () => { ]); expect(resolved).toBeDefined(); + + const attrA = await connection.getContent().getAttributes({library: LIBNAME, name: SPFNAME, member: MBRNAME, asp: connection.getConfiguredIAsp()?.name}); + console.log(attrA); + expect(attrA).toBeDefined(); //TODO: additional expects + + setToAsp(connection); // Reset to *SYSBAS + const attrB = await connection.getContent().getAttributes({library: LIBNAME, name: SPFNAME, member: MBRNAME}); + expect(attrB).toBeUndefined(); }); - it('can get library info', async () => { + it('can get library info', {timeout: 1000000}, async () => { + // Long running test on systems with many libraries expect(connection.getConfiguredIAsp()).toBeDefined(); const librariesA = await connection.getContent().getLibraryList([`QSYS2`, LIBNAME]); From ae6c3f829edf4ccac79a059c1b4ad391c95424bb Mon Sep 17 00:00:00 2001 From: worksofliam Date: Fri, 9 May 2025 12:56:43 -0400 Subject: [PATCH 17/24] Update text on ASP setting Signed-off-by: worksofliam --- src/webviews/settings/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webviews/settings/index.ts b/src/webviews/settings/index.ts index 51f7c8620..b5240b774 100644 --- a/src/webviews/settings/index.ts +++ b/src/webviews/settings/index.ts @@ -92,8 +92,8 @@ export class SettingsUI { asps.push({ selected: config.chosenAsp === `*SYSBAS` || !asps.some(asp => asp.value === config.chosenAsp), value: `*SYSBAS`, - description: `*SYSBAS`, - text: `*SYSBAS` + text: `Whatever the user profile is set to via the job description.`, + description: `User profile iASP (${connection.getCurrentUserIAspName() || `*SYSBAS`})` }); sourceTab.addSelect(`chosenAsp`, `ASP / Database`, asps, `iASP that should be used when navigating file systems and executing commands. The iASP configured on your user profile is ${connection.getCurrentUserIAspName() || `*SYSBAS`} and changing this setting will override that while using Code for IBM i.`); From 4bf6a98e3d5eeafea8a763dc174026fe01d1ab16 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Mon, 12 May 2025 11:43:19 -0400 Subject: [PATCH 18/24] When no libl is used, fallback to system Signed-off-by: worksofliam --- src/api/CompileTools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/CompileTools.ts b/src/api/CompileTools.ts index 57bf14599..4a2c2d9e5 100644 --- a/src/api/CompileTools.ts +++ b/src/api/CompileTools.ts @@ -90,7 +90,7 @@ export namespace CompileTools { case `ile`: default: - if (connection.usingBash()) { + if (connection.usingBash() && !options.noLibList) { const chosenAsp = connection.getConfiguredIAsp(); const aspName = chosenAsp ? chosenAsp.name : `*NONE`; From 7c94d5c73fb3e09ac81030bac487145edae17714 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 15 May 2025 09:22:35 -0400 Subject: [PATCH 19/24] Rollback getLibraryList lookup Signed-off-by: worksofliam --- src/api/IBMiContent.ts | 2 +- src/api/tests/suites/asp.test.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index c2d0b0fa3..e7d044226 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -396,7 +396,7 @@ export default class IBMiContent { os.OBJOWNER AS OWNER, os.OBJDEFINER AS CREATED_BY from table( SYSTOOLS.SPLIT( INPUT_LIST => '${libraries.toString()}', DELIMITER => ',' ) ) libs, - table( QSYS2.OBJECT_STATISTICS( OBJECT_SCHEMA => '*ALLUSR', OBJTYPELIST => '*LIB', OBJECT_NAME => libs.ELEMENT ) ) os + table( QSYS2.OBJECT_STATISTICS( OBJECT_SCHEMA => 'QSYS', OBJTYPELIST => '*LIB', OBJECT_NAME => libs.ELEMENT ) ) os `; const results = await this.ibmi.runSQL(statement); diff --git a/src/api/tests/suites/asp.test.ts b/src/api/tests/suites/asp.test.ts index 3a8f268ba..aa3f03c2d 100644 --- a/src/api/tests/suites/asp.test.ts +++ b/src/api/tests/suites/asp.test.ts @@ -136,14 +136,16 @@ describe(`iASP tests`, { concurrent: true }, () => { // Long running test on systems with many libraries expect(connection.getConfiguredIAsp()).toBeDefined(); - const librariesA = await connection.getContent().getLibraryList([`QSYS2`, LIBNAME]); - expect(librariesA.length).toBe(2); + const librariesA = await connection.getContent().getLibraryList([`QTEMP`, `QSYS2`, LIBNAME]); + expect(librariesA.length).toBe(3); + expect(librariesA.some(lib => lib.name === `QTEMP`)).toBeTruthy(); expect(librariesA.some(lib => lib.name === `QSYS2`)).toBeTruthy(); expect(librariesA.some(lib => lib.name === LIBNAME)).toBeTruthy(); setToAsp(connection); // Reset to *SYSBAS - const librariesB = await connection.getContent().getLibraryList([`QSYS2`, LIBNAME]); - expect(librariesB.length).toBe(2); + const librariesB = await connection.getContent().getLibraryList([`QTEMP`, `QSYS2`, LIBNAME]); + expect(librariesB.length).toBe(3); + expect(librariesB.some(lib => lib.name === `QTEMP`)).toBeTruthy(); expect(librariesB.some(lib => lib.name === `QSYS2`)).toBeTruthy(); const notFound = librariesB.find(lib => lib.name === LIBNAME); From b046014c39f1db367a0d76ca5e5142ae3eadc889 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 15 May 2025 09:54:05 -0400 Subject: [PATCH 20/24] Add additional environment type for backwards compatability Signed-off-by: worksofliam --- src/api/CompileTools.ts | 76 +++++++++++++++++++++---------------- src/api/tests/connection.ts | 2 +- src/api/types.ts | 2 +- src/ui/actions.ts | 4 +- 4 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/api/CompileTools.ts b/src/api/CompileTools.ts index 4a2c2d9e5..bd69e98c1 100644 --- a/src/api/CompileTools.ts +++ b/src/api/CompileTools.ts @@ -57,6 +57,17 @@ export namespace CompileTools { events.writeEvent(`Commands:\n${commands.map(command => `\t${command}\n`).join(``)}` + NEWLINE); } + if (!options.environment) { + // When no environment is specified, we default to CL. + options.environment = `ile`; + } + + if (options.environment === `ile` && !connection.usingBash()) { + // Since bash is not available, we can't use cl, so we have to use system + options.environment = `system`; + events.writeEvent?.(`Warning: 'cl' built-in is not supported in this shell. Using system instead. This means the ASP is picked up from the user profile JOBD and not the connection configuration.\n`); + } + const callbacks: StandardIO = events.writeEvent ? { onStdout: (data) => { events.writeEvent!(data.toString().replaceAll(`\n`, NEWLINE)); @@ -67,6 +78,7 @@ export namespace CompileTools { } : {}; let commandResult; + switch (options.environment) { case `pase`: commandResult = await connection.sendCommand({ @@ -89,39 +101,39 @@ export namespace CompileTools { break; case `ile`: + const chosenAsp = connection.getConfiguredIAsp(); + const aspName = chosenAsp ? chosenAsp.name : `*NONE`; + + const setAspGrp = connection.getContent().toCl(`SETASPGRP`, { + ASPGRP: aspName, + CURLIB: ileSetup.currentLibrary, + USRLIBL: ileSetup.libraryList.join(` `), + }); + + commandResult = await connection.sendCommand({ + command: [ + `cl "${setAspGrp}"`, + ...commands.map(command => + `${`cl "${IBMi.escapeForShell(command)}"`}`, + ) + ].join(` && `), + directory: cwd, + ...callbacks + }); + break; + + case `system`: default: - if (connection.usingBash() && !options.noLibList) { - const chosenAsp = connection.getConfiguredIAsp(); - const aspName = chosenAsp ? chosenAsp.name : `*NONE`; - - const setAspGrp = connection.getContent().toCl(`SETASPGRP`, { - ASPGRP: aspName, - CURLIB: ileSetup.currentLibrary, - USRLIBL: ileSetup.libraryList.join(` `), - }); - - commandResult = await connection.sendCommand({ - command: [ - `cl "${setAspGrp}"`, - ...commands.map(command => - `${`cl "${IBMi.escapeForShell(command)}"`}`, - ) - ].join(` && `), - directory: cwd, - ...callbacks - }); - } else { - commandResult = await connection.sendQsh({ - command: [ - ...options.noLibList? [] : buildLiblistCommands(connection, ileSetup), - ...commands.map(command => - `${`system "${IBMi.escapeForShell(command)}"`}`, - ) - ].join(` && `), - directory: cwd, - ...callbacks - }); - } + commandResult = await connection.sendQsh({ + command: [ + ...options.noLibList? [] : buildLiblistCommands(connection, ileSetup), + ...commands.map(command => + `${`system "${IBMi.escapeForShell(command)}"`}`, + ) + ].join(` && `), + directory: cwd, + ...callbacks + }); break; } diff --git a/src/api/tests/connection.ts b/src/api/tests/connection.ts index 49bc584d8..d73af3036 100644 --- a/src/api/tests/connection.ts +++ b/src/api/tests/connection.ts @@ -72,7 +72,7 @@ export async function newConnection() { }; // Override this so not to spam the console. - conn.appendOutput = (data) => {}; + // conn.appendOutput = (data) => {}; const result = await conn.connect( creds, diff --git a/src/api/types.ts b/src/api/types.ts index a57546dea..75329e175 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -13,7 +13,7 @@ export interface StandardIO { */ export type ActionType = "member" | "streamfile" | "object" | "file"; export type ActionRefresh = "no" | "parent" | "filter" | "browser"; -export type ActionEnvironment = "ile" | "qsh" | "pase"; +export type ActionEnvironment = "ile" | "system" | "qsh" | "pase"; export enum CcsidOrigin { User = "user", diff --git a/src/ui/actions.ts b/src/ui/actions.ts index 780e0ceae..3012ef7ba 100644 --- a/src/ui/actions.ts +++ b/src/ui/actions.ts @@ -110,7 +110,7 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur const chosenAction = customAction || ((availableActions.length === 1) ? availableActions[0] : await vscode.window.showQuickPick(availableActions))?.action; if (chosenAction) { actionUsed.set(chosenAction.name, Date.now()); - const environment = chosenAction.environment || `ile`; + let environment = chosenAction.environment || `cl`; let workspaceId: number | undefined = undefined; @@ -383,7 +383,7 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur if (commandResult && commandResult.code !== CompileTools.DID_NOT_RUN) { target.hasRun = true; - const isIleCommand = environment === `ile`; + const isIleCommand = [`ile`, `cl`].includes(environment); const useLocalEvfevent = fromWorkspace && chosenAction.postDownload && From cb5a65333c1bb1b0c3cc739cac4a4904e53bb263 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 15 May 2025 14:20:00 -0400 Subject: [PATCH 21/24] Remove use of CL Signed-off-by: worksofliam --- src/ui/actions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/actions.ts b/src/ui/actions.ts index 3012ef7ba..7cd549785 100644 --- a/src/ui/actions.ts +++ b/src/ui/actions.ts @@ -10,7 +10,7 @@ import { getBranchLibraryName, getEnvConfig } from '../filesystems/local/env'; import { getGitBranch } from '../filesystems/local/git'; import { parseFSOptions } from '../filesystems/qsys/QSysFs'; import Instance from '../Instance'; -import { Action, DeploymentMethod } from '../typings'; +import { Action, ActionEnvironment, DeploymentMethod } from '../typings'; import { CustomUI, TreeListItem } from '../webviews/CustomUI'; import { EvfEventInfo, refreshDiagnosticsFromLocal, refreshDiagnosticsFromServer, registerDiagnostics } from './diagnostics'; @@ -110,7 +110,7 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur const chosenAction = customAction || ((availableActions.length === 1) ? availableActions[0] : await vscode.window.showQuickPick(availableActions))?.action; if (chosenAction) { actionUsed.set(chosenAction.name, Date.now()); - let environment = chosenAction.environment || `cl`; + let environment: ActionEnvironment = chosenAction.environment || `ile`; let workspaceId: number | undefined = undefined; @@ -383,7 +383,7 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur if (commandResult && commandResult.code !== CompileTools.DID_NOT_RUN) { target.hasRun = true; - const isIleCommand = [`ile`, `cl`].includes(environment); + const isIleCommand = [`ile`, `system`].includes(environment); const useLocalEvfevent = fromWorkspace && chosenAction.postDownload && From cbdab01b87450db8f96f1bfe56a0df7d36a969a6 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 15 May 2025 14:23:52 -0400 Subject: [PATCH 22/24] Additional comment for lookupLibraryIAsp Signed-off-by: worksofliam --- src/api/IBMi.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 52abc7c02..5b3d45963 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -1554,6 +1554,9 @@ export default class IBMi { let foundAsps = this.libraryAsps.get(library) || []; if (foundAsps.length === 0) { + // We're using *ALLSIMPLE here because it's faster than *ALLAVL. + // The downside to this is that we can't tell if the user is request a library + // that is in a different ASP. We're trading off a bit of accuracy for speed. const rows = await this.runSQL(`SELECT IASP_NUMBER from table(qsys2.object_statistics('*ALLSIMPLE', '*LIB')) where objname = '${this.sysNameInAmerican(library)}'`); for (const row of rows) { From 959b1e6ce9aabccc610cbd4d462f8ebd6b756457 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 15 May 2025 14:24:56 -0400 Subject: [PATCH 23/24] Fix up test logging Signed-off-by: worksofliam --- src/api/tests/connection.ts | 2 +- src/api/tests/suites/asp.test.ts | 1 - src/api/tests/suites/content.test.ts | 1 - src/api/tests/suites/encoding.test.ts | 4 ---- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/api/tests/connection.ts b/src/api/tests/connection.ts index d73af3036..49bc584d8 100644 --- a/src/api/tests/connection.ts +++ b/src/api/tests/connection.ts @@ -72,7 +72,7 @@ export async function newConnection() { }; // Override this so not to spam the console. - // conn.appendOutput = (data) => {}; + conn.appendOutput = (data) => {}; const result = await conn.connect( creds, diff --git a/src/api/tests/suites/asp.test.ts b/src/api/tests/suites/asp.test.ts index aa3f03c2d..34c02e5ab 100644 --- a/src/api/tests/suites/asp.test.ts +++ b/src/api/tests/suites/asp.test.ts @@ -123,7 +123,6 @@ describe(`iASP tests`, { concurrent: true }, () => { expect(resolved).toBeDefined(); const attrA = await connection.getContent().getAttributes({library: LIBNAME, name: SPFNAME, member: MBRNAME, asp: connection.getConfiguredIAsp()?.name}); - console.log(attrA); expect(attrA).toBeDefined(); //TODO: additional expects diff --git a/src/api/tests/suites/content.test.ts b/src/api/tests/suites/content.test.ts index 3d54d0839..6cd30ba87 100644 --- a/src/api/tests/suites/content.test.ts +++ b/src/api/tests/suites/content.test.ts @@ -651,7 +651,6 @@ describe('Content Tests', {concurrent: true, timeout: 100000}, () => { const id = `${Tools.makeid().toUpperCase()}`; await connection.withTempDirectory(async directory => { const source = `${directory}/vscodetemp-${id}.clle`; - console.log(source); try { await content.runStatements( `CALL QSYS2.IFS_WRITE(PATH_NAME =>'${source}', diff --git a/src/api/tests/suites/encoding.test.ts b/src/api/tests/suites/encoding.test.ts index 30e5a6b17..731cf6490 100644 --- a/src/api/tests/suites/encoding.test.ts +++ b/src/api/tests/suites/encoding.test.ts @@ -96,9 +96,6 @@ describe('Encoding tests', { concurrent: true }, () => { const qshEscapeResult = await connection?.sendQsh({ command: printEscapeChar }); const paseEscapeResult = await connection?.sendCommand({ command: printEscapeChar }); - console.log(qshEscapeResult?.stdout); - console.log(paseEscapeResult?.stdout); - const qshTextResultA = await connection?.sendQsh({ command: basicCommandA }); const paseTextResultA = await connection?.sendCommand({ command: basicCommandA }); @@ -340,7 +337,6 @@ describe('Encoding tests', { concurrent: true }, () => { await connection.getContent().uploadMemberContent(tempLib, testFile, testMember, [`**free`, `dsply 'Hello world';`, ` `, ` `, `return;`].join(`\n`)); const compileResult = await connection.runCommand({ command: `CRTBNDRPG PGM(${tempLib}/${testMember}) SRCFILE(${tempLib}/${testFile}) SRCMBR(${testMember})`, noLibList: true }); - console.log(compileResult); expect(compileResult.code).toBe(0); if (compileResult.code === 0) { From bd46718a2474c54ce69d559d465b8be02d55402c Mon Sep 17 00:00:00 2001 From: worksofliam Date: Wed, 21 May 2025 10:37:23 -0400 Subject: [PATCH 24/24] Use cl for running SQL statements under the hood Signed-off-by: worksofliam --- src/api/IBMi.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 5b3d45963..e36cd203e 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -477,6 +477,14 @@ export default class IBMi { callbacks.message(`warning`, `IBM i ${this.systemVersion} is not supported. Code for IBM i only supports 7.3 and above. Some features may not work correctly.`); } + const commandShellResult = await this.sendCommand({ + command: `echo $SHELL` + }); + + if (commandShellResult.code === 0) { + this.shell = commandShellResult.stdout.trim(); + } + callbacks.progress({ message: `Checking Code for IBM i components.` }); await this.componentManager.startup(quickConnect() ? cachedServerSettings?.installedComponents : []); @@ -578,14 +586,6 @@ export default class IBMi { }); } - const commandShellResult = await this.sendCommand({ - command: `echo $SHELL` - }); - - if (commandShellResult.code === 0) { - this.shell = commandShellResult.stdout.trim(); - } - // Check for bad data areas? if (quickConnect() && cachedServerSettings?.badDataAreasChecked === true) { // Do nothing, bad data areas are already checked. @@ -1364,8 +1364,10 @@ export default class IBMi { let command = `${IBMi.locale} system "call QSYS/QZDFMDB2 PARM('-d' '-i' '-t' ${rdbParameter})"` let useCsv = options.forceSafe; - // Use custom QSH if available - if (this.canUseCqsh) { + if (this.usingBash()) { + command = `${IBMi.locale} cl "call QSYS/QZDFMDB2 PARM('-d' '-i' '-t' ${rdbParameter})"`; + } else if (this.canUseCqsh) { + // Use custom QSH if available const customQsh = this.getComponent(CustomQSh.ID)!; command = `${IBMi.locale} ${customQsh.installPath} -c "system \\"call QSYS/QZDFMDB2 PARM('-d' '-i' '-t' ${rdbParameter})\\""`; }