diff --git a/src/api/CompileTools.ts b/src/api/CompileTools.ts index 1e4b3c322..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,11 +101,32 @@ 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: - // escape $ and # in commands commandResult = await connection.sendQsh({ command: [ - ...options.noLibList ? [] : buildLiblistCommands(connection, ileSetup), + ...options.noLibList? [] : buildLiblistCommands(connection, ileSetup), ...commands.map(command => `${`system "${IBMi.escapeForShell(command)}"`}`, ) @@ -101,6 +134,7 @@ export namespace CompileTools { directory: cwd, ...callbacks }); + break; } diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 8d8617afd..e36cd203e 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -105,8 +105,8 @@ export default class IBMi { * the root of the IFS, thus why we store it. */ private iAspInfo: AspInfo[] = []; - private currentAsp: string|undefined; - private libraryAsps = new Map(); + private userProfileIAsp: string|undefined; + private libraryAsps = new Map(); /** * @deprecated Will be replaced with {@link IBMi.getAllIAsps} in v3.0.0 @@ -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. @@ -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`]) { @@ -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`) { @@ -781,7 +781,27 @@ export default class IBMi { message: `Fetching current iASP information.` }); - this.currentAsp = await this.getUserProfileAsp(); + this.userProfileIAsp = await this.getUserProfileAsp(); + + if (this.usingBash()) { + // Set the default ASP to the current ASP if it is not set. + const chosenAspIsConfigured = this.getConfiguredIAsp(); + 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 { + // 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? if (quickConnect() && cachedServerSettings?.jobCcsid !== null && cachedServerSettings?.userDefaultCCSID && cachedServerSettings?.qccsid) { @@ -1329,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` : ''); @@ -1337,13 +1357,19 @@ 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 && options.ignoreDatabase !== true ? `'-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) { + 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')\\""`; + command = `${IBMi.locale} ${customQsh.installPath} -c "system \\"call QSYS/QZDFMDB2 PARM('-d' '-i' '-t' ${rdbParameter})\\""`; } if (this.requiresTranslation) { @@ -1466,7 +1492,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]; @@ -1479,6 +1505,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; } @@ -1491,40 +1528,81 @@ 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() { - return this.currentAsp; + getCurrentUserIAspName() { + return this.userProfileIAsp; } + + getConfiguredIAsp(): AspInfo|undefined { + const selected = this.config?.chosenAsp; + + 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) { + // 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) { + 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); } /** diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index e9f12bd15..e7d044226 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) + ]); - if (result.stderr) { + 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 && result.stderr) { const lines = result.stderr.split(`\n`); lines.forEach(line => { @@ -514,7 +542,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.`); + } } } @@ -877,7 +917,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.getConfiguredIAsp()?.name; if (asp && asp.length > 0) { return [ Tools.qualifyPath(inAmerican(file.library), inAmerican(file.name), inAmerican(member), asp, true), 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/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..34c02e5ab 100644 --- a/src/api/tests/suites/asp.test.ts +++ b/src/api/tests/suites/asp.test.ts @@ -8,22 +8,26 @@ 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.getCurrentIAspName(); - if (!currentAsp) return false; + const currentAsp = connection.getCurrentUserIAspName(); + if (currentAsp !== undefined) return false; return true; } -async function ensureLibExists(connection: IBMi) { - const detail = connection.getIAspDetail(connection.getCurrentIAspName()!)!; - const res = await connection.runCommand({ command: `CRTLIB LIB(${LIBNAME}) ASPDEV(${detail.name})` }); - if (res.code) { - assert.strictEqual(res.code, 0, res.stderr || res.stdout); - } +async function ensureLibExists(connection: IBMi, aspName: string) { + const res = await connection.runCommand({ command: `CRTLIB LIB(${LIBNAME}) ASPDEV(${aspName})` }); +} + +async function setToAsp(connection: IBMi, name?: string) { + connection.getConfig().chosenAsp = name || `*SYSBAS`; } async function createTempRpgle(connection: IBMi) { @@ -35,7 +39,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 +53,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,35 +69,104 @@ describe(`iASP tests`, { concurrent: true }, () => { }, CONNECTION_TIMEOUT) afterAll(async () => { - await connection.runCommand({ command: `DLTLIB LIB(${LIBNAME})` }); - disposeConnection(connection); + if (checkAsps(connection)) { + setToAsp(connection, connection.getAllIAsps()[0].name); + + await connection.runCommand({ command: `DLTLIB LIB(${LIBNAME})` }); + disposeConnection(connection); + } }); beforeEach((t) => { 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 } ]); expect(resolved).toBeDefined(); + + const attrA = await connection.getContent().getAttributes({library: LIBNAME, name: SPFNAME, member: MBRNAME, asp: connection.getConfiguredIAsp()?.name}); + 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', {timeout: 1000000}, async () => { + // Long running test on systems with many libraries + expect(connection.getConfiguredIAsp()).toBeDefined(); + + 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([`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); + expect(notFound).toBeTruthy(); + 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 + + expect(connection.getConfiguredIAsp()).toBeUndefined(); }); }); 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..6cd30ba87 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(); @@ -651,7 +651,6 @@ describe('Content Tests', {concurrent: true}, () => { 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) { 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(); 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/commands/actions.ts b/src/commands/actions.ts index a67bebbf6..e5ff2a003 100644 --- a/src/commands/actions.ts +++ b/src/commands/actions.ts @@ -127,7 +127,7 @@ export function registerActionsCommands(instance: Instance): Disposable[] { } break; case `streamfile`: - detail.asp = connection.getCurrentIAspName(); + detail.asp = connection.getConfiguredIAsp()?.name; detail.lib = config.currentLibrary; break; } 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/ui/actions.ts b/src/ui/actions.ts index 780e0ceae..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()); - const environment = chosenAction.environment || `ile`; + 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 = environment === `ile`; + const isIleCommand = [`ile`, `system`].includes(environment); const useLocalEvfevent = fromWorkspace && chosenAction.postDownload && diff --git a/src/ui/views/objectBrowser.ts b/src/ui/views/objectBrowser.ts index ab8930d5a..d900a85e3 100644 --- a/src/ui/views/objectBrowser.ts +++ b/src/ui/views/objectBrowser.ts @@ -999,7 +999,7 @@ Do you want to replace it?`, item.name), { modal: true }, skipAllLabel, overwrit const connection = getConnection(); if (!parameters.some(p => p.path.split('/')[1] === '*ALL')) { - const selectedAsp = connection.getCurrentIAspName(); + 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 e94edb558..b5240b774 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) { @@ -77,8 +79,30 @@ 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) { + 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`, + 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.`); + } 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 - .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(`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) @@ -252,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) { @@ -286,9 +313,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())