From c4de790602133dda1e9cf2633bc63071a9ed218a Mon Sep 17 00:00:00 2001 From: worksofliam Date: Wed, 28 Aug 2024 10:49:54 -0400 Subject: [PATCH 01/28] Extend the base API to allow other extensions to add their own components. Signed-off-by: worksofliam --- src/api/IBMi.ts | 4 +-- src/components/component.ts | 50 +++---------------------------------- src/components/manager.ts | 49 ++++++++++++++++++++++++++++++++++++ src/extension.ts | 9 ++++++- src/typings.ts | 4 ++- 5 files changed, 66 insertions(+), 50 deletions(-) create mode 100644 src/components/manager.ts diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index b53ba312f..4317e81c9 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -6,7 +6,6 @@ import { parse } from 'csv-parse/sync'; import { existsSync } from "fs"; import os from "os"; import path from 'path'; -import { ComponentId, ComponentManager } from "../components/component"; import { CopyToImport } from "../components/copyToImport"; import { instance } from "../instantiate"; import { CommandData, CommandResult, ConnectionData, IBMiMember, RemoteCommand, SpecialAuthorities, WrapResult } from "../typings"; @@ -17,6 +16,7 @@ import { Tools } from './Tools'; import * as configVars from './configVars'; import { DebugConfiguration } from "./debug/config"; import { debugPTFInstalled } from "./debug/server"; +import { ComponentManager } from "../components/manager"; export interface MemberParts extends IBMiMember { basename: string @@ -1341,7 +1341,7 @@ export default class IBMi { } } - getComponent(id: ComponentId) { + getComponent(id: string) { return this.components.get(id); } diff --git a/src/components/component.ts b/src/components/component.ts index e40002c62..4f5518d17 100644 --- a/src/components/component.ts +++ b/src/components/component.ts @@ -1,7 +1,4 @@ import IBMi from "../api/IBMi"; -import { CopyToImport } from "./copyToImport"; -import { GetMemberInfo } from "./getMemberInfo"; -import { GetNewLibl } from "./getNewLibl"; export enum ComponentState { NotChecked = `NotChecked`, @@ -9,53 +6,14 @@ export enum ComponentState { Installed = `Installed`, Error = `Error`, } -interface ComponentRegistry { - GetNewLibl?: GetNewLibl; - CopyToImport?: CopyToImport; - GetMemberInfo?: GetMemberInfo; -} - -export type ComponentId = keyof ComponentRegistry; -export abstract class ComponentT { +export class ComponentT { public state: ComponentState = ComponentState.NotChecked; public currentVersion: number = 0; constructor(public connection: IBMi) { } - abstract getInstalledVersion(): Promise; - abstract checkState(): Promise - abstract getState(): ComponentState; -} - -export class ComponentManager { - private registered: ComponentRegistry = {}; - - public async startup(connection: IBMi) { - this.registered.GetNewLibl = new GetNewLibl(connection); - await ComponentManager.checkState(this.registered.GetNewLibl); - - this.registered.CopyToImport = new CopyToImport(connection); - await ComponentManager.checkState(this.registered.CopyToImport); - - this.registered.GetMemberInfo = new GetMemberInfo(connection); - await ComponentManager.checkState(this.registered.GetMemberInfo); - } - - // TODO: return type based on ComponentIds - get(id: ComponentId): T | undefined { - const component = this.registered[id]; - if (component && component.getState() === ComponentState.Installed) { - return component as T; - } - } - - private static async checkState(component: ComponentT) { - try { - await component.checkState(); - } catch (e) { - console.log(component); - console.log(`Error checking state for ${component.constructor.name}`, e); - } - } + async getInstalledVersion(): Promise {return}; + async checkState(): Promise {return false} + getState(): ComponentState {return this.state}; } \ No newline at end of file diff --git a/src/components/manager.ts b/src/components/manager.ts new file mode 100644 index 000000000..bf66cea0b --- /dev/null +++ b/src/components/manager.ts @@ -0,0 +1,49 @@ +import IBMi from "../api/IBMi"; +import { ComponentState, ComponentT } from "./component"; +import { CopyToImport } from "./copyToImport"; +import { GetMemberInfo } from "./getMemberInfo"; +import { GetNewLibl } from "./getNewLibl"; + +export class ComponentRegistry { + private allComponents: (typeof ComponentT)[] = [GetNewLibl, CopyToImport, GetMemberInfo]; + + public registerComponent(component: typeof ComponentT) { + this.allComponents.push(component); + } + + public getComponents() { + return this.allComponents; + } +} + +export const ExtensionComponentRegistry = new ComponentRegistry(); + +interface ComponentList {[name: string]: ComponentT}; + +export class ComponentManager { + private registered: ComponentList = {}; + + public async startup(connection: IBMi) { + for (const Component of ExtensionComponentRegistry.getComponents()) { + const instance = new Component(connection); + this.registered[Component.name] = instance; + await ComponentManager.checkState(instance); + } + } + + get(id: string): T | undefined { + const component = this.registered[id]; + if (component && component.getState() === ComponentState.Installed) { + return component as T; + } + } + + private static async checkState(component: ComponentT) { + try { + await component.checkState(); + } catch (e) { + console.log(component); + console.log(`Error checking state for ${component.constructor.name}`, e); + } + } +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index a2b4c97bc..67f89f50f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -31,6 +31,7 @@ import { initializeIFSBrowser } from "./views/ifsBrowser"; import { initializeObjectBrowser } from "./views/objectBrowser"; import { initializeSearchView } from "./views/searchView"; import { SettingsUI } from "./webviews/settings"; +import { ExtensionComponentRegistry } from "./components/manager"; export async function activate(context: ExtensionContext): Promise { // Use the console to output diagnostic information (console.log) and errors (console.error) @@ -125,7 +126,13 @@ export async function activate(context: ExtensionContext): Promise commands.executeCommand("code-for-ibmi.refreshProfileView"); }); - return { instance, customUI: () => new CustomUI(), deployTools: DeployTools, evfeventParser: parseErrors, tools: Tools }; + return { + instance, customUI: () => new CustomUI(), + deployTools: DeployTools, + evfeventParser: parseErrors, + tools: Tools, + componentRegistry: ExtensionComponentRegistry + }; } async function fixLoginSettings() { diff --git a/src/typings.ts b/src/typings.ts index cceef76f0..381b4e6cb 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -5,13 +5,15 @@ import { CustomUI } from "./api/CustomUI"; import Instance from "./api/Instance"; import { Tools } from "./api/Tools"; import { DeployTools } from "./api/local/deployTools"; +import { ComponentRegistry } from './components/manager'; export interface CodeForIBMi { instance: Instance, customUI: () => CustomUI, deployTools: typeof DeployTools, evfeventParser: (lines: string[]) => Map, - tools: typeof Tools + tools: typeof Tools, + componentRegistry: ComponentRegistry } export type DeploymentMethod = "all" | "staged" | "unstaged" | "changed" | "compare"; From 3d4a6115564a55166b7b211a596bec6ed62a87f7 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Mon, 2 Sep 2024 21:33:16 -0400 Subject: [PATCH 02/28] Implement custom casting function Signed-off-by: worksofliam --- src/api/IBMi.ts | 3 +-- src/api/Tools.ts | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index fe2efe28b..fca706830 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -1416,10 +1416,9 @@ export default class IBMi { return parse(csvContent, { columns: true, skip_empty_lines: true, - cast: true, onRecord(record) { for (const key of Object.keys(record)) { - record[key] = record[key] === ` ` ? `` : record[key]; + record[key] = record[key] === ` ` ? `` : Tools.assumeType(record[key]); } return record; } diff --git a/src/api/Tools.ts b/src/api/Tools.ts index 38433c105..27b4d0ef0 100644 --- a/src/api/Tools.ts +++ b/src/api/Tools.ts @@ -390,6 +390,13 @@ export namespace Tools { } } + export function assumeType(str: string) { + // The number is already generated on the server. + // So, we assume that if the string starts with a 0, it is a string. + if (str[0] === `0` || str.length > 10) return str; + return Number(str) || str; + } + const activeContexts: Map = new Map; /** * Runs a function while a context value is set to true. From 00482be4e9d85dc20a480650ad954711af45f2d6 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Mon, 2 Sep 2024 21:33:36 -0400 Subject: [PATCH 03/28] Remove use of deprecated function Signed-off-by: worksofliam --- src/filesystems/qsys/extendedContent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/filesystems/qsys/extendedContent.ts b/src/filesystems/qsys/extendedContent.ts index 477bd3154..85928f2a3 100644 --- a/src/filesystems/qsys/extendedContent.ts +++ b/src/filesystems/qsys/extendedContent.ts @@ -52,11 +52,11 @@ export class ExtendedIBMiContent { let rows; if (sourceColourSupport) - rows = await content.runSQL( + rows = await connection.runSQL( `select srcdat, rtrim(translate(srcdta, ${SEU_GREEN_UL_RI_temp}, ${SEU_GREEN_UL_RI})) as srcdta from ${aliasPath}` ); else - rows = await content.runSQL( + rows = await connection.runSQL( `select srcdat, srcdta from ${aliasPath}` ); From 178c3912c703265301a30a2a50e5db312797973f Mon Sep 17 00:00:00 2001 From: worksofliam Date: Mon, 2 Sep 2024 21:34:31 -0400 Subject: [PATCH 04/28] Implement test for ensuring correct lines are returned Signed-off-by: worksofliam --- src/testing/content.ts | 46 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/testing/content.ts b/src/testing/content.ts index a99b78ae3..9074f43ce 100644 --- a/src/testing/content.ts +++ b/src/testing/content.ts @@ -278,6 +278,52 @@ export const ContentSuite: TestSuite = { }, }, + {name: `Ensure source lines are correct`, test: async () => { + const connection = instance.getConnection(); + const config = instance.getConfig()!; + + assert.ok(config.enableSourceDates, `Source dates must be enabled for this test.`); + + const tempLib = config!.tempLibrary; + const file = `LINES`; + const member = `THEMEMBER`; + + await connection!.runCommand({ command: `CRTSRCPF FILE(${tempLib}/${file}) RCDLEN(112)`, noLibList: true }); + await connection!.runCommand({ command: `ADDPFM FILE(${tempLib}/${file}) MBR(${member}) SRCTYPE(TXT)`, noLibList: true }); + + const aliasName = `${tempLib}.test_${file}_${member}`; + await connection?.runSQL(`CREATE OR REPLACE ALIAS ${aliasName} for "${tempLib}"."${file}"("${member}")`); + + try { + await connection?.runSQL(`delete from ${aliasName}`); + } catch (e) {} + + const inLines = [ + `Hello world`, + `1`, + `001`, + `0002`, + `00003`, + ] + + const lines = [ + `insert into ${aliasName} (srcseq, srcdat, srcdta)`, + `values `, + inLines.map((line, index) => `(${index + 1}.00, 0, '${line}')`).join(`, `), + ]; + + await connection?.runSQL(lines.join(` `)); + + const theBadOneUri = getMemberUri({ library: tempLib, file, name: member, extension: `TXT` }); + + const memberContentBuf = await workspace.fs.readFile(theBadOneUri); + const fileContent = new TextDecoder().decode(memberContentBuf); + + const outLines = fileContent.split(`\n`); + + assert.deepStrictEqual(inLines, outLines); + }}, + { name: `Test runSQL (basic select)`, test: async () => { const content = instance.getContent(); From 51e55df71ef1b2900d1b62156398c26f6d6787c3 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Wed, 18 Sep 2024 17:47:53 +0200 Subject: [PATCH 05/28] Code cleanup Signed-off-by: Seb Julliand --- src/components/manager.ts | 14 ++++++-------- src/extension.ts | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/manager.ts b/src/components/manager.ts index bf66cea0b..a34ccf12a 100644 --- a/src/components/manager.ts +++ b/src/components/manager.ts @@ -5,7 +5,7 @@ import { GetMemberInfo } from "./getMemberInfo"; import { GetNewLibl } from "./getNewLibl"; export class ComponentRegistry { - private allComponents: (typeof ComponentT)[] = [GetNewLibl, CopyToImport, GetMemberInfo]; + private readonly allComponents: (typeof ComponentT)[] = [GetNewLibl, CopyToImport, GetMemberInfo]; public registerComponent(component: typeof ComponentT) { this.allComponents.push(component); @@ -16,23 +16,21 @@ export class ComponentRegistry { } } -export const ExtensionComponentRegistry = new ComponentRegistry(); - -interface ComponentList {[name: string]: ComponentT}; +export const extensionComponentRegistry = new ComponentRegistry(); export class ComponentManager { - private registered: ComponentList = {}; + private readonly registered: Map = new Map; public async startup(connection: IBMi) { - for (const Component of ExtensionComponentRegistry.getComponents()) { + for (const Component of extensionComponentRegistry.getComponents()) { const instance = new Component(connection); - this.registered[Component.name] = instance; + this.registered.set(Component.name, instance); await ComponentManager.checkState(instance); } } get(id: string): T | undefined { - const component = this.registered[id]; + const component = this.registered.get(id); if (component && component.getState() === ComponentState.Installed) { return component as T; } diff --git a/src/extension.ts b/src/extension.ts index 67f89f50f..0f0965f4d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,7 @@ import * as Debug from './api/debug'; import { parseErrors } from "./api/errors/parser"; import { DeployTools } from "./api/local/deployTools"; import { Deployment } from "./api/local/deployment"; +import { extensionComponentRegistry } from "./components/manager"; import { IFSFS } from "./filesystems/ifsFs"; import { LocalActionCompletionItemProvider } from "./languages/actions/completion"; import { updateLocale } from "./locale"; @@ -31,7 +32,6 @@ import { initializeIFSBrowser } from "./views/ifsBrowser"; import { initializeObjectBrowser } from "./views/objectBrowser"; import { initializeSearchView } from "./views/searchView"; import { SettingsUI } from "./webviews/settings"; -import { ExtensionComponentRegistry } from "./components/manager"; export async function activate(context: ExtensionContext): Promise { // Use the console to output diagnostic information (console.log) and errors (console.error) @@ -131,7 +131,7 @@ export async function activate(context: ExtensionContext): Promise deployTools: DeployTools, evfeventParser: parseErrors, tools: Tools, - componentRegistry: ExtensionComponentRegistry + componentRegistry: extensionComponentRegistry }; } From d440c7286f495265f385afb7609a88b42d2870d3 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Wed, 18 Sep 2024 23:41:37 +0200 Subject: [PATCH 06/28] Components management refactoring Signed-off-by: Seb Julliand --- src/api/IBMi.ts | 13 ++-- src/components/component.ts | 77 +++++++++++++++++--- src/components/copyToImport.ts | 35 ++++----- src/components/getMemberInfo.ts | 122 ++++++++++++-------------------- src/components/getNewLibl.ts | 84 +++++++++------------- src/components/manager.ts | 31 ++++---- src/instantiate.ts | 14 ++-- src/testing/components.ts | 4 +- src/views/ProfilesView.ts | 2 +- 9 files changed, 192 insertions(+), 190 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 4317e81c9..599bf0104 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -6,7 +6,9 @@ import { parse } from 'csv-parse/sync'; import { existsSync } from "fs"; import os from "os"; import path from 'path'; +import { IBMiComponent, IBMiComponentType } from "../components/component"; import { CopyToImport } from "../components/copyToImport"; +import { ComponentManager } from "../components/manager"; import { instance } from "../instantiate"; import { CommandData, CommandResult, ConnectionData, IBMiMember, RemoteCommand, SpecialAuthorities, WrapResult } from "../typings"; import { CompileTools } from "./CompileTools"; @@ -16,7 +18,6 @@ import { Tools } from './Tools'; import * as configVars from './configVars'; import { DebugConfiguration } from "./debug/config"; import { debugPTFInstalled } from "./debug/server"; -import { ComponentManager } from "../components/manager"; export interface MemberParts extends IBMiMember { basename: string @@ -55,7 +56,7 @@ export default class IBMi { /** User default CCSID is job default CCSID */ private userDefaultCCSID: number = 0; - private components: ComponentManager = new ComponentManager(); + private componentManager = new ComponentManager(this); client: node_ssh.NodeSSH; currentHost: string = ``; @@ -927,7 +928,7 @@ export default class IBMi { } progress.report({ message: `Checking Code for IBM i components.` }); - await this.components.startup(this); + await this.componentManager.startup(); if (!reconnecting) { vscode.workspace.getConfiguration().update(`workbench.editor.enablePreview`, false, true); @@ -1341,8 +1342,8 @@ export default class IBMi { } } - getComponent(id: string) { - return this.components.get(id); + getComponent(type: IBMiComponentType): T | undefined { + return this.componentManager.get(type); } /** @@ -1372,7 +1373,7 @@ export default class IBMi { if (lastStmt) { if ((asUpper?.startsWith(`SELECT`) || asUpper?.startsWith(`WITH`))) { - const copyToImport = this.getComponent(`CopyToImport`); + const copyToImport = this.getComponent(CopyToImport); if (copyToImport) { returningAsCsv = copyToImport.wrap(lastStmt); list.push(...returningAsCsv.newStatements); diff --git a/src/components/component.ts b/src/components/component.ts index 4f5518d17..f7ca0b92d 100644 --- a/src/components/component.ts +++ b/src/components/component.ts @@ -1,19 +1,80 @@ import IBMi from "../api/IBMi"; -export enum ComponentState { +export const enum ComponentState { NotChecked = `NotChecked`, NotInstalled = `NotInstalled`, Installed = `Installed`, + NeedUpdate = `NeedUpdate`, Error = `Error`, } +export type IBMiComponentType = new (c: IBMi) => T; -export class ComponentT { - public state: ComponentState = ComponentState.NotChecked; - public currentVersion: number = 0; +/** + * Defines a component that is managed per IBM i. + * + * Any class extending {@link IBMiComponent} needs to register itself in the Component Registry. + * + * For example, this class: + * ``` + * class MyIBMIComponent extends IBMiComponent { + * //implements getName(), getRemoteState() and update() + * } + * ``` + * Must be registered like this: + * ``` + * const codeForIBMiExtension = vscode.extensions.getExtension('halcyontechltd.code-for-ibmi'); + * if (codeForIBMiExtension) { + * codeForIBMi = codeForIBMiExtension.isActive ? codeForIBMiExtension.exports : await codeForIBMiExtension.activate(); + * codeForIBMi.componentRegistry.registerComponent(MyIBMIComponent); + * } + * ``` + * + */ +export abstract class IBMiComponent { + private state = ComponentState.NotChecked; - constructor(public connection: IBMi) { } + constructor(protected readonly connection: IBMi) { - async getInstalledVersion(): Promise {return}; - async checkState(): Promise {return false} - getState(): ComponentState {return this.state}; + } + + getState() { + return this.state; + } + + async check() { + try { + this.state = await this.getRemoteState(); + if (this.state !== ComponentState.Installed) { + this.state = await this.update(); + } + } + catch (error) { + console.log(`Error occurred while checking component ${this.getName()}`); + console.log(error); + this.state = ComponentState.Error; + } + + return this; + } + + /** + * The name of this component; mainly used for display and logging purposes + * + * @returns a human-readable name + */ + abstract getName(): string; + + /** + * @returns the component's {@link ComponentState state} on the IBM i + */ + protected abstract getRemoteState(): ComponentState | Promise; + + /** + * Called whenever the components needs to be installed or updated, depending on its {@link ComponentState state}. + * + * The Component Manager is responsible for calling this, so the {@link ComponentState state} doesn't need to be checked here. + * + * @returns the component's {@link ComponentState state} after the update is done + */ + protected abstract update(): ComponentState | Promise } \ No newline at end of file diff --git a/src/components/copyToImport.ts b/src/components/copyToImport.ts index ca1b57d4a..24b2d0458 100644 --- a/src/components/copyToImport.ts +++ b/src/components/copyToImport.ts @@ -1,27 +1,8 @@ -import IBMi from "../api/IBMi"; import { Tools } from "../api/Tools"; import { WrapResult } from "../typings"; -import { ComponentState, ComponentT } from "./component"; - -export class CopyToImport implements ComponentT { - private readonly name = 'CPYTOIMPF'; - public state: ComponentState = ComponentState.Installed; - public currentVersion: number = 1; - - constructor(public connection: IBMi) { } - - async getInstalledVersion(): Promise { - return 1; - } - - async checkState(): Promise { - return true; - } - - getState(): ComponentState { - return this.state; - } +import { ComponentState, IBMiComponent } from "./component"; +export class CopyToImport extends IBMiComponent { static isSimple(statement: string): boolean { statement = statement.trim(); if (statement.endsWith(';')) { @@ -32,6 +13,18 @@ export class CopyToImport implements ComponentT { return parts.length === 4 && parts[0].toUpperCase() === `SELECT` && parts[1] === `*` && parts[2].toUpperCase() === `FROM` && parts[3].includes(`.`); } + getName() { + return 'CPYTOIMPF'; + } + + protected getRemoteState() { + return ComponentState.Installed; + } + + protected update(): ComponentState | Promise { + return this.getRemoteState(); + } + wrap(statement: string): WrapResult { const outStmf = this.connection.getTempRemote(Tools.makeid())!; diff --git a/src/components/getMemberInfo.ts b/src/components/getMemberInfo.ts index 7ec9834c7..c78aeb258 100644 --- a/src/components/getMemberInfo.ts +++ b/src/components/getMemberInfo.ts @@ -1,107 +1,79 @@ import { posix } from "path"; -import IBMi from "../api/IBMi"; import { Tools } from "../api/Tools"; -import { instance } from "../instantiate"; import { IBMiMember } from "../typings"; -import { ComponentState, ComponentT } from "./component"; +import { ComponentState, IBMiComponent } from "./component"; -export class GetMemberInfo implements ComponentT { - public readonly name = 'GETMBRINFO'; - public state: ComponentState = ComponentState.NotInstalled; - public currentVersion: number = 1; +export class GetMemberInfo extends IBMiComponent { + private readonly currentVersion = 1; - constructor(public connection: IBMi) { } + getName() { + return "GETMBRINFO"; + } - async getInstalledVersion(): Promise { - const config = this.connection.config! - const lib = config.tempLibrary!; - const sql = `select LONG_COMMENT from qsys2.sysroutines where routine_schema = '${lib.toUpperCase()}' and routine_name = '${this.name}'` - const [result] = await this.connection.runSQL(sql); - if (result && result.LONG_COMMENT) { + protected async getRemoteState(): Promise { + let installedVersion = 0; + const [result] = await this.connection.runSQL(`select LONG_COMMENT from qsys2.sysroutines where routine_schema = '${this.connection.config?.tempLibrary.toUpperCase()}' and routine_name = 'GETMBRINFO'`); + if (result.LONG_COMMENT) { const comment = result.LONG_COMMENT as string; const dash = comment.indexOf('-'); if (dash > -1) { - const version = comment.substring(0, dash).trim(); - return parseInt(version); + installedVersion = Number(comment.substring(0, dash).trim()); } } - - return 0; - } - - async checkState(): Promise { - const installedVersion = await this.getInstalledVersion(); - - if (installedVersion === this.currentVersion) { - this.state = ComponentState.Installed; - return true; + if (installedVersion < this.currentVersion) { + return ComponentState.NeedUpdate; } - const config = this.connection.config! - const content = instance.getContent(); + return ComponentState.Installed; + } + protected async update() { + const config = this.connection.config!; return this.connection.withTempDirectory(async tempDir => { const tempSourcePath = posix.join(tempDir, `getMemberInfo.sql`); - - await content!.writeStreamfileRaw(tempSourcePath, getSource(config.tempLibrary, this.name, this.currentVersion)); + await this.connection.content.writeStreamfileRaw(tempSourcePath, getSource(config.tempLibrary, this.getName(), this.currentVersion)); const result = await this.connection.runCommand({ command: `RUNSQLSTM SRCSTMF('${tempSourcePath}') COMMIT(*NONE) NAMING(*SQL)`, cwd: `/`, noLibList: true }); - if (result.code === 0) { - this.state = ComponentState.Installed; + if (result.code) { + return ComponentState.Error; } else { - this.state = ComponentState.Error; + return ComponentState.Installed; } - - return this.state === ComponentState.Installed; }); } - getState(): ComponentState { - return this.state; - } - - - /** - * - * @param filter: the criterias used to list the members - * @returns - */ async getMemberInfo(library: string, sourceFile: string, member: string): Promise { - if (this.state === ComponentState.Installed) { - const config = this.connection.config!; - const tempLib = config.tempLibrary; - const statement = `select * from table(${tempLib}.GETMBRINFO('${library}', '${sourceFile}', '${member}'))`; - - let results: Tools.DB2Row[] = []; - if (config.enableSQL) { - try { - results = await this.connection.runSQL(statement); - } catch (e) { }; // Ignore errors, will return undefined. - } - else { - results = await this.connection.content.getQTempTable([`create table QTEMP.MEMBERINFO as (${statement}) with data`], "MEMBERINFO"); - } + const config = this.connection.config!; + const tempLib = config.tempLibrary; + const statement = `select * from table(${tempLib}.${this.getName()}('${library}', '${sourceFile}', '${member}'))`; + + let results: Tools.DB2Row[] = []; + if (config.enableSQL) { + try { + results = await this.connection.runSQL(statement); + } catch (e) { } // Ignore errors, will return undefined. + } + else { + results = await this.connection.content.getQTempTable([`create table QTEMP.MEMBERINFO as (${statement}) with data`], "MEMBERINFO"); + } - if (results.length === 1 && results[0].ISSOURCE === 'Y') { - const result = results[0]; - const asp = this.connection.aspInfo[Number(results[0].ASP)]; - return { - library: result.LIBRARY, - file: result.FILE, - name: result.MEMBER, - extension: result.EXTENSION, - text: result.DESCRIPTION, - created: new Date(result.CREATED ? Number(result.CREATED) : 0), - changed: new Date(result.CHANGED ? Number(result.CHANGED) : 0) - } as IBMiMember - } - else { - return undefined; - } + if (results.length === 1 && results[0].ISSOURCE === 'Y') { + const result = results[0]; + const asp = this.connection.aspInfo[Number(results[0].ASP)]; + return { + asp, + library: result.LIBRARY, + file: result.FILE, + name: result.MEMBER, + extension: result.EXTENSION, + text: result.DESCRIPTION, + created: new Date(result.CREATED ? Number(result.CREATED) : 0), + changed: new Date(result.CHANGED ? Number(result.CHANGED) : 0) + } as IBMiMember } } } diff --git a/src/components/getNewLibl.ts b/src/components/getNewLibl.ts index 914d1dc51..06d0928a9 100644 --- a/src/components/getNewLibl.ts +++ b/src/components/getNewLibl.ts @@ -1,26 +1,19 @@ import { posix } from "path"; -import IBMi from "../api/IBMi"; import { instance } from "../instantiate"; -import { ComponentState, ComponentT } from "./component"; +import { ComponentState, IBMiComponent } from "./component"; -export class GetNewLibl implements ComponentT { - public state: ComponentState = ComponentState.NotInstalled; - public currentVersion: number = 1; +export class GetNewLibl extends IBMiComponent { + private readonly currentVersion = 1; - constructor(public connection: IBMi) { } - - async getInstalledVersion(): Promise { - return (this.connection.remoteFeatures[`GETNEWLIBL.PGM`] ? 1 : 0); + getName() { + return 'GETNEWLIBL'; } - async checkState(): Promise { - const installedVersion = await this.getInstalledVersion(); - - if (installedVersion === this.currentVersion) { - this.state = ComponentState.Installed; - return true; - } + protected async getRemoteState() { + return this.connection.remoteFeatures[`GETNEWLIBL.PGM`] ? ComponentState.Installed : ComponentState.NotInstalled; + } + protected update() { const config = this.connection.config! const content = instance.getContent(); return this.connection.withTempDirectory(async tempDir => { @@ -33,48 +26,37 @@ export class GetNewLibl implements ComponentT { noLibList: true }); - if (result.code === 0) { - this.state = ComponentState.Installed; + if (!result.code) { + return ComponentState.Installed; } else { - this.state = ComponentState.Error; + return ComponentState.Error; } - - return this.state === ComponentState.Installed; }); } - getState(): ComponentState { - return this.state; - } - - async getLibraryListFromCommand(ileCommand: string): Promise<{ currentLibrary: string; libraryList: string[]; } | undefined> { - if (this.state === ComponentState.Installed) { - const tempLib = this.connection.config!.tempLibrary; - const resultSet = await this.connection.runSQL(`CALL ${tempLib}.GETNEWLIBL('${ileCommand.replace(new RegExp(`'`, 'g'), `''`)}')`); - - const result = { - currentLibrary: `QGPL`, - libraryList: [] as string[] - }; - - resultSet.forEach(row => { - const libraryName = String(row.SYSTEM_SCHEMA_NAME); - switch (row.PORTION) { - case `CURRENT`: - result.currentLibrary = libraryName; - break; - case `USER`: - result.libraryList.push(libraryName); - break; - } - }) - - return result; - } + async getLibraryListFromCommand(ileCommand: string) { + const tempLib = this.connection.config!.tempLibrary; + const resultSet = await this.connection.runSQL(`CALL ${tempLib}.GETNEWLIBL('${ileCommand.replace(new RegExp(`'`, 'g'), `''`)}')`); + + const result = { + currentLibrary: `QGPL`, + libraryList: [] as string[] + }; + + resultSet.forEach(row => { + const libraryName = String(row.SYSTEM_SCHEMA_NAME); + switch (row.PORTION) { + case `CURRENT`: + result.currentLibrary = libraryName; + break; + case `USER`: + result.libraryList.push(libraryName); + break; + } + }) - return undefined; + return result; } - } function getSource(library: string) { diff --git a/src/components/manager.ts b/src/components/manager.ts index a34ccf12a..40fc00e59 100644 --- a/src/components/manager.ts +++ b/src/components/manager.ts @@ -1,13 +1,13 @@ import IBMi from "../api/IBMi"; -import { ComponentState, ComponentT } from "./component"; +import { ComponentState, IBMiComponent, IBMiComponentType } from "./component"; import { CopyToImport } from "./copyToImport"; import { GetMemberInfo } from "./getMemberInfo"; import { GetNewLibl } from "./getNewLibl"; export class ComponentRegistry { - private readonly allComponents: (typeof ComponentT)[] = [GetNewLibl, CopyToImport, GetMemberInfo]; + private readonly allComponents: (IBMiComponentType)[] = [GetNewLibl, CopyToImport, GetMemberInfo]; - public registerComponent(component: typeof ComponentT) { + public registerComponent(component: IBMiComponentType) { this.allComponents.push(component); } @@ -19,29 +19,22 @@ export class ComponentRegistry { export const extensionComponentRegistry = new ComponentRegistry(); export class ComponentManager { - private readonly registered: Map = new Map; + private readonly registered: Map, IBMiComponent> = new Map; - public async startup(connection: IBMi) { + constructor(private readonly connection: IBMi) { + + } + + public async startup() { for (const Component of extensionComponentRegistry.getComponents()) { - const instance = new Component(connection); - this.registered.set(Component.name, instance); - await ComponentManager.checkState(instance); + this.registered.set(Component, await new Component(this.connection).check()); } } - get(id: string): T | undefined { - const component = this.registered.get(id); + get(type: IBMiComponentType): T | undefined { + const component = this.registered.get(type); if (component && component.getState() === ComponentState.Installed) { return component as T; } } - - private static async checkState(component: ComponentT) { - try { - await component.checkState(); - } catch (e) { - console.log(component); - console.log(`Error checking state for ${component.constructor.name}`, e); - } - } } \ No newline at end of file diff --git a/src/instantiate.ts b/src/instantiate.ts index dd90c0c62..b74d1b1e4 100644 --- a/src/instantiate.ts +++ b/src/instantiate.ts @@ -466,25 +466,25 @@ export async function loadAllofExtension(context: vscode.ExtensionContext) { const selectionSplit = connection!.upperCaseName(selection).split('/') if (selectionSplit.length === 3 || selection.startsWith(`/`)) { - const infoComponent = connection?.getComponent(`GetMemberInfo`); + const infoComponent = connection?.getComponent(GetMemberInfo); // When selection is QSYS path - if (!selection.startsWith(`/`) && infoComponent) { - const lib = selectionSplit[0]; + if (!selection.startsWith(`/`) && infoComponent && connection) { + const library = selectionSplit[0]; const file = selectionSplit[1]; const member = path.parse(selectionSplit[2]); member.ext = member.ext.substring(1); - const memberInfo = await infoComponent.getMemberInfo(lib, file, member.name); + const memberInfo = await infoComponent.getMemberInfo(library, file, member.name); if (!memberInfo) { - vscode.window.showWarningMessage(`Source member ${lib}/${file}/${member.base} does not exist.`); + vscode.window.showWarningMessage(`Source member ${library}/${file}/${member.base} does not exist.`); return; } else if (memberInfo.name !== member.name || (member.ext && memberInfo.extension !== member.ext)) { - vscode.window.showWarningMessage(`Member ${lib}/${file}/${member.name} of type ${member.ext} does not exist.`); + vscode.window.showWarningMessage(`Member ${library}/${file}/${member.name} of type ${member.ext} does not exist.`); return; } member.base = `${member.name}.${member.ext || memberInfo.extension}`; - selection = `${lib}/${file}/${member.base}`; + selection = `${library}/${file}/${member.base}`; }; // When select is IFS path diff --git a/src/testing/components.ts b/src/testing/components.ts index 491351a83..0893c70a8 100644 --- a/src/testing/components.ts +++ b/src/testing/components.ts @@ -16,7 +16,7 @@ export const ComponentSuite: TestSuite = { { name: `Get new libl`, test: async () => { const connection = instance.getConnection()! - const component = connection.getComponent(`GetNewLibl`); + const component = connection.getComponent(GetNewLibl); if (component) { const newLibl = await component.getLibraryListFromCommand(`CHGLIBL CURLIB(SYSTOOLS)`); @@ -31,7 +31,7 @@ export const ComponentSuite: TestSuite = { { name: `Check getMemberInfo`, test: async () => { const connection = instance.getConnection(); - const component = connection?.getComponent(`GetMemberInfo`)!; + const component = connection?.getComponent(GetMemberInfo)!; assert.ok(component); diff --git a/src/views/ProfilesView.ts b/src/views/ProfilesView.ts index f9c6e00be..c77005c93 100644 --- a/src/views/ProfilesView.ts +++ b/src/views/ProfilesView.ts @@ -121,7 +121,7 @@ export class ProfilesView { if (storedProfile) { try { - const component = connection?.getComponent(`GetNewLibl`) + const component = connection?.getComponent(GetNewLibl) const newSettings = await component?.getLibraryListFromCommand(storedProfile.command); if (newSettings) { From 5e5e0af4a674c6f38d69954d74b34b39c01c5a28 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 19 Sep 2024 11:12:02 +0200 Subject: [PATCH 07/28] Require context to register a component + display components list Signed-off-by: Seb Julliand --- src/api/IBMi.ts | 4 ++-- src/components/component.ts | 18 ++++++++++-------- src/components/manager.ts | 26 ++++++++++++++++---------- src/extension.ts | 19 +++++++++++++------ src/webviews/settings/index.ts | 23 +++++++++++++++++++++-- 5 files changed, 62 insertions(+), 28 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index d1f713f55..29c923057 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -1354,8 +1354,8 @@ export default class IBMi { } } - getComponent(type: IBMiComponentType): T | undefined { - return this.componentManager.get(type); + getComponent(type: IBMiComponentType, ignoreState?:boolean): T | undefined { + return this.componentManager.get(type, ignoreState); } /** diff --git a/src/components/component.ts b/src/components/component.ts index f7ca0b92d..7ed33838b 100644 --- a/src/components/component.ts +++ b/src/components/component.ts @@ -1,10 +1,10 @@ import IBMi from "../api/IBMi"; export const enum ComponentState { - NotChecked = `NotChecked`, - NotInstalled = `NotInstalled`, + NotChecked = `Not checked`, + NotInstalled = `Not installed`, Installed = `Installed`, - NeedUpdate = `NeedUpdate`, + NeedUpdate = `Need update`, Error = `Error`, } export type IBMiComponentType = new (c: IBMi) => T; @@ -20,12 +20,14 @@ export type IBMiComponentType = new (c: IBMi) => T; * //implements getName(), getRemoteState() and update() * } * ``` - * Must be registered like this: + * Must be registered like this, when the extension providing the component gets activated: * ``` - * const codeForIBMiExtension = vscode.extensions.getExtension('halcyontechltd.code-for-ibmi'); - * if (codeForIBMiExtension) { - * codeForIBMi = codeForIBMiExtension.isActive ? codeForIBMiExtension.exports : await codeForIBMiExtension.activate(); - * codeForIBMi.componentRegistry.registerComponent(MyIBMIComponent); + * export async function activate(context: ExtensionContext) { + * const codeForIBMiExtension = vscode.extensions.getExtension('halcyontechltd.code-for-ibmi'); + * if (codeForIBMiExtension) { + * codeForIBMi = codeForIBMiExtension.isActive ? codeForIBMiExtension.exports : await codeForIBMiExtension.activate(); + * codeForIBMi.componentRegistry.registerComponent(context, MyIBMIComponent); + * } * } * ``` * diff --git a/src/components/manager.ts b/src/components/manager.ts index 40fc00e59..ec0c19152 100644 --- a/src/components/manager.ts +++ b/src/components/manager.ts @@ -1,18 +1,23 @@ +import vscode from "vscode"; import IBMi from "../api/IBMi"; import { ComponentState, IBMiComponent, IBMiComponentType } from "./component"; -import { CopyToImport } from "./copyToImport"; -import { GetMemberInfo } from "./getMemberInfo"; -import { GetNewLibl } from "./getNewLibl"; export class ComponentRegistry { - private readonly allComponents: (IBMiComponentType)[] = [GetNewLibl, CopyToImport, GetMemberInfo]; + private readonly components: Map)[]> = new Map; - public registerComponent(component: IBMiComponentType) { - this.allComponents.push(component); + public registerComponent(context: vscode.ExtensionContext, component: IBMiComponentType) { + const key = context.extension.id; + const extensionComponents = this.components.get(key); + if (extensionComponents) { + extensionComponents.push(component); + } + else { + this.components.set(key, [component]); + } } public getComponents() { - return this.allComponents; + return this.components; } } @@ -26,14 +31,15 @@ export class ComponentManager { } public async startup() { - for (const Component of extensionComponentRegistry.getComponents()) { + const components = Array.from(extensionComponentRegistry.getComponents().values()).flatMap(a => a.flat()); + for (const Component of components) { this.registered.set(Component, await new Component(this.connection).check()); } } - get(type: IBMiComponentType): T | undefined { + get(type: IBMiComponentType, ignoreState?: boolean): T | undefined { const component = this.registered.get(type); - if (component && component.getState() === ComponentState.Installed) { + if (component && (ignoreState || component.getState() === ComponentState.Installed)) { return component as T; } } diff --git a/src/extension.ts b/src/extension.ts index 0f0965f4d..f4e04ab97 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,9 @@ import * as Debug from './api/debug'; import { parseErrors } from "./api/errors/parser"; import { DeployTools } from "./api/local/deployTools"; import { Deployment } from "./api/local/deployment"; +import { CopyToImport } from "./components/copyToImport"; +import { GetMemberInfo } from "./components/getMemberInfo"; +import { GetNewLibl } from "./components/getNewLibl"; import { extensionComponentRegistry } from "./components/manager"; import { IFSFS } from "./filesystems/ifsFs"; import { LocalActionCompletionItemProvider } from "./languages/actions/completion"; @@ -126,12 +129,16 @@ export async function activate(context: ExtensionContext): Promise commands.executeCommand("code-for-ibmi.refreshProfileView"); }); - return { - instance, customUI: () => new CustomUI(), - deployTools: DeployTools, - evfeventParser: parseErrors, - tools: Tools, - componentRegistry: extensionComponentRegistry + extensionComponentRegistry.registerComponent(context, GetNewLibl); + extensionComponentRegistry.registerComponent(context, GetMemberInfo); + extensionComponentRegistry.registerComponent(context, CopyToImport); + + return { + instance, customUI: () => new CustomUI(), + deployTools: DeployTools, + evfeventParser: parseErrors, + tools: Tools, + componentRegistry: extensionComponentRegistry }; } diff --git a/src/webviews/settings/index.ts b/src/webviews/settings/index.ts index 7694d390d..80b7e4130 100644 --- a/src/webviews/settings/index.ts +++ b/src/webviews/settings/index.ts @@ -6,6 +6,8 @@ import { Tools } from "../../api/Tools"; import { isManaged } from "../../api/debug"; import * as certificates from "../../api/debug/certificates"; import { isSEPSupported } from "../../api/debug/server"; +import { IBMiComponent } from "../../components/component"; +import { extensionComponentRegistry } from "../../components/manager"; import { instance } from "../../instantiate"; import { t } from "../../locale"; import { ConnectionData, Server } from '../../typings'; @@ -215,12 +217,29 @@ export class SettingsUI { debuggerTab.addParagraph('Connect to the server to see these settings.'); } - let tabs: ComplexTab[] = [ + const componentsTab = new Section(); + if (connection) { + componentsTab.addParagraph(`The following extensions contribute these components:`); + extensionComponentRegistry.getComponents().forEach((components, extensionId) => { + const extension = vscode.extensions.getExtension(extensionId); + componentsTab.addParagraph(`

+

${extension?.packageJSON.displayName || extension?.id || "Unnamed extension"}

+
    + ${components.map(type => connection.getComponent(type, true)).map(component => `
  • ${component?.getName()}: ${component?.getState()}
  • `).join(``)} +
+

`); + }) + } else { + componentsTab.addParagraph('Connect to the server to see these settings.'); + } + + const tabs: ComplexTab[] = [ { label: `Features`, fields: featuresTab.fields }, { label: `Source Code`, fields: sourceTab.fields }, { label: `Terminals`, fields: terminalsTab.fields }, { label: `Debugger`, fields: debuggerTab.fields }, { label: `Temporary Data`, fields: tempDataTab.fields }, + { label: `Components`, fields: componentsTab.fields }, ]; const ui = new CustomUI(); @@ -380,7 +399,7 @@ export class SettingsUI { await ConnectionManager.deleteStoredPassword(context, name); vscode.window.showInformationMessage(t(`login.privateKey.updated`, name)); } - else{ + else { delete data.privateKeyPath; } break; From 56427caef3c89ea28899c6da5b51b783dcd9a5b7 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 19 Sep 2024 15:29:27 +0200 Subject: [PATCH 08/28] Show version of each component Signed-off-by: Seb Julliand --- src/components/component.ts | 15 ++++++- src/components/copyToImport.ts | 4 +- src/components/getMemberInfo.ts | 80 ++++++++++++++++----------------- src/components/getNewLibl.ts | 6 +-- src/webviews/settings/index.ts | 2 +- 5 files changed, 56 insertions(+), 51 deletions(-) diff --git a/src/components/component.ts b/src/components/component.ts index 7ed33838b..0612c6f11 100644 --- a/src/components/component.ts +++ b/src/components/component.ts @@ -7,6 +7,12 @@ export const enum ComponentState { NeedUpdate = `Need update`, Error = `Error`, } + +export type ComponentIdentification = { + name: string + version: number +} + export type IBMiComponentType = new (c: IBMi) => T; /** @@ -51,7 +57,7 @@ export abstract class IBMiComponent { } } catch (error) { - console.log(`Error occurred while checking component ${this.getName()}`); + console.log(`Error occurred while checking component ${this.toString()}`); console.log(error); this.state = ComponentState.Error; } @@ -59,12 +65,17 @@ export abstract class IBMiComponent { return this; } + toString() { + const identification = this.getIdentification(); + return `${identification.name} (version ${identification.version})` + } + /** * The name of this component; mainly used for display and logging purposes * * @returns a human-readable name */ - abstract getName(): string; + abstract getIdentification(): ComponentIdentification; /** * @returns the component's {@link ComponentState state} on the IBM i diff --git a/src/components/copyToImport.ts b/src/components/copyToImport.ts index 24b2d0458..7232acc18 100644 --- a/src/components/copyToImport.ts +++ b/src/components/copyToImport.ts @@ -13,8 +13,8 @@ export class CopyToImport extends IBMiComponent { return parts.length === 4 && parts[0].toUpperCase() === `SELECT` && parts[1] === `*` && parts[2].toUpperCase() === `FROM` && parts[3].includes(`.`); } - getName() { - return 'CPYTOIMPF'; + getIdentification() { + return { name: 'CopyToImport', version: 1 }; } protected getRemoteState() { diff --git a/src/components/getMemberInfo.ts b/src/components/getMemberInfo.ts index 1d1b7126c..2f86720e2 100644 --- a/src/components/getMemberInfo.ts +++ b/src/components/getMemberInfo.ts @@ -4,23 +4,24 @@ import { IBMiMember } from "../typings"; import { ComponentState, IBMiComponent } from "./component"; export class GetMemberInfo extends IBMiComponent { + private readonly procedureName = 'GETMBRINFO'; private readonly currentVersion = 1; + private installedVersion = 0; - getName() { - return "GETMBRINFO"; + getIdentification() { + return { name: 'GetMemberInfo', version: this.installedVersion }; } protected async getRemoteState(): Promise { - let installedVersion = 0; - const [result] = await this.connection.runSQL(`select LONG_COMMENT from qsys2.sysroutines where routine_schema = '${this.connection.config?.tempLibrary.toUpperCase()}' and routine_name = 'GETMBRINFO'`); + const [result] = await this.connection.runSQL(`select LONG_COMMENT from qsys2.sysroutines where routine_schema = '${this.connection.config?.tempLibrary.toUpperCase()}' and routine_name = '${this.procedureName}'`); if (result.LONG_COMMENT) { const comment = result.LONG_COMMENT as string; const dash = comment.indexOf('-'); if (dash > -1) { - installedVersion = Number(comment.substring(0, dash).trim()); + this.installedVersion = Number(comment.substring(0, dash).trim()); } } - if (installedVersion < this.currentVersion) { + if (this.installedVersion < this.currentVersion) { return ComponentState.NeedUpdate; } @@ -31,7 +32,7 @@ export class GetMemberInfo extends IBMiComponent { const config = this.connection.config!; return this.connection.withTempDirectory(async tempDir => { const tempSourcePath = posix.join(tempDir, `getMemberInfo.sql`); - await this.connection.content.writeStreamfileRaw(tempSourcePath, getSource(config.tempLibrary, this.getName(), this.currentVersion)); + await this.connection.content.writeStreamfileRaw(tempSourcePath, getSource(config.tempLibrary, this.procedureName, this.currentVersion)); const result = await this.connection.runCommand({ command: `RUNSQLSTM SRCSTMF('${tempSourcePath}') COMMIT(*NONE) NAMING(*SQL)`, cwd: `/`, @@ -49,7 +50,7 @@ export class GetMemberInfo extends IBMiComponent { async getMemberInfo(library: string, sourceFile: string, member: string): Promise { const config = this.connection.config!; const tempLib = config.tempLibrary; - const statement = `select * from table(${tempLib}.${this.getName()}('${library}', '${sourceFile}', '${member}'))`; + const statement = `select * from table(${tempLib}.${this.procedureName}('${library}', '${sourceFile}', '${member}'))`; let results: Tools.DB2Row[] = []; if (config.enableSQL) { @@ -77,41 +78,36 @@ export class GetMemberInfo extends IBMiComponent { } } - async getMultipleMemberInfo(members: IBMiMember[]): Promise { - if (this.state === ComponentState.Installed) { - const config = this.connection.config!; - const tempLib = config.tempLibrary; - const statement = members - .map(member => `select * from table(${this.connection.config!.tempLibrary}.GETMBRINFO('${member.library}', '${member.file}', '${member.name}'))`) - .join(' union all '); - - let results: Tools.DB2Row[] = []; - if (config.enableSQL) { - try { - results = await this.connection.runSQL(statement); - } catch (e) { }; // Ignore errors, will return undefined. - } - else { - results = await this.connection.content.getQTempTable([`create table QTEMP.MEMBERINFO as (${statement}) with data`], "MEMBERINFO"); - } - - return results.filter(row => row.ISSOURCE === 'Y').map(result => { - const asp = this.connection.aspInfo[Number(result.ASP)]; - return { - asp, - library: result.LIBRARY, - file: result.FILE, - name: result.MEMBER, - extension: result.EXTENSION, - text: result.DESCRIPTION, - created: new Date(result.CREATED ? Number(result.CREATED) : 0), - changed: new Date(result.CHANGED ? Number(result.CHANGED) : 0) - } as IBMiMember - }); + async getMultipleMemberInfo(members: IBMiMember[]): Promise { + const config = this.connection.config!; + const tempLib = config.tempLibrary; + const statement = members + .map(member => `select * from table(${tempLib}.${this.procedureName}('${member.library}', '${member.file}', '${member.name}'))`) + .join(' union all '); - } else { - return undefined; + let results: Tools.DB2Row[] = []; + if (config.enableSQL) { + try { + results = await this.connection.runSQL(statement); + } catch (e) { }; // Ignore errors, will return undefined. + } + else { + results = await this.connection.content.getQTempTable([`create table QTEMP.MEMBERINFO as (${statement}) with data`], "MEMBERINFO"); } + + return results.filter(row => row.ISSOURCE === 'Y').map(result => { + const asp = this.connection.aspInfo[Number(result.ASP)]; + return { + asp, + library: result.LIBRARY, + file: result.FILE, + name: result.MEMBER, + extension: result.EXTENSION, + text: result.DESCRIPTION, + created: new Date(result.CREATED ? Number(result.CREATED) : 0), + changed: new Date(result.CHANGED ? Number(result.CHANGED) : 0) + } as IBMiMember + }); } } @@ -142,7 +138,7 @@ function getSource(library: string, name: string, version: number) { `, Description varchar( 50 )`, `, isSource char( 1 )`, `)`, - `specific GETMBRINFO`, + `specific ${name}`, `modifies sql data`, `begin`, ` declare buffer char( 135 ) for bit data not null default '';`, diff --git a/src/components/getNewLibl.ts b/src/components/getNewLibl.ts index 06d0928a9..64014eef7 100644 --- a/src/components/getNewLibl.ts +++ b/src/components/getNewLibl.ts @@ -3,10 +3,8 @@ import { instance } from "../instantiate"; import { ComponentState, IBMiComponent } from "./component"; export class GetNewLibl extends IBMiComponent { - private readonly currentVersion = 1; - - getName() { - return 'GETNEWLIBL'; + getIdentification() { + return { name: 'GetNewLibl', version: 1 }; } protected async getRemoteState() { diff --git a/src/webviews/settings/index.ts b/src/webviews/settings/index.ts index 80b7e4130..e3d660b5c 100644 --- a/src/webviews/settings/index.ts +++ b/src/webviews/settings/index.ts @@ -225,7 +225,7 @@ export class SettingsUI { componentsTab.addParagraph(`

${extension?.packageJSON.displayName || extension?.id || "Unnamed extension"}

    - ${components.map(type => connection.getComponent(type, true)).map(component => `
  • ${component?.getName()}: ${component?.getState()}
  • `).join(``)} + ${components.map(type => connection.getComponent(type, true)).map(component => `
  • ${component?.toString()}: ${component?.getState()}
  • `).join(``)}

`); }) From cee0a5486ffac43512ebd68a6ebea0abb258949e Mon Sep 17 00:00:00 2001 From: krethan Date: Thu, 26 Sep 2024 23:15:13 -0600 Subject: [PATCH 09/28] Seperating logic from IBMi.ts into a new class. --- src/api/IBMi.ts | 550 ++++++++++------------------------------ src/api/IBMiApps.ts | 87 +++++++ src/api/IBMiSettings.ts | 515 +++++++++++++++++++++++++++++++++++++ src/typings.ts | 21 +- 4 files changed, 757 insertions(+), 416 deletions(-) create mode 100644 src/api/IBMiApps.ts create mode 100644 src/api/IBMiSettings.ts diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index fe2efe28b..3c250b276 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -9,7 +9,7 @@ import path from 'path'; import { ComponentId, ComponentManager } from "../components/component"; import { CopyToImport } from "../components/copyToImport"; import { instance } from "../instantiate"; -import { CommandData, CommandResult, ConnectionData, IBMiMember, RemoteCommand, SpecialAuthorities, WrapResult } from "../typings"; +import { CommandData, CommandResult, ConnectionData, MemberParts, RemoteCommand, RemoteFeatures, SpecialAuthorities, WrapResult } from "../typings"; import { CompileTools } from "./CompileTools"; import IBMiContent from "./IBMiContent"; import { CachedServerSettings, GlobalStorage } from './Storage'; @@ -17,38 +17,12 @@ import { Tools } from './Tools'; import * as configVars from './configVars'; import { DebugConfiguration } from "./debug/config"; import { debugPTFInstalled } from "./debug/server"; - -export interface MemberParts extends IBMiMember { - basename: string -} +import IBMiSettings from "./IBMiSettings"; +import IBMiApps from "./IBMiApps"; const CCSID_SYSVAL = -2; const bashShellPath = '/QOpenSys/pkgs/bin/bash'; -const remoteApps = [ // All names MUST also be defined as key in 'remoteFeatures' below!! - { - path: `/usr/bin/`, - names: [`setccsid`, `iconv`, `attr`, `tar`, `ls`] - }, - { - path: `/QOpenSys/pkgs/bin/`, - names: [`git`, `grep`, `tn5250`, `md5sum`, `bash`, `chsh`, `stat`, `sort`, `tar`, `ls`, `find`] - }, - { - path: `/QSYS.LIB/`, - // In the future, we may use a generic specific. - // Right now we only need one program - // specific: `*.PGM`, - specific: `QZDFMDB2.PGM`, - names: [`QZDFMDB2.PGM`] - }, - { - path: `/QIBM/ProdData/IBMiDebugService/bin/`, - specific: `startDebugService.sh`, - names: [`startDebugService.sh`] - } -]; - export default class IBMi { private qccsid: number = 65535; private jobCcsid: number = CCSID_SYSVAL; @@ -72,7 +46,7 @@ export default class IBMi { * the root of the IFS, thus why we store it. */ aspInfo: { [id: number]: string } = {}; - remoteFeatures: { [name: string]: string | undefined }; + remoteFeatures: RemoteFeatures; variantChars: { american: string, local: string }; /** @@ -94,26 +68,7 @@ export default class IBMi { constructor() { this.client = new node_ssh.NodeSSH; - this.remoteFeatures = { - git: undefined, - grep: undefined, - tn5250: undefined, - setccsid: undefined, - md5sum: undefined, - bash: undefined, - chsh: undefined, - stat: undefined, - sort: undefined, - 'GETNEWLIBL.PGM': undefined, - 'GETMBRINFO.SQL': undefined, - 'QZDFMDB2.PGM': undefined, - 'startDebugService.sh': undefined, - attr: undefined, - iconv: undefined, - tar: undefined, - ls: undefined, - find: undefined, - }; + this.remoteFeatures = {}; this.variantChars = { american: `#@$`, @@ -191,17 +146,17 @@ export default class IBMi { // Reload server settings? const quickConnect = (this.config.quickConnect === true && reloadServerSettings === false); + //Initialize IBMiConnectionSettings to be used throughout to get settings + let connSettings = new IBMiSettings(this); + // Check shell output for additional user text - this will confuse Code... progress.report({ message: `Checking shell output.` }); - const checkShellText = `This should be the only text!`; - const checkShellResult = await this.sendCommand({ - command: `echo "${checkShellText}"`, - directory: `.` - }); - if (checkShellResult.stdout.split(`\n`)[0] !== checkShellText) { + const checkShellResult = await connSettings.checkShellOutput(); + + if (!checkShellResult) { const chosen = await vscode.window.showErrorMessage(`Error in shell configuration!`, { detail: [ `This extension can not work with the shell configured on ${this.currentConnectionName},`, @@ -233,74 +188,37 @@ export default class IBMi { message: `Checking home directory.` }); - let defaultHomeDir; - - const echoHomeResult = await this.sendCommand({ - command: `echo $HOME && cd && test -w $HOME`, - directory: `.` - }); - // Note: if the home directory does not exist, the behavior of the echo/cd/test command combo is as follows: - // - stderr contains 'Could not chdir to home directory /home/________: No such file or directory' - // (The output contains 'chdir' regardless of locale and shell, so maybe we could use that - // if we iterate on this code again in the future) - // - stdout contains the name of the home directory (even if it does not exist) - // - The 'cd' command causes an error if the home directory does not exist or otherwise can't be cd'ed into - // - The 'test' command causes an error if the home directory is not writable (one can cd into a non-writable directory) - let isHomeUsable = (0 == echoHomeResult.code); - if (isHomeUsable) { - defaultHomeDir = echoHomeResult.stdout.trim(); - } else { - // Let's try to provide more valuable information to the user about why their home directory - // is bad and maybe even provide the opportunity to create the home directory - - let actualHomeDir = echoHomeResult.stdout.trim(); - - // we _could_ just assume the home directory doesn't exist but maybe there's something more going on, namely mucked-up permissions - let doesHomeExist = (0 === (await this.sendCommand({ command: `test -e ${actualHomeDir}` })).code); - if (doesHomeExist) { - // Note: this logic might look backward because we fall into this (failure) leg on what looks like success (home dir exists). - // But, remember, but we only got here if 'cd $HOME' failed. - // Let's try to figure out why.... - if (0 !== (await this.sendCommand({ command: `test -d ${actualHomeDir}` })).code) { - await vscode.window.showWarningMessage(`Your home directory (${actualHomeDir}) is not a directory! Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: !reconnecting }); - } - else if (0 !== (await this.sendCommand({ command: `test -w ${actualHomeDir}` })).code) { - await vscode.window.showWarningMessage(`Your home directory (${actualHomeDir}) is not writable! Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: !reconnecting }); - } - else if (0 !== (await this.sendCommand({ command: `test -x ${actualHomeDir}` })).code) { - await vscode.window.showWarningMessage(`Your home directory (${actualHomeDir}) is not usable due to permissions! Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: !reconnecting }); + const homeResult = await connSettings.getHomeDirectory(); + if (homeResult.homeMsg) { + if (homeResult.homeExists) { + //Home Directory exists but give informational message + await vscode.window.showWarningMessage(homeResult.homeMsg, { modal: !reconnecting }); + } + else { + //Home Directory does not exist + if (reconnecting) { + vscode.window.showWarningMessage(homeResult.homeMsg, { modal: false }); } else { - // not sure, but get your sys admin involved - await vscode.window.showWarningMessage(`Your home directory (${actualHomeDir}) exists but is unusable. Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: !reconnecting }); - } - } - else if (reconnecting) { - vscode.window.showWarningMessage(`Your home directory (${actualHomeDir}) does not exist. Code for IBM i may not function correctly.`, { modal: false }); - } - else if (await vscode.window.showWarningMessage(`Home directory does not exist`, { - modal: true, - detail: `Your home directory (${actualHomeDir}) does not exist, so Code for IBM i may not function correctly. Would you like to create this directory now?`, - }, `Yes`)) { - this.appendOutput(`creating home directory ${actualHomeDir}`); - let mkHomeCmd = `mkdir -p ${actualHomeDir} && chown ${connectionObject.username.toLowerCase()} ${actualHomeDir} && chmod 0755 ${actualHomeDir}`; - let mkHomeResult = await this.sendCommand({ command: mkHomeCmd, directory: `.` }); - if (0 === mkHomeResult.code) { - defaultHomeDir = actualHomeDir; - } else { - let mkHomeErrs = mkHomeResult.stderr; - // We still get 'Could not chdir to home directory' in stderr so we need to hackily gut that out, as well as the bashisms that are a side effect of our API - mkHomeErrs = mkHomeErrs.substring(1 + mkHomeErrs.indexOf(`\n`)).replace(`bash: line 1: `, ``); - await vscode.window.showWarningMessage(`Error creating home directory (${actualHomeDir}):\n${mkHomeErrs}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); + if (await vscode.window.showWarningMessage(`Home directory does not exist`, { + modal: true, + detail: `Your home directory (${homeResult.homeDir}) does not exist, so Code for IBM i may not function correctly. Would you like to create this directory now?`, + }, `Yes`)) { + this.appendOutput(`creating home directory ${homeResult.homeDir}`); + let homeCreatedResult = await connSettings.createHomeDirectory(homeResult.homeDir, connectionObject.username); + if (!homeCreatedResult.homeCreated) { + await vscode.window.showWarningMessage(homeCreatedResult.homeMsg, { modal: true }); + } + } } } } // Check to see if we need to store a new value for the home directory - if (defaultHomeDir) { - if (this.config.homeDirectory !== defaultHomeDir) { - this.config.homeDirectory = defaultHomeDir; - vscode.window.showInformationMessage(`Configured home directory reset to ${defaultHomeDir}.`); + if (homeResult.homeDir) { + if (this.config.homeDirectory !== homeResult.homeDir) { + this.config.homeDirectory = homeResult.homeDir; + vscode.window.showInformationMessage(`Configured home directory reset to ${homeResult.homeDir}.`); } } else { // New connections always have `.` as the initial value. @@ -311,7 +229,7 @@ export default class IBMi { //Set a default IFS listing if (this.config.ifsShortcuts.length === 0) { - if (defaultHomeDir) { + if (homeResult.homeDir) { this.config.ifsShortcuts = [this.config.homeDirectory]; } else { this.config.ifsShortcuts = [`/`]; @@ -322,42 +240,19 @@ export default class IBMi { message: `Checking library list configuration.` }); - //Since the compiles are stateless, then we have to set the library list each time we use the `SYSTEM` command - //We setup the defaultUserLibraries here so we can remove them later on so the user can setup their own library list - let currentLibrary = `QGPL`; this.defaultUserLibraries = []; - const liblResult = await this.sendQsh({ - command: `liblist` - }); - if (liblResult.code === 0) { - const libraryListString = liblResult.stdout; - if (libraryListString !== ``) { - const libraryList = libraryListString.split(`\n`); - - let lib, type; - for (const line of libraryList) { - lib = line.substring(0, 10).trim(); - type = line.substring(12); - - switch (type) { - case `USR`: - this.defaultUserLibraries.push(lib); - break; - - case `CUR`: - currentLibrary = lib; - break; - } - } + let libraryListResult = await connSettings.getLibraryList(); + if (libraryListResult.libStatus) { + + this.defaultUserLibraries = libraryListResult.defaultUserLibraries; - //If this is the first time the config is made, then these arrays will be empty - if (this.config.currentLibrary.length === 0) { - this.config.currentLibrary = currentLibrary; - } - if (this.config.libraryList.length === 0) { - this.config.libraryList = this.defaultUserLibraries; - } + //If this is the first time the config is made, then these arrays will be empty + if (this.config.currentLibrary.length === 0) { + this.config.currentLibrary = libraryListResult.currentLibrary; + } + if (this.config.libraryList.length === 0) { + this.config.libraryList = libraryListResult.defaultUserLibraries; } } @@ -366,60 +261,20 @@ export default class IBMi { }); //Next, we need to check the temp lib (where temp outfile data lives) exists - const createdTempLib = await this.runCommand({ - command: `CRTLIB LIB(${this.config.tempLibrary}) TEXT('Code for i temporary objects. May be cleared.')`, - noLibList: true - }); - - if (createdTempLib.code === 0) { - tempLibrarySet = true; - } else { - const messages = Tools.parseMessages(createdTempLib.stderr); - if (messages.findId(`CPF2158`) || messages.findId(`CPF2111`)) { //Already exists, hopefully ok :) + tempLibrarySet = await connSettings.setTempLibrary(this.config.tempLibrary); + if (!tempLibrarySet) { + if (libraryListResult.currentLibrary && !libraryListResult.currentLibrary.startsWith(`Q`)) { + //Using ${currentLibrary} as the temporary library for temporary data. + this.config.tempLibrary = this.config.currentLibrary; tempLibrarySet = true; } - else if (messages.findId(`CPD0032`)) { //Can't use CRTLIB - const tempLibExists = await this.runCommand({ - command: `CHKOBJ OBJ(QSYS/${this.config.tempLibrary}) OBJTYPE(*LIB)`, - noLibList: true - }); - - if (tempLibExists.code === 0) { - //We're all good if no errors - tempLibrarySet = true; - } else if (currentLibrary && !currentLibrary.startsWith(`Q`)) { - //Using ${currentLibrary} as the temporary library for temporary data. - this.config.tempLibrary = currentLibrary; - tempLibrarySet = true; - } - } } progress.report({ message: `Checking temporary directory configuration.` }); - let tempDirSet = false; - // Next, we need to check if the temp directory exists - let result = await this.sendCommand({ - command: `[ -d "${this.config.tempDir}" ]` - }); - - if (result.code === 0) { - // Directory exists - tempDirSet = true; - } else { - // Directory does not exist, try to create it - let result = await this.sendCommand({ - command: `mkdir -p ${this.config.tempDir}` - }); - if (result.code === 0) { - // Directory created - tempDirSet = true; - } else { - // Directory not created - } - } + let tempDirSet = await connSettings.setTempDirectory(this.config.tempDir); if (!tempDirSet) { this.config.tempDir = `/tmp`; @@ -429,47 +284,38 @@ export default class IBMi { progress.report({ message: `Clearing temporary data.` }); - - this.runCommand({ - command: `DLTOBJ OBJ(${this.config.tempLibrary}/O_*) OBJTYPE(*FILE)`, - noLibList: true, - }) - .then(result => { - // All good! - if (result && result.stderr) { - const messages = Tools.parseMessages(result.stderr); - if (!messages.findId(`CPF2125`)) { - // @ts-ignore We know the config exists. - vscode.window.showErrorMessage(`Temporary data not cleared from ${this.config.tempLibrary}.`, `View log`).then(async choice => { - if (choice === `View log`) { - this.outputChannel!.show(); - } - }); + //Clear Temporary Library Data + let clearMsg = await connSettings.clearTempLibrary(this.config.tempLibrary); + if (clearMsg) { + // @ts-ignore We know the config exists. + vscode.window.showErrorMessage(clearMsg, `View log`).then + (async choice => { + if (choice === `View log`) { + this.outputChannel!.show(); } - } - }) - - this.sendCommand({ - command: `rm -rf ${path.posix.join(this.config.tempDir, `vscodetemp*`)}` - }) - .then(result => { - // All good! - }) - .catch(e => { - // CPF2125: No objects deleted. - // @ts-ignore We know the config exists. - vscode.window.showErrorMessage(`Temporary data not cleared from ${this.config.tempDir}.`, `View log`).then(async choice => { + }); + } + + //Clear Temporary Directory Data + clearMsg = await connSettings.clearTempDirectory(this.config.tempDir); + if (clearMsg) { + // @ts-ignore We know the config exists. + vscode.window.showErrorMessage(clearMsg, `View log`).then + (async choice => { if (choice === `View log`) { this.outputChannel!.show(); } }); - }); + } + } + //TO DO: why is this required???? const commandShellResult = await this.sendCommand({ command: `echo $SHELL` }); + //TO DO: why is this required???? if (commandShellResult.code === 0) { this.shell = commandShellResult.stdout.trim(); } @@ -482,29 +328,26 @@ export default class IBMi { message: `Checking for bad data areas.` }); - const QCPTOIMPF = await this.runCommand({ - command: `CHKOBJ OBJ(QSYS/QCPTOIMPF) OBJTYPE(*DTAARA)`, - noLibList: true - }); + const QCPTOIMPF = await connSettings.checkObjectExists('QSYS', 'QCPTOIMPF', '*DTAARA'); - if (QCPTOIMPF?.code === 0) { + if (QCPTOIMPF) { vscode.window.showWarningMessage(`The data area QSYS/QCPTOIMPF exists on this system and may impact Code for IBM i functionality.`, { detail: `For V5R3, the code for the command CPYTOIMPF had a major design change to increase functionality and performance. The QSYS/QCPTOIMPF data area lets developers keep the pre-V5R2 version of CPYTOIMPF. Code for IBM i cannot function correctly while this data area exists.`, modal: true, }, `Delete`, `Read more`).then(choice => { switch (choice) { case `Delete`: - this.runCommand({ - command: `DLTOBJ OBJ(QSYS/QCPTOIMPF) OBJTYPE(*DTAARA)`, - noLibList: true - }) - .then((result) => { - if (result?.code === 0) { - vscode.window.showInformationMessage(`The data area QSYS/QCPTOIMPF has been deleted.`); - } else { - vscode.window.showInformationMessage(`Failed to delete the data area QSYS/QCPTOIMPF. Code for IBM i may not work as intended.`); - } - }) + connSettings.deleteObject('QSYS', 'QCPTOIMPF', '*DTAARA').then((result) => { + if (result) { + vscode.window.showInformationMessage(`The data + area QSYS/QCPTOIMPF has been deleted.`); + } + else { + vscode.window.showInformationMessage(`Failed to + delete the data area QSYS/QCPTOIMPF. Code for IBM + i may not work as intended.`); + } + }); break; case `Read more`: vscode.env.openExternal(vscode.Uri.parse(`https://github.com/codefori/vscode-ibmi/issues/476#issuecomment-1018908018`)); @@ -513,23 +356,17 @@ export default class IBMi { }); } - const QCPFRMIMPF = await this.runCommand({ - command: `CHKOBJ OBJ(QSYS/QCPFRMIMPF) OBJTYPE(*DTAARA)`, - noLibList: true - }); + const QCPFRMIMPF = await connSettings.checkObjectExists('QSYS', 'QCPFRMIMPF', '*DTAARA'); - if (QCPFRMIMPF?.code === 0) { + if (QCPFRMIMPF) { vscode.window.showWarningMessage(`The data area QSYS/QCPFRMIMPF exists on this system and may impact Code for IBM i functionality.`, { modal: false, }, `Delete`, `Read more`).then(choice => { switch (choice) { case `Delete`: - this.runCommand({ - command: `DLTOBJ OBJ(QSYS/QCPFRMIMPF) OBJTYPE(*DTAARA)`, - noLibList: true - }) + connSettings.deleteObject('QSYS', 'QCPFRMIMPF', '*DTAARA') .then((result) => { - if (result?.code === 0) { + if (result) { vscode.window.showInformationMessage(`The data area QSYS/QCPFRMIMPF has been deleted.`); } else { vscode.window.showInformationMessage(`Failed to delete the data area QSYS/QCPFRMIMPF. Code for IBM i may not work as intended.`); @@ -553,8 +390,10 @@ export default class IBMi { message: `Checking installed components on host IBM i.` }); + let remoteApps = new IBMiApps(); + // We need to check if our remote programs are installed. - remoteApps.push( + remoteApps.addRemoteApp( { path: `/QSYS.lib/${this.upperCaseName(this.config.tempLibrary)}.lib/`, names: [`GETNEWLIBL.PGM`], @@ -564,25 +403,15 @@ export default class IBMi { //Next, we see what pase features are available (installed via yum) //This may enable certain features in the future. - for (const feature of remoteApps) { + for (const remoteApp of remoteApps.getRemoteApps()) { try { progress.report({ - message: `Checking installed components on host IBM i: ${feature.path}` + message: `Checking installed components on host IBM i: ${remoteApp.path}` }); - const call = await this.sendCommand({ command: `ls -p ${feature.path}${feature.specific || ``}` }); - if (call.stdout) { - const files = call.stdout.split(`\n`); - - if (feature.specific) { - for (const name of feature.names) - this.remoteFeatures[name] = files.find(file => file.includes(name)); - } else { - for (const name of feature.names) - if (files.includes(name)) - this.remoteFeatures[name] = feature.path + name; - } - } + await remoteApps.checkRemoteFeatures(remoteApp, this); + this.remoteFeatures = remoteApps.getRemoteFeatures(); + } catch (e) { console.log(e); } @@ -590,22 +419,6 @@ export default class IBMi { } if (this.sqlRunnerAvailable()) { - //Temporary function to run SQL - - // TODO: stop using this runSQL function and this.runSql - const runSQL = async (statement: string) => { - const output = await this.sendCommand({ - command: `LC_ALL=EN_US.UTF-8 system "call QSYS/QZDFMDB2 PARM('-d' '-i')"`, - stdin: statement - }); - - if (output.code === 0) { - return Tools.db2Parse(output.stdout); - } - else { - throw new Error(output.stdout); - } - }; // Check for ASP information? if (quickConnect === true && cachedServerSettings?.aspInfo) { @@ -617,19 +430,14 @@ 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 runSQL(`SELECT * FROM QSYS2.ASP_INFO`); - resultSet.forEach(row => { - if (row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME !== `null`) { - this.aspInfo[Number(row.ASP_NUMBER)] = String(row.DEVICE_DESCRIPTION_NAME); - } - }); - } catch (e) { - //Oh well + + this.aspInfo = await connSettings.getASPInfo(); + if (Object.keys(this.aspInfo).length === 0) { progress.report({ message: `Failed to get ASP information.` }); } + } // Fetch conversion values? @@ -646,18 +454,11 @@ export default class IBMi { // Next, we're going to see if we can get the CCSID from the user or the system. // Some things don't work without it!!! try { - // we need to grab the system CCSID (QCCSID) - const [systemCCSID] = await runSQL(`select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`); - if (typeof systemCCSID.CURRENT_NUMERIC_VALUE === 'number') { - this.qccsid = systemCCSID.CURRENT_NUMERIC_VALUE; - } + this.qccsid = await connSettings.getQCCSID(); // we grab the users default CCSID - const [userInfo] = await runSQL(`select CHARACTER_CODE_SET_ID from table( QSYS2.QSYUSRINFO( USERNAME => upper('${this.currentUser}') ) )`); - if (userInfo.CHARACTER_CODE_SET_ID !== `null` && typeof userInfo.CHARACTER_CODE_SET_ID === 'number') { - this.jobCcsid = userInfo.CHARACTER_CODE_SET_ID; - } + this.jobCcsid = await connSettings.getjobCCSID(this.currentUser); // if the job ccsid is *SYSVAL, then assign it to sysval if (this.jobCcsid === CCSID_SYSVAL) { @@ -665,36 +466,14 @@ export default class IBMi { } // Let's also get the user's default CCSID - try { - const [activeJob] = await runSQL(`Select DEFAULT_CCSID From Table(QSYS2.ACTIVE_JOB_INFO( JOB_NAME_FILTER => '*', DETAILED_INFO => 'ALL' ))`); - this.userDefaultCCSID = Number(activeJob.DEFAULT_CCSID); - } - catch (error) { - const [defaultCCSID] = (await this.runCommand({ command: "DSPJOB OPTION(*DFNA)" })) - .stdout - .split("\n") - .filter(line => line.includes("DFTCCSID")); - - const defaultCCSCID = Number(defaultCCSID.split("DFTCCSID").at(1)?.trim()); - if (defaultCCSCID && !isNaN(defaultCCSCID)) { - this.userDefaultCCSID = defaultCCSCID; - } - } + this.userDefaultCCSID = await connSettings.getDefaultCCSID(); progress.report({ message: `Fetching local encoding values.` }); - const [variants] = await runSQL(`With VARIANTS ( HASH, AT, DOLLARSIGN ) as (` - + ` values ( cast( x'7B' as varchar(1) )` - + ` , cast( x'7C' as varchar(1) )` - + ` , cast( x'5B' as varchar(1) ) )` - + `)` - + `Select HASH concat AT concat DOLLARSIGN as LOCAL from VARIANTS`); + this.variantChars.local = await connSettings.getLocalEncodingValues(); - if (typeof variants.LOCAL === 'string' && variants.LOCAL !== `null`) { - this.variantChars.local = variants.LOCAL; - } } catch (e) { // Oh well! console.log(e); @@ -737,11 +516,10 @@ export default class IBMi { vscode.window.showInformationMessage(`IBM recommends using bash as your default shell.`, `Set shell to bash`, `Read More`,).then(async choice => { switch (choice) { case `Set shell to bash`: - const commandSetBashResult = await this.sendCommand({ - command: `/QOpenSys/pkgs/bin/chsh -s /QOpenSys/pkgs/bin/bash` - }); - if (!commandSetBashResult.stderr) { + const commandSetBashResult = await connSettings.setBash(); + + if (!commandSetBashResult) { vscode.window.showInformationMessage(`Shell is now bash! Reconnect for change to take effect.`); usesBash = true; } else { @@ -764,72 +542,32 @@ export default class IBMi { }); if ((!quickConnect || !cachedServerSettings?.pathChecked)) { - const currentPaths = (await this.sendCommand({ command: "echo $PATH" })).stdout.split(":"); - const bashrcFile = `${defaultHomeDir}/.bashrc`; - let bashrcExists = (await this.sendCommand({ command: `test -e ${bashrcFile}` })).code === 0; - let reason; - const requiredPaths = ["/QOpenSys/pkgs/bin", "/usr/bin", "/QOpenSys/usr/bin"] - let missingPath; - for (const requiredPath of requiredPaths) { - if (!currentPaths.includes(requiredPath)) { - reason = `Your $PATH shell environment variable does not include ${requiredPath}`; - missingPath = requiredPath - break; - } - } - // If reason is still undefined, then we know the user has all the required paths. Then we don't - // need to check for their existence before checking the order of the required paths. - if (!reason && - (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/usr/bin") - || (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/QOpenSys/usr/bin")))) { - reason = "/QOpenSys/pkgs/bin is not in the right position in your $PATH shell environment variable"; - missingPath = "/QOpenSys/pkgs/bin" - } - if (reason && await vscode.window.showWarningMessage(`${missingPath} not found in $PATH`, { + + const bashrcFile = `${homeResult.homeDir}/.bashrc`; + + let bashrcExists = await connSettings.checkBashRCFile(bashrcFile); + + let checkPathResult = await connSettings.checkPaths(["/QOpenSys/pkgs/bin", "/usr/bin", "/QOpenSys/usr/bin"]); + + if (checkPathResult.reason && await vscode.window.showWarningMessage(`${checkPathResult.missingPath} not found in $PATH`, { modal: true, - detail: `${reason}, so Code for IBM i may not function correctly. Would you like to ${bashrcExists ? "update" : "create"} ${bashrcFile} to fix this now?`, + detail: `${checkPathResult.reason}, so Code for IBM i may not function correctly. Would you like to ${bashrcExists ? "update" : "create"} ${bashrcFile} to fix this now?`, }, `Yes`)) { delayedOperations.push(async () => { this.appendOutput(`${bashrcExists ? "update" : "create"} ${bashrcFile}`); if (!bashrcExists) { - // Add "/usr/bin" and "/QOpenSys/usr/bin" to the end of the path. This way we know that the user has - // all the required paths, but we don't overwrite the priority of other items on their path. - const createBashrc = await this.sendCommand({ command: `echo "# Generated by Code for IBM i\nexport PATH=/QOpenSys/pkgs/bin:\\$PATH:/QOpenSys/usr/bin:/usr/bin" >> ${bashrcFile} && chown ${connectionObject.username.toLowerCase()} ${bashrcFile} && chmod 755 ${bashrcFile}` }); - if (createBashrc.code !== 0) { - vscode.window.showWarningMessage(`Error creating ${bashrcFile}):\n${createBashrc.stderr}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); + //Create bashrc File + let createBashResult = await connSettings.createBashrcFile(bashrcFile,connectionObject.username); + //Error creating bashrc File + if (!createBashResult.createBash) { + vscode.window.showWarningMessage(`Error creating ${bashrcFile}):\n${createBashResult.createBashMsg}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); } } else { - try { - const content = this.content; - if (content) { - const bashrcContent = (await content.downloadStreamfile(bashrcFile)).split("\n"); - let replaced = false; - bashrcContent.forEach((line, index) => { - if (!replaced) { - const pathRegex = /^((?:export )?PATH=)(.*)(?:)$/.exec(line); - if (pathRegex) { - bashrcContent[index] = `${pathRegex[1]}/QOpenSys/pkgs/bin:${pathRegex[2] - .replace("/QOpenSys/pkgs/bin", "") //Removes /QOpenSys/pkgs/bin wherever it is - .replace("::", ":")}:/QOpenSys/usr/bin:/usr/bin`; //Removes double : in case /QOpenSys/pkgs/bin wasn't at the end - replaced = true; - } - } - }); - - if (!replaced) { - bashrcContent.push( - "", - "# Generated by Code for IBM i", - "export PATH=/QOpenSys/pkgs/bin:$PATH:/QOpenSys/usr/bin:/usr/bin" - ); - } - - await content.writeStreamfile(bashrcFile, bashrcContent.join("\n")); - } - } - catch (error) { - vscode.window.showWarningMessage(`Error modifying PATH in ${bashrcFile}):\n${error}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); + //Update bashRC file + let updateBashResult = await connSettings.updateBashrcFile(bashrcFile); + if(!updateBashResult.updateBash) { + vscode.window.showWarningMessage(`Error modifying PATH in ${bashrcFile}):\n${updateBashResult.updateBashMsg}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); } } }); @@ -850,7 +588,7 @@ export default class IBMi { } } - if (defaultHomeDir) { + if (homeResult.homeDir) { if (!tempLibrarySet) { vscode.window.showWarningMessage(`Code for IBM i will not function correctly until the temporary library has been corrected in the settings.`, `Open Settings`) .then(result => { @@ -873,34 +611,14 @@ export default class IBMi { progress.report({ message: `Validate configured library list` }); - let validLibs: string[] = []; - let badLibs: string[] = []; - - const result = await this.sendQsh({ - command: [ - `liblist -d ` + this.defaultUserLibraries.join(` `).replace(/\$/g, `\\$`), - ...this.config.libraryList.map(lib => `liblist -a ` + lib.replace(/\$/g, `\\$`)) - ].join(`; `) - }); - if (result.stderr) { - const lines = result.stderr.split(`\n`); - - lines.forEach(line => { - const badLib = this.config?.libraryList.find(lib => line.includes(`ibrary ${lib} `)); - - // If there is an error about the library, store it - if (badLib) badLibs.push(badLib); - }); - } - - if (result && badLibs.length > 0) { - validLibs = this.config.libraryList.filter(lib => !badLibs.includes(lib)); - const chosen = await vscode.window.showWarningMessage(`The following ${badLibs.length > 1 ? `libraries` : `library`} does not exist: ${badLibs.join(`,`)}. Remove ${badLibs.length > 1 ? `them` : `it`} from the library list?`, `Yes`, `No`); + let libraryListResult = await connSettings.validateLibraryList(this.defaultUserLibraries,this.config.libraryList); + if(libraryListResult.badLibs.length > 0) { + const chosen = await vscode.window.showWarningMessage(`The following ${libraryListResult.badLibs.length > 1 ? `libraries` : `library`} does not exist: ${libraryListResult.badLibs.join(`,`)}. Remove ${libraryListResult.badLibs.length > 1 ? `them` : `it`} from the library list?`, `Yes`, `No`); if (chosen === `Yes`) { - this.config!.libraryList = validLibs; + this.config!.libraryList = libraryListResult.validLibs; } else { - vscode.window.showWarningMessage(`The following libraries does not exist: ${badLibs.join(`,`)}.`); + vscode.window.showWarningMessage(`The following libraries does not exist: ${libraryListResult.badLibs.join(`,`)}.`); } } } @@ -1365,6 +1083,8 @@ export default class IBMi { * @param statements * @returns a Result set */ + + // TODO: stop using this.runSql async runSQL(statements: string): Promise { const { 'QZDFMDB2.PGM': QZDFMDB2 } = this.remoteFeatures; diff --git a/src/api/IBMiApps.ts b/src/api/IBMiApps.ts new file mode 100644 index 000000000..abfc48974 --- /dev/null +++ b/src/api/IBMiApps.ts @@ -0,0 +1,87 @@ +import IBMi from "./IBMi"; +import { RemoteApp, RemoteApps, RemoteFeatures } from "../typings"; + +export default class IBMiApps { + + private remoteApps: RemoteApps; + private remoteFeatures: RemoteFeatures; + + constructor() { + this.remoteApps = [ + { + path: `/usr/bin/`, + names: [`setccsid`, `iconv`, `attr`, `tar`, `ls`] + }, + { + path: `/QOpenSys/pkgs/bin/`, + names: [`git`, `grep`, `tn5250`, `md5sum`, `bash`, `chsh`, `stat`, `sort`, `tar`, `ls`, `find`] + }, + { + path: `/QSYS.LIB/`, + // In the future, we may use a generic specific. + // Right now we only need one program + // specific: `*.PGM`, + specific: `QZDFMDB2.PGM`, + names: [`QZDFMDB2.PGM`] + }, + { + path: `/QIBM/ProdData/IBMiDebugService/bin/`, + specific: `startDebugService.sh`, + names: [`startDebugService.sh`] + } + ]; + + this.remoteFeatures = {}; + this.setRemoteFeatures(); + + } + + addRemoteApp(remoteApp: RemoteApp) { + + //Add remote App + this.remoteApps.push(remoteApp); + + //Add possible features to list + for(const name of remoteApp.names) { + this.remoteFeatures[name] = undefined; + } + + } + + getRemoteApps(): RemoteApps { + return this.remoteApps; + } + + setRemoteFeatures() { + + for (const feature of this.remoteApps) { + for(const name of feature.names) { + this.remoteFeatures[name] = undefined; + } + } + + } + + getRemoteFeatures(): RemoteFeatures { + return this.remoteFeatures; + } + + async checkRemoteFeatures(remoteApp: RemoteApp, connection: IBMi) { + + const call = await connection.sendCommand({ command: `ls -p ${remoteApp.path}${remoteApp.specific || ``}` }); + if (call.stdout) { + const files = call.stdout.split(`\n`); + + if (remoteApp.specific) { + for (const name of remoteApp.names) + this.remoteFeatures[name] = files.find(file => file.includes(name)); + } else { + for (const name of remoteApp.names) + if (files.includes(name)) + this.remoteFeatures[name] = remoteApp.path + name; + } + } + + } + +} \ No newline at end of file diff --git a/src/api/IBMiSettings.ts b/src/api/IBMiSettings.ts new file mode 100644 index 000000000..ad5d07869 --- /dev/null +++ b/src/api/IBMiSettings.ts @@ -0,0 +1,515 @@ +import IBMi from "./IBMi"; +import { Tools } from "./Tools"; +import path from 'path'; +import { aspInfo } from "../typings"; + +const CCSID_SYSVAL = -2; + +export default class IBMiSettings { + + constructor(private connection: IBMi) { + + } + + async checkShellOutput(): Promise { + + const checkShellText = `This should be the only text!`; + const checkShellResult = await this.connection.sendCommand({ + command: `echo "${checkShellText}"`, + directory: `.` + }); + + return Promise.resolve(checkShellResult.stdout.split(`\n`)[0] == checkShellText); + + } + + async getHomeDirectory(): Promise<{ homeExists: boolean, homeDir: string, homeMsg: string }> { + + let homeDir; + let homeMsg = ''; + let homeExists; + + const homeResult = await this.connection.sendCommand({ + command: `echo $HOME && cd && test -w $HOME`, + directory: `.` + }); + + // Note: if the home directory does not exist, the behavior of the echo/cd/test command combo is as follows: + // - stderr contains 'Could not chdir to home directory /home/________: No such file or directory' + // (The output contains 'chdir' regardless of locale and shell, so maybe we could use that + // if we iterate on this code again in the future) + // - stdout contains the name of the home directory (even if it does not exist) + // - The 'cd' command causes an error if the home directory does not exist or otherwise can't be cd'ed into + // - The 'test' command causes an error if the home directory is not writable (one can cd into a non-writable directory) + + homeExists = homeResult.code == 0; + homeDir = homeResult.stdout.trim(); + + if (!homeExists) { + // Let's try to provide more valuable information to the user about why their home directory + // is bad and maybe even provide the opportunity to create the home directory + + // we _could_ just assume the home directory doesn't exist but maybe there's something more going on, namely mucked-up permissions + homeExists = (0 === (await this.connection.sendCommand({ command: `test -e ${homeDir}` })).code); + if (homeExists) { + // Note: this logic might look backward because we fall into this (failure) leg on what looks like success (home dir exists). + // But, remember, but we only got here if 'cd $HOME' failed. + // Let's try to figure out why.... + if (0 !== (await this.connection.sendCommand({ command: `test -d ${homeDir}` })).code) { + homeMsg = `Your home directory (${homeDir}) is not a directory! Code for IBM i may not function correctly. Please contact your system administrator.`; + } + else if (0 !== (await this.connection.sendCommand({ command: `test -w ${homeDir}` })).code) { + homeMsg = `Your home directory (${homeDir}) is not writable! Code for IBM i may not function correctly. Please contact your system administrator.`; + + } + else if (0 !== (await this.connection.sendCommand({ command: `test -x ${homeDir}` })).code) { + homeMsg = `Your home directory (${homeDir}) is not usable due to permissions! Code for IBM i may not function correctly. Please contact your system administrator.`; + } + else { + // not sure, but get your sys admin involved + homeMsg = `Your home directory (${homeDir}) exists but is unusable. Code for IBM i may not function correctly. Please contact your system administrator.`; + } + } + else { + homeMsg = `Your home directory (${homeDir}) does not exist, so Code for IBM i may not function correctly.`; + } + } + + return Promise.resolve({ homeExists, homeDir, homeMsg }); + + } + + async createHomeDirectory(homeDir: string, username: string): Promise<{ homeCreated: boolean, homeMsg: string }> { + + let homeCreated = false; + let homeMsg = ''; + + const homeCmd = `mkdir -p ${homeDir} && chown ${username.toLowerCase()} ${homeDir} && chmod 0755 ${homeDir}`; + + let mkHomeResult = await this.connection.sendCommand({ command: homeCmd, directory: `.` }); + + if (0 === mkHomeResult.code) { + homeCreated = true; + } else { + let mkHomeErrs = mkHomeResult.stderr; + // We still get 'Could not chdir to home directory' in stderr so we need to hackily gut that out, as well as the bashisms that are a side effect of our API + homeMsg = mkHomeErrs.substring(1 + mkHomeErrs.indexOf(`\n`)).replace(`bash: line 1: `, ``); + } + + return Promise.resolve({ homeCreated, homeMsg }); + + } + + async getLibraryList(): Promise<{ libStatus: boolean, currentLibrary: string, defaultUserLibraries: string[] }> { + + + + //Since the compiles are stateless, then we have to set the library list each time we use the `SYSTEM` command + //We setup the defaultUserLibraries here so we can remove them later on so the user can setup their own library list + + let currentLibrary = `QGPL`; + let defaultUserLibraries = []; + let libStatus = false; + + const liblResult = await this.connection.sendQsh({ + command: `liblist` + }); + + if (liblResult.code === 0) { + libStatus = true; + const libraryListString = liblResult.stdout; + if (libraryListString !== ``) { + const libraryList = libraryListString.split(`\n`); + + let lib, type; + for (const line of libraryList) { + lib = line.substring(0, 10).trim(); + type = line.substring(12); + + switch (type) { + case `USR`: + defaultUserLibraries.push(lib); + break; + + case `CUR`: + currentLibrary = lib; + break; + } + } + } + } + + return Promise.resolve({ libStatus, currentLibrary, defaultUserLibraries }); + + } + + async setTempLibrary(tempLibrary: string): Promise { + + let tempLibrarySet = false; + + //Check the temp lib (where temp outfile data lives) exists + const createdTempLib = await this.connection.runCommand({ + command: `CRTLIB LIB(${tempLibrary}) TEXT('Code for i temporary objects. May be cleared.')`, + noLibList: true + }); + + if (createdTempLib.code === 0) { + tempLibrarySet = true; + } + else { + const messages = Tools.parseMessages(createdTempLib.stderr); + if (messages.findId(`CPF2158`) || messages.findId(`CPF2111`)) { //Already exists, hopefully ok :) + tempLibrarySet = true; + } + else if (messages.findId(`CPD0032`)) { //Can't use CRTLIB + const tempLibExists = await this.connection.runCommand({ + command: `CHKOBJ OBJ(QSYS/${tempLibrary}) OBJTYPE(*LIB)`, + noLibList: true + }); + + if (tempLibExists.code === 0) { + //We're all good if no errors + tempLibrarySet = true; + } + else { + tempLibrarySet = false; + } + + } + } + + return Promise.resolve(tempLibrarySet); + + } + + async setTempDirectory(tempDir: string): Promise { + + let tempDirSet = false; + + // Check if the temp directory exists + let result = await this.connection.sendCommand({ + command: `[ -d "${tempDir}" ]` + }); + + if (result.code === 0) { + // Directory exists + tempDirSet = true; + } else { + // Directory does not exist, try to create it + let result = await this.connection.sendCommand({ + command: `mkdir -p ${tempDir}` + }); + if (result.code === 0) { + // Directory created + tempDirSet = true; + } else { + // Directory not created + } + } + + return Promise.resolve(tempDirSet); + + } + + async clearTempLibrary(tempLibrary: string): Promise { + + let clearMsg = ''; + + this.connection.runCommand({ + command: `DLTOBJ OBJ(${tempLibrary}/O_*) OBJTYPE(*FILE)`, + noLibList: true, + }) + .then(result => { + // All good! + if (result && result.stderr) { + const messages = Tools.parseMessages(result.stderr); + if (!messages.findId(`CPF2125`)) { + clearMsg = `Temporary data not cleared from ${tempLibrary}.`; + } + } + }); + + return Promise.resolve(clearMsg); + + } + + async clearTempDirectory(tempDir: string): Promise { + + let clearMsg = ''; + + this.connection.sendCommand({ + command: `rm -rf ${path.posix.join(tempDir, `vscodetemp*`)}` + }) + .then(result => { + // All good! + }) + .catch(e => { + // CPF2125: No objects deleted. + clearMsg = `Temporary data not cleared from ${tempDir}.`; + }); + + return Promise.resolve(clearMsg); + + } + + async checkObjectExists(library: string, object: string, type: string): Promise { + + const objResult = await this.connection.runCommand({ + command: `CHKOBJ OBJ(${library}/${object}) OBJTYPE(${type})`, + noLibList: true + }); + + return Promise.resolve(objResult.code === 0); + + } + + async deleteObject(library: string, object: string, type: string): Promise { + + const deleteResult = await this.connection.runCommand({ + command: `DLTOBJ OBJ(${library}/${object}) OBJTYPE(${type})`, + noLibList: true + }); + + return Promise.resolve(deleteResult.code === 0); + + } + + async getASPInfo(): Promise { + + let aspInfo: aspInfo = {}; + + try { + const resultSet = await this.connection.runSQL(`SELECT * FROM QSYS2.ASP_INFO`); + if (resultSet.length) { + resultSet.forEach(row => { + if (row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME !== `null`) { + aspInfo[Number(row.ASP_NUMBER)] = String(row.DEVICE_DESCRIPTION_NAME); + } + }); + } + } catch (e) { + //Oh well + aspInfo = {}; + } + + return Promise.resolve(aspInfo); + + } + + async getQCCSID(): Promise { + + let qccsid = 0; + + const [systemCCSID] = await this.connection.runSQL(`select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`); + if (typeof systemCCSID.CURRENT_NUMERIC_VALUE === 'number') { + qccsid = systemCCSID.CURRENT_NUMERIC_VALUE; + } + + return Promise.resolve(qccsid); + + } + + async getjobCCSID(userName: string): Promise { + + let jobCCSID = CCSID_SYSVAL; + + const [userInfo] = await this.connection.runSQL(`select CHARACTER_CODE_SET_ID from table( QSYS2.QSYUSRINFO( USERNAME => upper('${userName}') ) )`); + if (userInfo.CHARACTER_CODE_SET_ID !== `null` && typeof userInfo.CHARACTER_CODE_SET_ID === 'number') { + jobCCSID = userInfo.CHARACTER_CODE_SET_ID; + } + + return Promise.resolve(jobCCSID); + + } + + async getDefaultCCSID(): Promise { + + let userDefaultCCSID = 0; + + try { + const [activeJob] = await this.connection.runSQL(`Select DEFAULT_CCSID From Table(QSYS2.ACTIVE_JOB_INFO( JOB_NAME_FILTER => '*', DETAILED_INFO => 'ALL' ))`); + userDefaultCCSID = Number(activeJob.DEFAULT_CCSID); + } + catch (error) { + const [defaultCCSID] = (await this.connection.runCommand({ command: "DSPJOB OPTION(*DFNA)" })) + .stdout + .split("\n") + .filter(line => line.includes("DFTCCSID")); + + const defaultCCSCID = Number(defaultCCSID.split("DFTCCSID").at(1)?.trim()); + if (defaultCCSCID && !isNaN(defaultCCSCID)) { + userDefaultCCSID = defaultCCSCID; + } + } + + return Promise.resolve(userDefaultCCSID); + + } + + async getLocalEncodingValues(): Promise { + + let localEncoding = ''; + + const [variants] = await this.connection.runSQL(`With VARIANTS ( HASH, AT, DOLLARSIGN ) as (` + + ` values ( cast( x'7B' as varchar(1) )` + + ` , cast( x'7C' as varchar(1) )` + + ` , cast( x'5B' as varchar(1) ) )` + + `)` + + `Select HASH concat AT concat DOLLARSIGN as LOCAL from VARIANTS`); + + if (typeof variants.LOCAL === 'string' && variants.LOCAL !== `null`) { + localEncoding = variants.LOCAL; + } + + return Promise.resolve(localEncoding); + + } + + async setBash(): Promise { + + let bashset = false; + + const commandSetBashResult = await this.connection.sendCommand({ + command: `/QOpenSys/pkgs/bin/chsh -s /QOpenSys/pkgs/bin/bash` + }); + + if (!commandSetBashResult.stderr) bashset = true; + + return Promise.resolve(bashset); + + } + + async getEnvironmentVariable(envVar: string): Promise { + return (await this.connection.sendCommand({ command: `echo ${envVar}` })).stdout.split(":"); + } + + async checkPaths(requiredPaths: string[]): Promise<{ reason: string, missingPath: string }> { + + const currentPaths = await this.getEnvironmentVariable('$PATH'); + + let reason = ''; + let missingPath = ''; + + for (const requiredPath of requiredPaths) { + if (!currentPaths.includes(requiredPath)) { + reason = `Your $PATH shell environment variable does not include ${requiredPath}`; + missingPath = requiredPath + break; + } + } + // If reason is still undefined, then we know the user has all the required paths. Then we don't + // need to check for their existence before checking the order of the required paths. + if (!reason) { + for (let x = 1; x <= requiredPaths.length; x++) { + if (currentPaths.indexOf(requiredPaths[0]) > currentPaths.indexOf(requiredPaths[x])) { + reason = `${requiredPaths[0]} is not in the right position in your $PATH shell environment variable`; + missingPath = requiredPaths[0]; + break; + } + } + } + + return Promise.resolve({ reason: reason, missingPath: missingPath }); + + } + + async checkBashRCFile(bashrcFile: string): Promise { + + let bashrcExists = false; + + bashrcExists = (await this.connection.sendCommand({ command: `test -e ${bashrcFile}` })).code === 0; + + return Promise.resolve(bashrcExists); + } + + async createBashrcFile(bashrcFile: string, username: string): Promise<{ createBash: boolean, createBashMsg: string }> { + + let createBash = true; + let createBashMsg = ''; + + // Add "/usr/bin" and "/QOpenSys/usr/bin" to the end of the path. This way we know that the user has + // all the required paths, but we don't overwrite the priority of other items on their path. + const createBashrc = await this.connection.sendCommand({ command: `echo "# Generated by Code for IBM i\nexport PATH=/QOpenSys/pkgs/bin:\\$PATH:/QOpenSys/usr/bin:/usr/bin" >> ${bashrcFile} && chown ${username.toLowerCase()} ${bashrcFile} && chmod 755 ${bashrcFile}` }); + + if (createBashrc.code !== 0) { + createBash = false; + createBashMsg = createBashrc.stderr; + } + + return Promise.resolve({ createBash, createBashMsg }); + + } + + async updateBashrcFile(bashrcFile: string): Promise<{ updateBash: boolean, updateBashMsg: string }> { + + let updateBash = true; + let updateBashMsg = ''; + + try { + const content = this.connection.content; + if (content) { + const bashrcContent = (await content.downloadStreamfile(bashrcFile)).split("\n"); + let replaced = false; + bashrcContent.forEach((line, index) => { + if (!replaced) { + const pathRegex = /^((?:export )?PATH=)(.*)(?:)$/.exec(line); + if (pathRegex) { + bashrcContent[index] = `${pathRegex[1]}/QOpenSys/pkgs/bin:${pathRegex[2] + .replace("/QOpenSys/pkgs/bin", "") //Removes /QOpenSys/pkgs/bin wherever it is + .replace("::", ":")}:/QOpenSys/usr/bin:/usr/bin`; //Removes double : in case /QOpenSys/pkgs/bin wasn't at the end + replaced = true; + } + } + }); + + if (!replaced) { + bashrcContent.push( + "", + "# Generated by Code for IBM i", + "export PATH=/QOpenSys/pkgs/bin:$PATH:/QOpenSys/usr/bin:/usr/bin" + ); + } + + await content.writeStreamfile(bashrcFile, bashrcContent.join("\n")); + } + } + catch (error) { + updateBash = false; + updateBashMsg = error; + } + + return Promise.resolve({ updateBash, updateBashMsg }); + } + + async validateLibraryList(defaultUserLibraries: string[], libraryList: string[]): Promise<{validLibs: string[], badLibs: string[]}> { + + let validLibs: string[] = []; + let badLibs: string[] = []; + + const result = await this.connection.sendQsh({ + command: [ + `liblist -d ` + defaultUserLibraries.join(` `).replace(/\$/g, `\\$`), + ...libraryList.map(lib => `liblist -a ` + lib.replace(/\$/g, `\\$`)) + ].join(`; `) + }); + + if (result.stderr) { + const lines = result.stderr.split(`\n`); + + lines.forEach(line => { + const badLib = libraryList.find(lib => line.includes(`ibrary ${lib} `)); + + // If there is an error about the library, store it + if (badLib) badLibs.push(badLib); + }); + } + + if (result && badLibs.length > 0) { + validLibs = libraryList.filter(lib => !badLibs.includes(lib)); + } + + return Promise.resolve({validLibs,badLibs}); + + } + +} \ No newline at end of file diff --git a/src/typings.ts b/src/typings.ts index cceef76f0..531e73849 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -238,4 +238,23 @@ export type SearchHit = { export type SearchHitLine = { number: number content: string -} \ No newline at end of file +} + +export interface MemberParts extends IBMiMember { + basename: string +} + + +export type RemoteApp = { + path: string + specific ?: string + names: string[] +} + +export type RemoteApps = RemoteApp[]; + +export type RemoteFeature = string | undefined; + +export type RemoteFeatures = { [name: string]: RemoteFeature }; + +export type aspInfo = { [id: number]: string }; From 9564ffcaf664e1dced3bd5b5867545c95089ae92 Mon Sep 17 00:00:00 2001 From: krethan Date: Tue, 1 Oct 2024 23:38:26 -0600 Subject: [PATCH 10/28] Fixed few issues introduced with IBMiSettings class. --- src/api/IBMi.ts | 30 +++++++++++++++----------- src/api/IBMiSettings.ts | 48 ++++++++++++++++++++--------------------- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 5e929a48b..4ee77eaa8 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -243,7 +243,7 @@ export default class IBMi { let libraryListResult = await connSettings.getLibraryList(); if (libraryListResult.libStatus) { - + this.defaultUserLibraries = libraryListResult.defaultUserLibraries; //If this is the first time the config is made, then these arrays will be empty @@ -409,12 +409,14 @@ export default class IBMi { }); await remoteApps.checkRemoteFeatures(remoteApp, this); - this.remoteFeatures = remoteApps.getRemoteFeatures(); } catch (e) { console.log(e); } } + + this.remoteFeatures = remoteApps.getRemoteFeatures(); + } if (this.sqlRunnerAvailable()) { @@ -429,9 +431,11 @@ 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. - - this.aspInfo = await connSettings.getASPInfo(); - if (Object.keys(this.aspInfo).length === 0) { + try { + this.aspInfo = await connSettings.getASPInfo(); + } + catch (e) { + //Oh well progress.report({ message: `Failed to get ASP information.` }); @@ -541,13 +545,13 @@ export default class IBMi { }); if ((!quickConnect || !cachedServerSettings?.pathChecked)) { - + const bashrcFile = `${homeResult.homeDir}/.bashrc`; - + let bashrcExists = await connSettings.checkBashRCFile(bashrcFile); - + let checkPathResult = await connSettings.checkPaths(["/QOpenSys/pkgs/bin", "/usr/bin", "/QOpenSys/usr/bin"]); - + if (checkPathResult.reason && await vscode.window.showWarningMessage(`${checkPathResult.missingPath} not found in $PATH`, { modal: true, detail: `${checkPathResult.reason}, so Code for IBM i may not function correctly. Would you like to ${bashrcExists ? "update" : "create"} ${bashrcFile} to fix this now?`, @@ -556,7 +560,7 @@ export default class IBMi { this.appendOutput(`${bashrcExists ? "update" : "create"} ${bashrcFile}`); if (!bashrcExists) { //Create bashrc File - let createBashResult = await connSettings.createBashrcFile(bashrcFile,connectionObject.username); + let createBashResult = await connSettings.createBashrcFile(bashrcFile, connectionObject.username); //Error creating bashrc File if (!createBashResult.createBash) { vscode.window.showWarningMessage(`Error creating ${bashrcFile}):\n${createBashResult.createBashMsg}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); @@ -565,7 +569,7 @@ export default class IBMi { else { //Update bashRC file let updateBashResult = await connSettings.updateBashrcFile(bashrcFile); - if(!updateBashResult.updateBash) { + if (!updateBashResult.updateBash) { vscode.window.showWarningMessage(`Error modifying PATH in ${bashrcFile}):\n${updateBashResult.updateBashMsg}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); } } @@ -611,8 +615,8 @@ export default class IBMi { message: `Validate configured library list` }); - let libraryListResult = await connSettings.validateLibraryList(this.defaultUserLibraries,this.config.libraryList); - if(libraryListResult.badLibs.length > 0) { + let libraryListResult = await connSettings.validateLibraryList(this.defaultUserLibraries, this.config.libraryList); + if (libraryListResult.badLibs.length > 0) { const chosen = await vscode.window.showWarningMessage(`The following ${libraryListResult.badLibs.length > 1 ? `libraries` : `library`} does not exist: ${libraryListResult.badLibs.join(`,`)}. Remove ${libraryListResult.badLibs.length > 1 ? `them` : `it`} from the library list?`, `Yes`, `No`); if (chosen === `Yes`) { this.config!.libraryList = libraryListResult.validLibs; diff --git a/src/api/IBMiSettings.ts b/src/api/IBMiSettings.ts index ad5d07869..c3eb961a9 100644 --- a/src/api/IBMiSettings.ts +++ b/src/api/IBMiSettings.ts @@ -289,7 +289,7 @@ export default class IBMiSettings { } } catch (e) { //Oh well - aspInfo = {}; + return Promise.reject(e); } return Promise.resolve(aspInfo); @@ -399,14 +399,12 @@ export default class IBMiSettings { } // If reason is still undefined, then we know the user has all the required paths. Then we don't // need to check for their existence before checking the order of the required paths. - if (!reason) { - for (let x = 1; x <= requiredPaths.length; x++) { - if (currentPaths.indexOf(requiredPaths[0]) > currentPaths.indexOf(requiredPaths[x])) { - reason = `${requiredPaths[0]} is not in the right position in your $PATH shell environment variable`; - missingPath = requiredPaths[0]; - break; - } - } + + if (!reason && + (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/usr/bin") + || (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/QOpenSys/usr/bin")))) { + reason = "/QOpenSys/pkgs/bin is not in the right position in your $PATH shell environment variable"; + missingPath = "/QOpenSys/pkgs/bin" } return Promise.resolve({ reason: reason, missingPath: missingPath }); @@ -481,34 +479,34 @@ export default class IBMiSettings { return Promise.resolve({ updateBash, updateBashMsg }); } - async validateLibraryList(defaultUserLibraries: string[], libraryList: string[]): Promise<{validLibs: string[], badLibs: string[]}> { - - let validLibs: string[] = []; - let badLibs: string[] = []; + async validateLibraryList(defaultUserLibraries: string[], libraryList: string[]): Promise<{ validLibs: string[], badLibs: string[] }> { + + let validLibs: string[] = []; + let badLibs: string[] = []; - const result = await this.connection.sendQsh({ + const result = await this.connection.sendQsh({ command: [ - `liblist -d ` + defaultUserLibraries.join(` `).replace(/\$/g, `\\$`), - ...libraryList.map(lib => `liblist -a ` + lib.replace(/\$/g, `\\$`)) + `liblist -d ` + defaultUserLibraries.join(` `).replace(/\$/g, `\\$`), + ...libraryList.map(lib => `liblist -a ` + lib.replace(/\$/g, `\\$`)) ].join(`; `) - }); + }); - if (result.stderr) { + if (result.stderr) { const lines = result.stderr.split(`\n`); lines.forEach(line => { - const badLib = libraryList.find(lib => line.includes(`ibrary ${lib} `)); + const badLib = libraryList.find(lib => line.includes(`ibrary ${lib} `)); - // If there is an error about the library, store it - if (badLib) badLibs.push(badLib); + // If there is an error about the library, store it + if (badLib) badLibs.push(badLib); }); - } + } - if (result && badLibs.length > 0) { + if (result && badLibs.length > 0) { validLibs = libraryList.filter(lib => !badLibs.includes(lib)); - } + } - return Promise.resolve({validLibs,badLibs}); + return Promise.resolve({ validLibs, badLibs }); } From 9f6a3556146aa07eb5a65ae80e8e5233503ce450 Mon Sep 17 00:00:00 2001 From: krethan Date: Tue, 1 Oct 2024 23:41:30 -0600 Subject: [PATCH 11/28] Removed unneccessary comments --- src/api/IBMi.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 4ee77eaa8..13ba090b2 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -309,12 +309,10 @@ export default class IBMi { } - //TO DO: why is this required???? const commandShellResult = await this.sendCommand({ command: `echo $SHELL` }); - //TO DO: why is this required???? if (commandShellResult.code === 0) { this.shell = commandShellResult.stdout.trim(); } From 226d0ce05f6201b88c7cd64f74d4b0cb2cf9c868 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 3 Oct 2024 18:06:34 +0200 Subject: [PATCH 12/28] Fixed wrong import Signed-off-by: Seb Julliand --- src/api/IBMi.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 838bce179..06ec6b5d8 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -4,7 +4,7 @@ import * as node_ssh from "node-ssh"; import os from "os"; import path, { parse as parsePath } from 'path'; import * as vscode from "vscode"; -import { IBMiComponent, IBMiComponentType } from "../components/componen +import { IBMiComponent, IBMiComponentType } from "../components/component"; import { CopyToImport } from "../components/copyToImport"; import { ComponentManager } from "../components/manager"; import { instance } from "../instantiate"; @@ -1353,7 +1353,7 @@ export default class IBMi { } } - getComponent(type: IBMiComponentType, ignoreState?:boolean): T | undefined { + getComponent(type: IBMiComponentType, ignoreState?: boolean): T | undefined { return this.componentManager.get(type, ignoreState); } From 34fda10907b7588ee55ee6ca99730a01c77debbd Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Fri, 4 Oct 2024 17:22:45 +0200 Subject: [PATCH 13/28] Always show "Download client certificate" button Especially when there is a problem... Signed-off-by: Seb Julliand --- src/webviews/settings/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/webviews/settings/index.ts b/src/webviews/settings/index.ts index 7694d390d..4ec8d6ade 100644 --- a/src/webviews/settings/index.ts +++ b/src/webviews/settings/index.ts @@ -199,10 +199,8 @@ export class SettingsUI { localCertificateIssue = `${String(error)}. Debugging will not function correctly.`; } debuggerTab.addParagraph(`${localCertificateIssue || "Client certificate for service has been imported and matches remote certificate."}`) - .addParagraph(`To debug on IBM i, Visual Studio Code needs to load a client certificate to connect to the Debug Service. Each server has a unique certificate. This client certificate should exist at ${certificates.getLocalCertPath(connection)}`); - if (!localCertificateIssue) { - debuggerTab.addButtons({ id: `import`, label: `Download client certificate` }) - } + .addParagraph(`To debug on IBM i, Visual Studio Code needs to load a client certificate to connect to the Debug Service. Each server has a unique certificate. This client certificate should exist at ${certificates.getLocalCertPath(connection)}`) + .addButtons({ id: `import`, label: `Download client certificate` }); } else { debuggerTab.addParagraph(`The service certificate doesn't exist or is incomplete; it must be generated before the debug service can be started.`) From f79e370e08962acc2a8032ca719257dbbced4144 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Fri, 4 Oct 2024 13:35:44 -0400 Subject: [PATCH 14/28] Use type for component state instead of enum Signed-off-by: worksofliam --- src/components/component.ts | 14 ++++---------- src/components/copyToImport.ts | 4 ++-- src/components/getMemberInfo.ts | 10 +++++----- src/components/getNewLibl.ts | 12 ++++++------ src/components/manager.ts | 2 +- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/components/component.ts b/src/components/component.ts index 0612c6f11..74e2d1e47 100644 --- a/src/components/component.ts +++ b/src/components/component.ts @@ -1,12 +1,6 @@ import IBMi from "../api/IBMi"; -export const enum ComponentState { - NotChecked = `Not checked`, - NotInstalled = `Not installed`, - Installed = `Installed`, - NeedUpdate = `Need update`, - Error = `Error`, -} +export type ComponentState = `NotChecked` | `NotInstalled` | `Installed` | `NeedsUpdate` | `Error`; export type ComponentIdentification = { name: string @@ -39,7 +33,7 @@ export type IBMiComponentType = new (c: IBMi) => T; * */ export abstract class IBMiComponent { - private state = ComponentState.NotChecked; + private state: ComponentState = `NotChecked`; constructor(protected readonly connection: IBMi) { @@ -52,14 +46,14 @@ export abstract class IBMiComponent { async check() { try { this.state = await this.getRemoteState(); - if (this.state !== ComponentState.Installed) { + if (this.state !== `Installed`) { this.state = await this.update(); } } catch (error) { console.log(`Error occurred while checking component ${this.toString()}`); console.log(error); - this.state = ComponentState.Error; + this.state = `Error`; } return this; diff --git a/src/components/copyToImport.ts b/src/components/copyToImport.ts index 7232acc18..854b73062 100644 --- a/src/components/copyToImport.ts +++ b/src/components/copyToImport.ts @@ -17,8 +17,8 @@ export class CopyToImport extends IBMiComponent { return { name: 'CopyToImport', version: 1 }; } - protected getRemoteState() { - return ComponentState.Installed; + protected getRemoteState(): ComponentState { + return `Installed`; } protected update(): ComponentState | Promise { diff --git a/src/components/getMemberInfo.ts b/src/components/getMemberInfo.ts index 2f86720e2..0caa3bcb0 100644 --- a/src/components/getMemberInfo.ts +++ b/src/components/getMemberInfo.ts @@ -22,13 +22,13 @@ export class GetMemberInfo extends IBMiComponent { } } if (this.installedVersion < this.currentVersion) { - return ComponentState.NeedUpdate; + return `NeedsUpdate`; } - return ComponentState.Installed; + return `Installed`; } - protected async update() { + protected async update(): Promise { const config = this.connection.config!; return this.connection.withTempDirectory(async tempDir => { const tempSourcePath = posix.join(tempDir, `getMemberInfo.sql`); @@ -40,9 +40,9 @@ export class GetMemberInfo extends IBMiComponent { }); if (result.code) { - return ComponentState.Error; + return `Error`; } else { - return ComponentState.Installed; + return `Installed`; } }); } diff --git a/src/components/getNewLibl.ts b/src/components/getNewLibl.ts index 64014eef7..7d0b98860 100644 --- a/src/components/getNewLibl.ts +++ b/src/components/getNewLibl.ts @@ -7,14 +7,14 @@ export class GetNewLibl extends IBMiComponent { return { name: 'GetNewLibl', version: 1 }; } - protected async getRemoteState() { - return this.connection.remoteFeatures[`GETNEWLIBL.PGM`] ? ComponentState.Installed : ComponentState.NotInstalled; + protected async getRemoteState(): Promise { + return this.connection.remoteFeatures[`GETNEWLIBL.PGM`] ? `Installed` : `NotInstalled`; } - protected update() { + protected update(): Promise { const config = this.connection.config! const content = instance.getContent(); - return this.connection.withTempDirectory(async tempDir => { + return this.connection.withTempDirectory(async (tempDir): Promise => { const tempSourcePath = posix.join(tempDir, `getnewlibl.sql`); await content!.writeStreamfileRaw(tempSourcePath, getSource(config.tempLibrary)); @@ -25,9 +25,9 @@ export class GetNewLibl extends IBMiComponent { }); if (!result.code) { - return ComponentState.Installed; + return `Installed`; } else { - return ComponentState.Error; + return `Error`; } }); } diff --git a/src/components/manager.ts b/src/components/manager.ts index ec0c19152..60b8d9e0e 100644 --- a/src/components/manager.ts +++ b/src/components/manager.ts @@ -39,7 +39,7 @@ export class ComponentManager { get(type: IBMiComponentType, ignoreState?: boolean): T | undefined { const component = this.registered.get(type); - if (component && (ignoreState || component.getState() === ComponentState.Installed)) { + if (component && (ignoreState || component.getState() === `Installed`)) { return component as T; } } From 41609bec727a02eea2b38132e61de0d7c87a24cd Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 7 Oct 2024 21:57:51 +0200 Subject: [PATCH 15/28] Discover IBM i Java installation Signed-off-by: Seb Julliand --- src/api/IBMi.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 06ec6b5d8..789074727 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -113,6 +113,10 @@ export default class IBMi { tar: undefined, ls: undefined, find: undefined, + jdk80: undefined, + jdk11: undefined, + jdk17: undefined, + openjdk11: undefined }; this.variantChars = { @@ -587,6 +591,16 @@ export default class IBMi { console.log(e); } } + + //Specific Java installations check + progress.report({ + message: `Checking installed components on host IBM i: Java` + }); + const javaCheck = async (root: string) => await this.content.testStreamFile(`${root}/bin/java`, 'x') ? `${root}` : undefined; + this.remoteFeatures.jdk80 = await javaCheck(`/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit`); + this.remoteFeatures.jdk11 = await javaCheck(`/QOpenSys/QIBM/ProdData/JavaVM/jdk11/64bit`); + this.remoteFeatures.openjdk11 = await javaCheck(`/QOpensys/pkgs/lib/jvm/openjdk-11`); + this.remoteFeatures.jdk17 = await javaCheck(`/QOpenSys/QIBM/ProdData/JavaVM/jdk17/64bit`); } if (this.sqlRunnerAvailable()) { From d23ff7d0e269ffb46e5f535f6dd6c755def13a09 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 7 Oct 2024 22:10:36 +0200 Subject: [PATCH 16/28] Use IBMi remote Java feature for debug service Signed-off-by: Seb Julliand --- src/api/debug/certificates.ts | 4 ++-- src/api/debug/config.ts | 12 ++++++++---- src/locale/ids/da.json | 1 + src/locale/ids/de.json | 1 + src/locale/ids/en.json | 1 + src/locale/ids/fr.json | 1 + src/locale/ids/no.json | 1 + src/locale/ids/pl.json | 1 + src/testing/debug.ts | 31 +++++++++++++++++++++++++++++++ src/testing/index.ts | 2 ++ 10 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 src/testing/debug.ts diff --git a/src/api/debug/certificates.ts b/src/api/debug/certificates.ts index e8fa7eae6..2a36452a8 100644 --- a/src/api/debug/certificates.ts +++ b/src/api/debug/certificates.ts @@ -170,7 +170,7 @@ export async function setup(connection: IBMi, imported?: ImportedCertificate) { debugConfig.delete("DEBUG_SERVICE_KEYSTORE_PASSWORD"); await debugConfig.save(); } - const javaHome = getJavaHome((await getDebugServiceDetails()).java); + const javaHome = getJavaHome(connection, (await getDebugServiceDetails()).java); const encryptResult = await connection.sendCommand({ command: `${path.posix.join(debugConfig.getRemoteServiceBin(), `encryptKeystorePassword.sh`)} | /usr/bin/tail -n 1`, env: { @@ -268,7 +268,7 @@ export async function sanityCheck(connection: IBMi, content: IBMiContent) { //Check if java home needs to be updated if the service got updated (e.g: v1 uses Java 8 and v2 uses Java 11) const javaHome = debugConfig.get("JAVA_HOME"); - const expectedJavaHome = getJavaHome((await getDebugServiceDetails()).java); + const expectedJavaHome = getJavaHome(connection, (await getDebugServiceDetails()).java); if (javaHome && javaHome !== expectedJavaHome) { if (await content.testStreamFile(DEBUG_CONFIG_FILE, "w")) { //Automatically make the change if possible diff --git a/src/api/debug/config.ts b/src/api/debug/config.ts index 324402d40..d399de841 100644 --- a/src/api/debug/config.ts +++ b/src/api/debug/config.ts @@ -2,6 +2,7 @@ import path from "path"; import vscode from "vscode"; import { instance } from "../../instantiate"; import { t } from "../../locale"; +import IBMi from "../IBMi"; import { SERVICE_CERTIFICATE } from "./certificates"; type ConfigLine = { @@ -170,9 +171,12 @@ export async function getDebugServiceDetails(): Promise { return debugServiceDetails; } -export function getJavaHome(version: string) { - switch (version) { - case "11": return `/QOpenSys/QIBM/ProdData/JavaVM/jdk11/64bit`; - default: return `/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit`; +export function getJavaHome(connection: IBMi, version: string) { + version = version.padEnd(2, '0'); + const javaHome = connection.remoteFeatures[`jdk${version}`]; + if (!javaHome) { + throw new Error(t('java.not.found', version)); } + + return javaHome; } \ No newline at end of file diff --git a/src/locale/ids/da.json b/src/locale/ids/da.json index ab549a1c8..73a6839c3 100644 --- a/src/locale/ids/da.json +++ b/src/locale/ids/da.json @@ -224,6 +224,7 @@ "ifsBrowser.uploadStreamfile.select.type.title": "Hvad ønsker du at uploade?", "ifsBrowser.uploadStreamfile.uploadedFiles": "Upload er udført.", "JAVA_HOME": "Java Home", + "java.not.found":"Java version {0} is not installed.", "job": "Job", "JOB_NAME_SHORT": "Job navn", "JOB_NUMBER": "Job nummer", diff --git a/src/locale/ids/de.json b/src/locale/ids/de.json index 34c686177..0fae41858 100644 --- a/src/locale/ids/de.json +++ b/src/locale/ids/de.json @@ -224,6 +224,7 @@ "ifsBrowser.uploadStreamfile.select.type.title": "Was möchten Sie hochladen?", "ifsBrowser.uploadStreamfile.uploadedFiles": "Hochladen abgeschlossen.", "JAVA_HOME": "Java-Home Pfad", + "java.not.found":"Java version {0} is not installed.", "job": "Job", "JOB_NAME_SHORT": "Job Name", "JOB_NUMBER": "Job Nummer", diff --git a/src/locale/ids/en.json b/src/locale/ids/en.json index d73988885..ee6b11c5b 100644 --- a/src/locale/ids/en.json +++ b/src/locale/ids/en.json @@ -224,6 +224,7 @@ "ifsBrowser.uploadStreamfile.select.type.title": "What do you want to upload?", "ifsBrowser.uploadStreamfile.uploadedFiles": "Upload completed.", "JAVA_HOME": "Java Home", + "java.not.found":"Java version {0} is not installed.", "job": "Job", "JOB_NAME_SHORT": "Job name", "JOB_NUMBER": "Job number", diff --git a/src/locale/ids/fr.json b/src/locale/ids/fr.json index 3ef4b6849..bc59252ba 100644 --- a/src/locale/ids/fr.json +++ b/src/locale/ids/fr.json @@ -224,6 +224,7 @@ "ifsBrowser.uploadStreamfile.select.type.title": "Que souhaitez-vous envoyer?", "ifsBrowser.uploadStreamfile.uploadedFiles": "Envoi terminée.", "JAVA_HOME": "Java Home", + "java.not.found":"Java version {0} n'est pas installé.", "job": "Job", "JOB_NAME_SHORT": "Nom du job", "JOB_NUMBER": "Numéro du job", diff --git a/src/locale/ids/no.json b/src/locale/ids/no.json index 7fdc97dab..c38a9ca7e 100644 --- a/src/locale/ids/no.json +++ b/src/locale/ids/no.json @@ -224,6 +224,7 @@ "ifsBrowser.uploadStreamfile.select.type.title": "Hva ønsker du å uploade?", "ifsBrowser.uploadStreamfile.uploadedFiles": "Upload er utført.", "JAVA_HOME": "Java Home", + "java.not.found":"Java version {0} is not installed.", "job": "Jobb", "JOB_NAME_SHORT": "Jobb navn", "JOB_NUMBER": "Jobb nummer", diff --git a/src/locale/ids/pl.json b/src/locale/ids/pl.json index 50b762bf5..fa79eb6c5 100644 --- a/src/locale/ids/pl.json +++ b/src/locale/ids/pl.json @@ -224,6 +224,7 @@ "ifsBrowser.uploadStreamfile.select.type.title": "Co chcesz przesłać?", "ifsBrowser.uploadStreamfile.uploadedFiles": "Przesyłanie zakończone.", "JAVA_HOME": "Java Home", + "java.not.found":"Java version {0} is not installed.", "job": "Zadanie", "JOB_NAME_SHORT": "Nazwa zadania`", "JOB_NUMBER": "Numer zadania", diff --git a/src/testing/debug.ts b/src/testing/debug.ts new file mode 100644 index 000000000..709091cb0 --- /dev/null +++ b/src/testing/debug.ts @@ -0,0 +1,31 @@ +import assert from "assert"; +import { TestSuite } from "."; +import { getJavaHome } from "../api/debug/config"; +import { instance } from "../instantiate"; + +export const DebugSuite: TestSuite = { + name: `Debug engine tests`, + tests: [ + { + name: "Check Java versions", test: async () => { + const connection = instance.getConnection()!; + if(connection.remoteFeatures.jdk80){ + const jdk8 = getJavaHome(connection, '8'); + assert.strictEqual(jdk8, connection.remoteFeatures.jdk80); + } + + if(connection.remoteFeatures.jdk11){ + const jdk11 = getJavaHome(connection, '11'); + assert.strictEqual(jdk11, connection.remoteFeatures.jdk11); + } + + if(connection.remoteFeatures.jdk17){ + const jdk11 = getJavaHome(connection, '17'); + assert.strictEqual(jdk11, connection.remoteFeatures.jdk11); + } + + assert.throws(() => getJavaHome(connection, '666')); + } + } + ] +} \ No newline at end of file diff --git a/src/testing/index.ts b/src/testing/index.ts index 8ea80a0c4..b4c76b3c3 100644 --- a/src/testing/index.ts +++ b/src/testing/index.ts @@ -5,6 +5,7 @@ import { ActionSuite } from "./action"; import { ComponentSuite } from "./components"; import { ConnectionSuite } from "./connection"; import { ContentSuite } from "./content"; +import { DebugSuite } from "./debug"; import { DeployToolsSuite } from "./deployTools"; import { EncodingSuite } from "./encoding"; import { FilterSuite } from "./filter"; @@ -18,6 +19,7 @@ const suites: TestSuite[] = [ ActionSuite, ConnectionSuite, ContentSuite, + DebugSuite, DeployToolsSuite, ToolsSuite, ILEErrorSuite, From d3a5e508499af224464f4ab7cd736bef7730ebf9 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 7 Oct 2024 22:15:56 +0200 Subject: [PATCH 17/28] Fix debug java test Signed-off-by: Seb Julliand --- src/testing/debug.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing/debug.ts b/src/testing/debug.ts index 709091cb0..44aa2e89d 100644 --- a/src/testing/debug.ts +++ b/src/testing/debug.ts @@ -21,7 +21,7 @@ export const DebugSuite: TestSuite = { if(connection.remoteFeatures.jdk17){ const jdk11 = getJavaHome(connection, '17'); - assert.strictEqual(jdk11, connection.remoteFeatures.jdk11); + assert.strictEqual(jdk11, connection.remoteFeatures.jdk17); } assert.throws(() => getJavaHome(connection, '666')); From 449783f75f56f36f093213b2e770aa1db5570b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Julliand?= Date: Tue, 8 Oct 2024 18:51:17 +0200 Subject: [PATCH 18/28] Do not use interpolation when not needed Co-authored-by: LJ <3708366+worksofliam@users.noreply.github.com> --- src/api/IBMi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 789074727..e9159b409 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -596,7 +596,7 @@ export default class IBMi { progress.report({ message: `Checking installed components on host IBM i: Java` }); - const javaCheck = async (root: string) => await this.content.testStreamFile(`${root}/bin/java`, 'x') ? `${root}` : undefined; + const javaCheck = async (root: string) => await this.content.testStreamFile(`${root}/bin/java`, 'x') ? root : undefined; this.remoteFeatures.jdk80 = await javaCheck(`/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit`); this.remoteFeatures.jdk11 = await javaCheck(`/QOpenSys/QIBM/ProdData/JavaVM/jdk11/64bit`); this.remoteFeatures.openjdk11 = await javaCheck(`/QOpensys/pkgs/lib/jvm/openjdk-11`); From 32e4566fddf25e4b10273c0b3545e059d3c1646b Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 10 Oct 2024 10:01:21 -0400 Subject: [PATCH 19/28] Bump to 2.13.1 Signed-off-by: worksofliam --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c33095398..ddf92d944 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "icon": "icon.png", "displayName": "Code for IBM i", "description": "Maintain your RPGLE, CL, COBOL, C/CPP on IBM i right from Visual Studio Code.", - "version": "2.13.4-dev.0", + "version": "2.13.1", "keywords": [ "ibmi", "rpgle", From f7da8212168452590d16c325bae2a364d21cddc3 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 10 Oct 2024 10:18:50 -0400 Subject: [PATCH 20/28] Fix type issue in Search Signed-off-by: worksofliam --- src/api/Search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/Search.ts b/src/api/Search.ts index 5189b0991..ce2254c5c 100644 --- a/src/api/Search.ts +++ b/src/api/Search.ts @@ -63,7 +63,7 @@ export namespace Search { } else { // Else, we need to fetch the member info for each hit so we can display the correct extension - const infoComponent = connection?.getComponent(`GetMemberInfo`); + const infoComponent = connection?.getComponent(GetMemberInfo); const memberInfos: IBMiMember[] = hits.map(hit => { const { name, dir } = path.parse(hit.path); const [library, file] = dir.split(`/`); From fea455d5edae6b60b5f265900dfa5c8489417fb2 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Thu, 10 Oct 2024 10:20:56 -0400 Subject: [PATCH 21/28] Bump to 2.13.2 Signed-off-by: worksofliam --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ddf92d944..46078173c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "icon": "icon.png", "displayName": "Code for IBM i", "description": "Maintain your RPGLE, CL, COBOL, C/CPP on IBM i right from Visual Studio Code.", - "version": "2.13.1", + "version": "2.13.2", "keywords": [ "ibmi", "rpgle", From 6c4f0fd9b499e4a4ee7d7d9df2a26c7fed7c2c5f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 10 Oct 2024 14:22:41 +0000 Subject: [PATCH 22/28] Release 2.13.2 --- package-lock.json | 4 ++-- types/package-lock.json | 4 ++-- types/package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 305a1238b..9fd7962fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-for-ibmi", - "version": "2.13.4-dev.0", + "version": "2.13.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "code-for-ibmi", - "version": "2.13.4-dev.0", + "version": "2.13.2", "license": "MIT", "dependencies": { "@bendera/vscode-webview-elements": "^0.12.0", diff --git a/types/package-lock.json b/types/package-lock.json index bb8713f8e..6fe9f0788 100644 --- a/types/package-lock.json +++ b/types/package-lock.json @@ -1,12 +1,12 @@ { "name": "@halcyontech/vscode-ibmi-types", - "version": "2.13.4-dev.0", + "version": "2.13.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@halcyontech/vscode-ibmi-types", - "version": "2.13.4-dev.0", + "version": "2.13.2", "license": "ISC" } } diff --git a/types/package.json b/types/package.json index 1387b1b83..f6d1f35ee 100644 --- a/types/package.json +++ b/types/package.json @@ -1,6 +1,6 @@ { "name": "@halcyontech/vscode-ibmi-types", - "version": "2.13.4-dev.0", + "version": "2.13.2", "description": "Types for vscode-ibmi", "typings": "./typings.d.ts", "scripts": { From 0acd262be78a7b309a5e9bf4b9a0a3817065e4f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 10 Oct 2024 14:22:42 +0000 Subject: [PATCH 23/28] Starting 2.13.3-dev.0 development --- package-lock.json | 4 ++-- package.json | 2 +- types/package-lock.json | 4 ++-- types/package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9fd7962fe..0b7016888 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-for-ibmi", - "version": "2.13.2", + "version": "2.13.3-dev.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "code-for-ibmi", - "version": "2.13.2", + "version": "2.13.3-dev.0", "license": "MIT", "dependencies": { "@bendera/vscode-webview-elements": "^0.12.0", diff --git a/package.json b/package.json index 46078173c..33ed5241f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "icon": "icon.png", "displayName": "Code for IBM i", "description": "Maintain your RPGLE, CL, COBOL, C/CPP on IBM i right from Visual Studio Code.", - "version": "2.13.2", + "version": "2.13.3-dev.0", "keywords": [ "ibmi", "rpgle", diff --git a/types/package-lock.json b/types/package-lock.json index 6fe9f0788..76d3b71cf 100644 --- a/types/package-lock.json +++ b/types/package-lock.json @@ -1,12 +1,12 @@ { "name": "@halcyontech/vscode-ibmi-types", - "version": "2.13.2", + "version": "2.13.3-dev.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@halcyontech/vscode-ibmi-types", - "version": "2.13.2", + "version": "2.13.3-dev.0", "license": "ISC" } } diff --git a/types/package.json b/types/package.json index f6d1f35ee..98131eb54 100644 --- a/types/package.json +++ b/types/package.json @@ -1,6 +1,6 @@ { "name": "@halcyontech/vscode-ibmi-types", - "version": "2.13.2", + "version": "2.13.3-dev.0", "description": "Types for vscode-ibmi", "typings": "./typings.d.ts", "scripts": { From 685141f94a3eb163d307a0bd464d05d2bb01aa2e Mon Sep 17 00:00:00 2001 From: krethan Date: Sat, 12 Oct 2024 21:14:24 -0600 Subject: [PATCH 24/28] Reuse checkObject from content and moved deleteObject into content. Fixed indentation from 4 characters to 2. --- src/api/IBMi.ts | 8 +- src/api/IBMiApps.ts | 136 ++++---- src/api/IBMiContent.ts | 14 +- src/api/IBMiSettings.ts | 720 +++++++++++++++++++--------------------- 4 files changed, 433 insertions(+), 445 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 13ba090b2..71e1948fb 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -325,7 +325,7 @@ export default class IBMi { message: `Checking for bad data areas.` }); - const QCPTOIMPF = await connSettings.checkObjectExists('QSYS', 'QCPTOIMPF', '*DTAARA'); + const QCPTOIMPF = await this.content.checkObject({ library: 'QSYS', name: 'QCPTOIMPF', type: '*DTAARA' }); if (QCPTOIMPF) { vscode.window.showWarningMessage(`The data area QSYS/QCPTOIMPF exists on this system and may impact Code for IBM i functionality.`, { @@ -334,7 +334,7 @@ export default class IBMi { }, `Delete`, `Read more`).then(choice => { switch (choice) { case `Delete`: - connSettings.deleteObject('QSYS', 'QCPTOIMPF', '*DTAARA').then((result) => { + this.content.deleteObject({ library: 'QSYS', name: 'QCPTOIMPF', type: '*DTAARA' }).then((result) => { if (result) { vscode.window.showInformationMessage(`The data area QSYS/QCPTOIMPF has been deleted.`); @@ -353,7 +353,7 @@ export default class IBMi { }); } - const QCPFRMIMPF = await connSettings.checkObjectExists('QSYS', 'QCPFRMIMPF', '*DTAARA'); + const QCPFRMIMPF = await this.content.checkObject({ library: 'QSYS', name: 'QCPFRMIMPF', type: '*DTAARA' }); if (QCPFRMIMPF) { vscode.window.showWarningMessage(`The data area QSYS/QCPFRMIMPF exists on this system and may impact Code for IBM i functionality.`, { @@ -361,7 +361,7 @@ export default class IBMi { }, `Delete`, `Read more`).then(choice => { switch (choice) { case `Delete`: - connSettings.deleteObject('QSYS', 'QCPFRMIMPF', '*DTAARA') + this.content.deleteObject({ library: 'QSYS', name: 'QCPFRMIMPF', type: '*DTAARA' }) .then((result) => { if (result) { vscode.window.showInformationMessage(`The data area QSYS/QCPFRMIMPF has been deleted.`); diff --git a/src/api/IBMiApps.ts b/src/api/IBMiApps.ts index abfc48974..b5527c122 100644 --- a/src/api/IBMiApps.ts +++ b/src/api/IBMiApps.ts @@ -3,85 +3,85 @@ import { RemoteApp, RemoteApps, RemoteFeatures } from "../typings"; export default class IBMiApps { - private remoteApps: RemoteApps; - private remoteFeatures: RemoteFeatures; - - constructor() { - this.remoteApps = [ - { - path: `/usr/bin/`, - names: [`setccsid`, `iconv`, `attr`, `tar`, `ls`] - }, - { - path: `/QOpenSys/pkgs/bin/`, - names: [`git`, `grep`, `tn5250`, `md5sum`, `bash`, `chsh`, `stat`, `sort`, `tar`, `ls`, `find`] - }, - { - path: `/QSYS.LIB/`, - // In the future, we may use a generic specific. - // Right now we only need one program - // specific: `*.PGM`, - specific: `QZDFMDB2.PGM`, - names: [`QZDFMDB2.PGM`] - }, - { - path: `/QIBM/ProdData/IBMiDebugService/bin/`, - specific: `startDebugService.sh`, - names: [`startDebugService.sh`] - } - ]; - - this.remoteFeatures = {}; - this.setRemoteFeatures(); - + private remoteApps: RemoteApps; + private remoteFeatures: RemoteFeatures; + + constructor() { + this.remoteApps = [ + { + path: `/usr/bin/`, + names: [`setccsid`, `iconv`, `attr`, `tar`, `ls`] + }, + { + path: `/QOpenSys/pkgs/bin/`, + names: [`git`, `grep`, `tn5250`, `md5sum`, `bash`, `chsh`, `stat`, `sort`, `tar`, `ls`, `find`] + }, + { + path: `/QSYS.LIB/`, + // In the future, we may use a generic specific. + // Right now we only need one program + // specific: `*.PGM`, + specific: `QZDFMDB2.PGM`, + names: [`QZDFMDB2.PGM`] + }, + { + path: `/QIBM/ProdData/IBMiDebugService/bin/`, + specific: `startDebugService.sh`, + names: [`startDebugService.sh`] + } + ]; + + this.remoteFeatures = {}; + this.initRemoteFeatures(); + + } + + addRemoteApp(remoteApp: RemoteApp) { + + //Add remote App + this.remoteApps.push(remoteApp); + + //Add possible features to list + for (const name of remoteApp.names) { + this.remoteFeatures[name] = undefined; } - addRemoteApp(remoteApp: RemoteApp) { - - //Add remote App - this.remoteApps.push(remoteApp); + } - //Add possible features to list - for(const name of remoteApp.names) { - this.remoteFeatures[name] = undefined; - } + getRemoteApps(): RemoteApps { + return this.remoteApps; + } - } + initRemoteFeatures() { - getRemoteApps(): RemoteApps { - return this.remoteApps; + for (const feature of this.remoteApps) { + for (const name of feature.names) { + this.remoteFeatures[name] = undefined; + } } - setRemoteFeatures() { + } - for (const feature of this.remoteApps) { - for(const name of feature.names) { - this.remoteFeatures[name] = undefined; - } - } - - } + getRemoteFeatures(): RemoteFeatures { + return this.remoteFeatures; + } - getRemoteFeatures(): RemoteFeatures { - return this.remoteFeatures; - } + async checkRemoteFeatures(remoteApp: RemoteApp, connection: IBMi) { - async checkRemoteFeatures(remoteApp: RemoteApp, connection: IBMi) { - - const call = await connection.sendCommand({ command: `ls -p ${remoteApp.path}${remoteApp.specific || ``}` }); - if (call.stdout) { - const files = call.stdout.split(`\n`); - - if (remoteApp.specific) { - for (const name of remoteApp.names) - this.remoteFeatures[name] = files.find(file => file.includes(name)); - } else { - for (const name of remoteApp.names) - if (files.includes(name)) - this.remoteFeatures[name] = remoteApp.path + name; - } - } + const call = await connection.sendCommand({ command: `ls -p ${remoteApp.path}${remoteApp.specific || ``}` }); + if (call.stdout) { + const files = call.stdout.split(`\n`); + if (remoteApp.specific) { + for (const name of remoteApp.names) + this.remoteFeatures[name] = files.find(file => file.includes(name)); + } else { + for (const name of remoteApp.names) + if (files.includes(name)) + this.remoteFeatures[name] = remoteApp.path + name; + } } + } + } \ No newline at end of file diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 0775b561a..1a5a1c096 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -906,6 +906,18 @@ export default class IBMiContent { })).code === 0; } + async deleteObject(object: { library: string, name: string, type: string }) { + + return (await this.ibmi.runCommand({ + command: this.toCl(`DLTOBJ`, { + obj: `${this.ibmi.upperCaseName(object.library)}/${this.ibmi.upperCaseName(object.name)}`, + objtype: object.type.toLocaleUpperCase() + }), + noLibList: true + })).code === 0; + + } + async testStreamFile(path: string, right: "e" | "f" | "d" | "r" | "w" | "x") { return (await this.ibmi.sendCommand({ command: `test -${right} ${Tools.escapePath(path)}` })).code === 0; } @@ -951,7 +963,7 @@ export default class IBMiContent { return cl; } - async getAttributes(path: string | (QsysPath & { member?: string }), ...operands: AttrOperands[]) { + async getAttributes(path: string | (QsysPath & { member?: string }), ...operands: AttrOperands[]) { const target = (path = typeof path === 'string' ? Tools.escapePath(path) : this.ibmi.sysNameInAmerican(Tools.qualifyPath(path.library, path.name, path.member, path.asp))); const result = await this.ibmi.sendCommand({ command: `${this.ibmi.remoteFeatures.attr} -p ${target} ${operands.join(" ")}` }); diff --git a/src/api/IBMiSettings.ts b/src/api/IBMiSettings.ts index c3eb961a9..e0b3755ab 100644 --- a/src/api/IBMiSettings.ts +++ b/src/api/IBMiSettings.ts @@ -7,507 +7,483 @@ const CCSID_SYSVAL = -2; export default class IBMiSettings { - constructor(private connection: IBMi) { + constructor(private connection: IBMi) { - } + } - async checkShellOutput(): Promise { + async checkShellOutput(): Promise { - const checkShellText = `This should be the only text!`; - const checkShellResult = await this.connection.sendCommand({ - command: `echo "${checkShellText}"`, - directory: `.` - }); + const checkShellText = `This should be the only text!`; + const checkShellResult = await this.connection.sendCommand({ + command: `echo "${checkShellText}"`, + directory: `.` + }); - return Promise.resolve(checkShellResult.stdout.split(`\n`)[0] == checkShellText); + return Promise.resolve(checkShellResult.stdout.split(`\n`)[0] == checkShellText); - } + } - async getHomeDirectory(): Promise<{ homeExists: boolean, homeDir: string, homeMsg: string }> { + async getHomeDirectory(): Promise<{ homeExists: boolean, homeDir: string, homeMsg: string }> { - let homeDir; - let homeMsg = ''; - let homeExists; + let homeDir; + let homeMsg = ''; + let homeExists; - const homeResult = await this.connection.sendCommand({ - command: `echo $HOME && cd && test -w $HOME`, - directory: `.` - }); + const homeResult = await this.connection.sendCommand({ + command: `echo $HOME && cd && test -w $HOME`, + directory: `.` + }); - // Note: if the home directory does not exist, the behavior of the echo/cd/test command combo is as follows: - // - stderr contains 'Could not chdir to home directory /home/________: No such file or directory' - // (The output contains 'chdir' regardless of locale and shell, so maybe we could use that - // if we iterate on this code again in the future) - // - stdout contains the name of the home directory (even if it does not exist) - // - The 'cd' command causes an error if the home directory does not exist or otherwise can't be cd'ed into - // - The 'test' command causes an error if the home directory is not writable (one can cd into a non-writable directory) - - homeExists = homeResult.code == 0; - homeDir = homeResult.stdout.trim(); - - if (!homeExists) { - // Let's try to provide more valuable information to the user about why their home directory - // is bad and maybe even provide the opportunity to create the home directory - - // we _could_ just assume the home directory doesn't exist but maybe there's something more going on, namely mucked-up permissions - homeExists = (0 === (await this.connection.sendCommand({ command: `test -e ${homeDir}` })).code); - if (homeExists) { - // Note: this logic might look backward because we fall into this (failure) leg on what looks like success (home dir exists). - // But, remember, but we only got here if 'cd $HOME' failed. - // Let's try to figure out why.... - if (0 !== (await this.connection.sendCommand({ command: `test -d ${homeDir}` })).code) { - homeMsg = `Your home directory (${homeDir}) is not a directory! Code for IBM i may not function correctly. Please contact your system administrator.`; - } - else if (0 !== (await this.connection.sendCommand({ command: `test -w ${homeDir}` })).code) { - homeMsg = `Your home directory (${homeDir}) is not writable! Code for IBM i may not function correctly. Please contact your system administrator.`; - - } - else if (0 !== (await this.connection.sendCommand({ command: `test -x ${homeDir}` })).code) { - homeMsg = `Your home directory (${homeDir}) is not usable due to permissions! Code for IBM i may not function correctly. Please contact your system administrator.`; - } - else { - // not sure, but get your sys admin involved - homeMsg = `Your home directory (${homeDir}) exists but is unusable. Code for IBM i may not function correctly. Please contact your system administrator.`; - } - } - else { - homeMsg = `Your home directory (${homeDir}) does not exist, so Code for IBM i may not function correctly.`; - } - } + // Note: if the home directory does not exist, the behavior of the echo/cd/test command combo is as follows: + // - stderr contains 'Could not chdir to home directory /home/________: No such file or directory' + // (The output contains 'chdir' regardless of locale and shell, so maybe we could use that + // if we iterate on this code again in the future) + // - stdout contains the name of the home directory (even if it does not exist) + // - The 'cd' command causes an error if the home directory does not exist or otherwise can't be cd'ed into + // - The 'test' command causes an error if the home directory is not writable (one can cd into a non-writable directory) + + homeExists = homeResult.code == 0; + homeDir = homeResult.stdout.trim(); + + if (!homeExists) { + // Let's try to provide more valuable information to the user about why their home directory + // is bad and maybe even provide the opportunity to create the home directory - return Promise.resolve({ homeExists, homeDir, homeMsg }); + // we _could_ just assume the home directory doesn't exist but maybe there's something more going on, namely mucked-up permissions + homeExists = (0 === (await this.connection.sendCommand({ command: `test -e ${homeDir}` })).code); + if (homeExists) { + // Note: this logic might look backward because we fall into this (failure) leg on what looks like success (home dir exists). + // But, remember, but we only got here if 'cd $HOME' failed. + // Let's try to figure out why.... + if (0 !== (await this.connection.sendCommand({ command: `test -d ${homeDir}` })).code) { + homeMsg = `Your home directory (${homeDir}) is not a directory! Code for IBM i may not function correctly. Please contact your system administrator.`; + } + else if (0 !== (await this.connection.sendCommand({ command: `test -w ${homeDir}` })).code) { + homeMsg = `Your home directory (${homeDir}) is not writable! Code for IBM i may not function correctly. Please contact your system administrator.`; + } + else if (0 !== (await this.connection.sendCommand({ command: `test -x ${homeDir}` })).code) { + homeMsg = `Your home directory (${homeDir}) is not usable due to permissions! Code for IBM i may not function correctly. Please contact your system administrator.`; + } + else { + // not sure, but get your sys admin involved + homeMsg = `Your home directory (${homeDir}) exists but is unusable. Code for IBM i may not function correctly. Please contact your system administrator.`; + } + } + else { + homeMsg = `Your home directory (${homeDir}) does not exist, so Code for IBM i may not function correctly.`; + } } - async createHomeDirectory(homeDir: string, username: string): Promise<{ homeCreated: boolean, homeMsg: string }> { + return Promise.resolve({ homeExists, homeDir, homeMsg }); - let homeCreated = false; - let homeMsg = ''; + } - const homeCmd = `mkdir -p ${homeDir} && chown ${username.toLowerCase()} ${homeDir} && chmod 0755 ${homeDir}`; + async createHomeDirectory(homeDir: string, username: string): Promise<{ homeCreated: boolean, homeMsg: string }> { - let mkHomeResult = await this.connection.sendCommand({ command: homeCmd, directory: `.` }); + let homeCreated = false; + let homeMsg = ''; - if (0 === mkHomeResult.code) { - homeCreated = true; - } else { - let mkHomeErrs = mkHomeResult.stderr; - // We still get 'Could not chdir to home directory' in stderr so we need to hackily gut that out, as well as the bashisms that are a side effect of our API - homeMsg = mkHomeErrs.substring(1 + mkHomeErrs.indexOf(`\n`)).replace(`bash: line 1: `, ``); - } + const homeCmd = `mkdir -p ${homeDir} && chown ${username.toLowerCase()} ${homeDir} && chmod 0755 ${homeDir}`; - return Promise.resolve({ homeCreated, homeMsg }); + let mkHomeResult = await this.connection.sendCommand({ command: homeCmd, directory: `.` }); + if (0 === mkHomeResult.code) { + homeCreated = true; + } else { + let mkHomeErrs = mkHomeResult.stderr; + // We still get 'Could not chdir to home directory' in stderr so we need to hackily gut that out, as well as the bashisms that are a side effect of our API + homeMsg = mkHomeErrs.substring(1 + mkHomeErrs.indexOf(`\n`)).replace(`bash: line 1: `, ``); } - async getLibraryList(): Promise<{ libStatus: boolean, currentLibrary: string, defaultUserLibraries: string[] }> { - + return Promise.resolve({ homeCreated, homeMsg }); + } - //Since the compiles are stateless, then we have to set the library list each time we use the `SYSTEM` command - //We setup the defaultUserLibraries here so we can remove them later on so the user can setup their own library list + async getLibraryList(): Promise<{ libStatus: boolean, currentLibrary: string, defaultUserLibraries: string[] }> { - let currentLibrary = `QGPL`; - let defaultUserLibraries = []; - let libStatus = false; - const liblResult = await this.connection.sendQsh({ - command: `liblist` - }); - if (liblResult.code === 0) { - libStatus = true; - const libraryListString = liblResult.stdout; - if (libraryListString !== ``) { - const libraryList = libraryListString.split(`\n`); - - let lib, type; - for (const line of libraryList) { - lib = line.substring(0, 10).trim(); - type = line.substring(12); - - switch (type) { - case `USR`: - defaultUserLibraries.push(lib); - break; - - case `CUR`: - currentLibrary = lib; - break; - } - } - } - } + //Since the compiles are stateless, then we have to set the library list each time we use the `SYSTEM` command + //We setup the defaultUserLibraries here so we can remove them later on so the user can setup their own library list - return Promise.resolve({ libStatus, currentLibrary, defaultUserLibraries }); + let currentLibrary = `QGPL`; + let defaultUserLibraries = []; + let libStatus = false; - } + const liblResult = await this.connection.sendQsh({ + command: `liblist` + }); - async setTempLibrary(tempLibrary: string): Promise { + if (liblResult.code === 0) { + libStatus = true; + const libraryListString = liblResult.stdout; + if (libraryListString !== ``) { + const libraryList = libraryListString.split(`\n`); - let tempLibrarySet = false; + let lib, type; + for (const line of libraryList) { + lib = line.substring(0, 10).trim(); + type = line.substring(12); - //Check the temp lib (where temp outfile data lives) exists - const createdTempLib = await this.connection.runCommand({ - command: `CRTLIB LIB(${tempLibrary}) TEXT('Code for i temporary objects. May be cleared.')`, - noLibList: true - }); + switch (type) { + case `USR`: + defaultUserLibraries.push(lib); + break; - if (createdTempLib.code === 0) { - tempLibrarySet = true; + case `CUR`: + currentLibrary = lib; + break; + } } - else { - const messages = Tools.parseMessages(createdTempLib.stderr); - if (messages.findId(`CPF2158`) || messages.findId(`CPF2111`)) { //Already exists, hopefully ok :) - tempLibrarySet = true; - } - else if (messages.findId(`CPD0032`)) { //Can't use CRTLIB - const tempLibExists = await this.connection.runCommand({ - command: `CHKOBJ OBJ(QSYS/${tempLibrary}) OBJTYPE(*LIB)`, - noLibList: true - }); - - if (tempLibExists.code === 0) { - //We're all good if no errors - tempLibrarySet = true; - } - else { - tempLibrarySet = false; - } + } + } - } - } + return Promise.resolve({ libStatus, currentLibrary, defaultUserLibraries }); - return Promise.resolve(tempLibrarySet); + } - } + async setTempLibrary(tempLibrary: string): Promise { - async setTempDirectory(tempDir: string): Promise { + let tempLibrarySet = false; - let tempDirSet = false; + //Check the temp lib (where temp outfile data lives) exists + const createdTempLib = await this.connection.runCommand({ + command: `CRTLIB LIB(${tempLibrary}) TEXT('Code for i temporary objects. May be cleared.')`, + noLibList: true + }); - // Check if the temp directory exists - let result = await this.connection.sendCommand({ - command: `[ -d "${tempDir}" ]` + if (createdTempLib.code === 0) { + tempLibrarySet = true; + } + else { + const messages = Tools.parseMessages(createdTempLib.stderr); + if (messages.findId(`CPF2158`) || messages.findId(`CPF2111`)) { //Already exists, hopefully ok :) + tempLibrarySet = true; + } + else if (messages.findId(`CPD0032`)) { //Can't use CRTLIB + const tempLibExists = await this.connection.runCommand({ + command: `CHKOBJ OBJ(QSYS/${tempLibrary}) OBJTYPE(*LIB)`, + noLibList: true }); - if (result.code === 0) { - // Directory exists - tempDirSet = true; - } else { - // Directory does not exist, try to create it - let result = await this.connection.sendCommand({ - command: `mkdir -p ${tempDir}` - }); - if (result.code === 0) { - // Directory created - tempDirSet = true; - } else { - // Directory not created - } + if (tempLibExists.code === 0) { + //We're all good if no errors + tempLibrarySet = true; + } + else { + tempLibrarySet = false; } - return Promise.resolve(tempDirSet); + } + } + return Promise.resolve(tempLibrarySet); + + } + + async setTempDirectory(tempDir: string): Promise { + + let tempDirSet = false; + + // Check if the temp directory exists + let result = await this.connection.sendCommand({ + command: `[ -d "${tempDir}" ]` + }); + + if (result.code === 0) { + // Directory exists + tempDirSet = true; + } else { + // Directory does not exist, try to create it + let result = await this.connection.sendCommand({ + command: `mkdir -p ${tempDir}` + }); + if (result.code === 0) { + // Directory created + tempDirSet = true; + } else { + // Directory not created + } } - async clearTempLibrary(tempLibrary: string): Promise { + return Promise.resolve(tempDirSet); - let clearMsg = ''; + } - this.connection.runCommand({ - command: `DLTOBJ OBJ(${tempLibrary}/O_*) OBJTYPE(*FILE)`, - noLibList: true, - }) - .then(result => { - // All good! - if (result && result.stderr) { - const messages = Tools.parseMessages(result.stderr); - if (!messages.findId(`CPF2125`)) { - clearMsg = `Temporary data not cleared from ${tempLibrary}.`; - } - } - }); + async clearTempLibrary(tempLibrary: string): Promise { - return Promise.resolve(clearMsg); + let clearMsg = ''; - } + this.connection.runCommand({ + command: `DLTOBJ OBJ(${tempLibrary}/O_*) OBJTYPE(*FILE)`, + noLibList: true, + }) + .then(result => { + // All good! + if (result && result.stderr) { + const messages = Tools.parseMessages(result.stderr); + if (!messages.findId(`CPF2125`)) { + clearMsg = `Temporary data not cleared from ${tempLibrary}.`; + } + } + }); - async clearTempDirectory(tempDir: string): Promise { + return Promise.resolve(clearMsg); - let clearMsg = ''; + } - this.connection.sendCommand({ - command: `rm -rf ${path.posix.join(tempDir, `vscodetemp*`)}` - }) - .then(result => { - // All good! - }) - .catch(e => { - // CPF2125: No objects deleted. - clearMsg = `Temporary data not cleared from ${tempDir}.`; - }); + async clearTempDirectory(tempDir: string): Promise { - return Promise.resolve(clearMsg); + let clearMsg = ''; + try { + this.connection.sendCommand({ + command: `rm -rf ${path.posix.join(tempDir, `vscodetemp*`)}` + }); + } + catch (e) { + // CPF2125: No objects deleted. + clearMsg = `Temporary data not cleared from ${tempDir}.`; } - async checkObjectExists(library: string, object: string, type: string): Promise { - - const objResult = await this.connection.runCommand({ - command: `CHKOBJ OBJ(${library}/${object}) OBJTYPE(${type})`, - noLibList: true - }); + return Promise.resolve(clearMsg); - return Promise.resolve(objResult.code === 0); + } - } + async getASPInfo(): Promise { - async deleteObject(library: string, object: string, type: string): Promise { + let aspInfo: aspInfo = {}; - const deleteResult = await this.connection.runCommand({ - command: `DLTOBJ OBJ(${library}/${object}) OBJTYPE(${type})`, - noLibList: true + try { + const resultSet = await this.connection.runSQL(`SELECT * FROM QSYS2.ASP_INFO`); + if (resultSet.length) { + resultSet.forEach(row => { + if (row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME !== `null`) { + aspInfo[Number(row.ASP_NUMBER)] = String(row.DEVICE_DESCRIPTION_NAME); + } }); - - return Promise.resolve(deleteResult.code === 0); - + } + } catch (e) { + //Oh well + return Promise.reject(e); } - async getASPInfo(): Promise { + return Promise.resolve(aspInfo); - let aspInfo: aspInfo = {}; + } - try { - const resultSet = await this.connection.runSQL(`SELECT * FROM QSYS2.ASP_INFO`); - if (resultSet.length) { - resultSet.forEach(row => { - if (row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME !== `null`) { - aspInfo[Number(row.ASP_NUMBER)] = String(row.DEVICE_DESCRIPTION_NAME); - } - }); - } - } catch (e) { - //Oh well - return Promise.reject(e); - } + async getQCCSID(): Promise { - return Promise.resolve(aspInfo); + let qccsid = 0; + const [systemCCSID] = await this.connection.runSQL(`select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`); + if (typeof systemCCSID.CURRENT_NUMERIC_VALUE === 'number') { + qccsid = systemCCSID.CURRENT_NUMERIC_VALUE; } - async getQCCSID(): Promise { + return Promise.resolve(qccsid); - let qccsid = 0; + } - const [systemCCSID] = await this.connection.runSQL(`select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`); - if (typeof systemCCSID.CURRENT_NUMERIC_VALUE === 'number') { - qccsid = systemCCSID.CURRENT_NUMERIC_VALUE; - } + async getjobCCSID(userName: string): Promise { - return Promise.resolve(qccsid); + let jobCCSID = CCSID_SYSVAL; + const [userInfo] = await this.connection.runSQL(`select CHARACTER_CODE_SET_ID from table( QSYS2.QSYUSRINFO( USERNAME => upper('${userName}') ) )`); + if (userInfo.CHARACTER_CODE_SET_ID !== `null` && typeof userInfo.CHARACTER_CODE_SET_ID === 'number') { + jobCCSID = userInfo.CHARACTER_CODE_SET_ID; } - async getjobCCSID(userName: string): Promise { + return Promise.resolve(jobCCSID); - let jobCCSID = CCSID_SYSVAL; + } - const [userInfo] = await this.connection.runSQL(`select CHARACTER_CODE_SET_ID from table( QSYS2.QSYUSRINFO( USERNAME => upper('${userName}') ) )`); - if (userInfo.CHARACTER_CODE_SET_ID !== `null` && typeof userInfo.CHARACTER_CODE_SET_ID === 'number') { - jobCCSID = userInfo.CHARACTER_CODE_SET_ID; - } + async getDefaultCCSID(): Promise { - return Promise.resolve(jobCCSID); + let userDefaultCCSID = 0; + try { + const [activeJob] = await this.connection.runSQL(`Select DEFAULT_CCSID From Table(QSYS2.ACTIVE_JOB_INFO( JOB_NAME_FILTER => '*', DETAILED_INFO => 'ALL' ))`); + userDefaultCCSID = Number(activeJob.DEFAULT_CCSID); + } + catch (error) { + const [defaultCCSID] = (await this.connection.runCommand({ command: "DSPJOB OPTION(*DFNA)" })) + .stdout + .split("\n") + .filter(line => line.includes("DFTCCSID")); + + const defaultCCSCID = Number(defaultCCSID.split("DFTCCSID").at(1)?.trim()); + if (defaultCCSCID && !isNaN(defaultCCSCID)) { + userDefaultCCSID = defaultCCSCID; + } } - async getDefaultCCSID(): Promise { + return Promise.resolve(userDefaultCCSID); - let userDefaultCCSID = 0; + } - try { - const [activeJob] = await this.connection.runSQL(`Select DEFAULT_CCSID From Table(QSYS2.ACTIVE_JOB_INFO( JOB_NAME_FILTER => '*', DETAILED_INFO => 'ALL' ))`); - userDefaultCCSID = Number(activeJob.DEFAULT_CCSID); - } - catch (error) { - const [defaultCCSID] = (await this.connection.runCommand({ command: "DSPJOB OPTION(*DFNA)" })) - .stdout - .split("\n") - .filter(line => line.includes("DFTCCSID")); - - const defaultCCSCID = Number(defaultCCSID.split("DFTCCSID").at(1)?.trim()); - if (defaultCCSCID && !isNaN(defaultCCSCID)) { - userDefaultCCSID = defaultCCSCID; - } - } + async getLocalEncodingValues(): Promise { - return Promise.resolve(userDefaultCCSID); + let localEncoding = ''; + const [variants] = await this.connection.runSQL(`With VARIANTS ( HASH, AT, DOLLARSIGN ) as (` + + ` values ( cast( x'7B' as varchar(1) )` + + ` , cast( x'7C' as varchar(1) )` + + ` , cast( x'5B' as varchar(1) ) )` + + `)` + + `Select HASH concat AT concat DOLLARSIGN as LOCAL from VARIANTS`); + + if (typeof variants.LOCAL === 'string' && variants.LOCAL !== `null`) { + localEncoding = variants.LOCAL; } - async getLocalEncodingValues(): Promise { + return Promise.resolve(localEncoding); - let localEncoding = ''; + } - const [variants] = await this.connection.runSQL(`With VARIANTS ( HASH, AT, DOLLARSIGN ) as (` - + ` values ( cast( x'7B' as varchar(1) )` - + ` , cast( x'7C' as varchar(1) )` - + ` , cast( x'5B' as varchar(1) ) )` - + `)` - + `Select HASH concat AT concat DOLLARSIGN as LOCAL from VARIANTS`); + async setBash(): Promise { - if (typeof variants.LOCAL === 'string' && variants.LOCAL !== `null`) { - localEncoding = variants.LOCAL; - } + let bashset = false; - return Promise.resolve(localEncoding); + const commandSetBashResult = await this.connection.sendCommand({ + command: `/QOpenSys/pkgs/bin/chsh -s /QOpenSys/pkgs/bin/bash` + }); - } + if (!commandSetBashResult.stderr) bashset = true; - async setBash(): Promise { + return Promise.resolve(bashset); - let bashset = false; + } - const commandSetBashResult = await this.connection.sendCommand({ - command: `/QOpenSys/pkgs/bin/chsh -s /QOpenSys/pkgs/bin/bash` - }); + async getEnvironmentVariable(envVar: string): Promise { + return (await this.connection.sendCommand({ command: `echo ${envVar}` })).stdout.split(":"); + } - if (!commandSetBashResult.stderr) bashset = true; + async checkPaths(requiredPaths: string[]): Promise<{ reason: string, missingPath: string }> { - return Promise.resolve(bashset); + const currentPaths = await this.getEnvironmentVariable('$PATH'); - } + let reason = ''; + let missingPath = ''; - async getEnvironmentVariable(envVar: string): Promise { - return (await this.connection.sendCommand({ command: `echo ${envVar}` })).stdout.split(":"); + for (const requiredPath of requiredPaths) { + if (!currentPaths.includes(requiredPath)) { + reason = `Your $PATH shell environment variable does not include ${requiredPath}`; + missingPath = requiredPath + break; + } + } + // If reason is still undefined, then we know the user has all the required paths. Then we don't + // need to check for their existence before checking the order of the required paths. + + if (!reason && + (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/usr/bin") + || (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/QOpenSys/usr/bin")))) { + reason = "/QOpenSys/pkgs/bin is not in the right position in your $PATH shell environment variable"; + missingPath = "/QOpenSys/pkgs/bin" } - async checkPaths(requiredPaths: string[]): Promise<{ reason: string, missingPath: string }> { + return Promise.resolve({ reason: reason, missingPath: missingPath }); - const currentPaths = await this.getEnvironmentVariable('$PATH'); + } - let reason = ''; - let missingPath = ''; + async checkBashRCFile(bashrcFile: string): Promise { - for (const requiredPath of requiredPaths) { - if (!currentPaths.includes(requiredPath)) { - reason = `Your $PATH shell environment variable does not include ${requiredPath}`; - missingPath = requiredPath - break; - } - } - // If reason is still undefined, then we know the user has all the required paths. Then we don't - // need to check for their existence before checking the order of the required paths. - - if (!reason && - (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/usr/bin") - || (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/QOpenSys/usr/bin")))) { - reason = "/QOpenSys/pkgs/bin is not in the right position in your $PATH shell environment variable"; - missingPath = "/QOpenSys/pkgs/bin" - } + let bashrcExists = false; - return Promise.resolve({ reason: reason, missingPath: missingPath }); + bashrcExists = (await this.connection.sendCommand({ command: `test -e ${bashrcFile}` })).code === 0; - } + return Promise.resolve(bashrcExists); + } - async checkBashRCFile(bashrcFile: string): Promise { + async createBashrcFile(bashrcFile: string, username: string): Promise<{ createBash: boolean, createBashMsg: string }> { - let bashrcExists = false; + let createBash = true; + let createBashMsg = ''; - bashrcExists = (await this.connection.sendCommand({ command: `test -e ${bashrcFile}` })).code === 0; + // Add "/usr/bin" and "/QOpenSys/usr/bin" to the end of the path. This way we know that the user has + // all the required paths, but we don't overwrite the priority of other items on their path. + const createBashrc = await this.connection.sendCommand({ command: `echo "# Generated by Code for IBM i\nexport PATH=/QOpenSys/pkgs/bin:\\$PATH:/QOpenSys/usr/bin:/usr/bin" >> ${bashrcFile} && chown ${username.toLowerCase()} ${bashrcFile} && chmod 755 ${bashrcFile}` }); - return Promise.resolve(bashrcExists); + if (createBashrc.code !== 0) { + createBash = false; + createBashMsg = createBashrc.stderr; } - async createBashrcFile(bashrcFile: string, username: string): Promise<{ createBash: boolean, createBashMsg: string }> { + return Promise.resolve({ createBash, createBashMsg }); - let createBash = true; - let createBashMsg = ''; + } - // Add "/usr/bin" and "/QOpenSys/usr/bin" to the end of the path. This way we know that the user has - // all the required paths, but we don't overwrite the priority of other items on their path. - const createBashrc = await this.connection.sendCommand({ command: `echo "# Generated by Code for IBM i\nexport PATH=/QOpenSys/pkgs/bin:\\$PATH:/QOpenSys/usr/bin:/usr/bin" >> ${bashrcFile} && chown ${username.toLowerCase()} ${bashrcFile} && chmod 755 ${bashrcFile}` }); + async updateBashrcFile(bashrcFile: string): Promise<{ updateBash: boolean, updateBashMsg: string }> { - if (createBashrc.code !== 0) { - createBash = false; - createBashMsg = createBashrc.stderr; - } + let updateBash = true; + let updateBashMsg = ''; - return Promise.resolve({ createBash, createBashMsg }); - - } - - async updateBashrcFile(bashrcFile: string): Promise<{ updateBash: boolean, updateBashMsg: string }> { - - let updateBash = true; - let updateBashMsg = ''; - - try { - const content = this.connection.content; - if (content) { - const bashrcContent = (await content.downloadStreamfile(bashrcFile)).split("\n"); - let replaced = false; - bashrcContent.forEach((line, index) => { - if (!replaced) { - const pathRegex = /^((?:export )?PATH=)(.*)(?:)$/.exec(line); - if (pathRegex) { - bashrcContent[index] = `${pathRegex[1]}/QOpenSys/pkgs/bin:${pathRegex[2] - .replace("/QOpenSys/pkgs/bin", "") //Removes /QOpenSys/pkgs/bin wherever it is - .replace("::", ":")}:/QOpenSys/usr/bin:/usr/bin`; //Removes double : in case /QOpenSys/pkgs/bin wasn't at the end - replaced = true; - } - } - }); - - if (!replaced) { - bashrcContent.push( - "", - "# Generated by Code for IBM i", - "export PATH=/QOpenSys/pkgs/bin:$PATH:/QOpenSys/usr/bin:/usr/bin" - ); - } - - await content.writeStreamfile(bashrcFile, bashrcContent.join("\n")); + try { + const content = this.connection.content; + if (content) { + const bashrcContent = (await content.downloadStreamfile(bashrcFile)).split("\n"); + let replaced = false; + bashrcContent.forEach((line, index) => { + if (!replaced) { + const pathRegex = /^((?:export )?PATH=)(.*)(?:)$/.exec(line); + if (pathRegex) { + bashrcContent[index] = `${pathRegex[1]}/QOpenSys/pkgs/bin:${pathRegex[2] + .replace("/QOpenSys/pkgs/bin", "") //Removes /QOpenSys/pkgs/bin wherever it is + .replace("::", ":")}:/QOpenSys/usr/bin:/usr/bin`; //Removes double : in case /QOpenSys/pkgs/bin wasn't at the end + replaced = true; } - } - catch (error) { - updateBash = false; - updateBashMsg = error; + } + }); + + if (!replaced) { + bashrcContent.push( + "", + "# Generated by Code for IBM i", + "export PATH=/QOpenSys/pkgs/bin:$PATH:/QOpenSys/usr/bin:/usr/bin" + ); } - return Promise.resolve({ updateBash, updateBashMsg }); + await content.writeStreamfile(bashrcFile, bashrcContent.join("\n")); + } + } + catch (error) { + updateBash = false; + updateBashMsg = error; } - async validateLibraryList(defaultUserLibraries: string[], libraryList: string[]): Promise<{ validLibs: string[], badLibs: string[] }> { + return Promise.resolve({ updateBash, updateBashMsg }); + } - let validLibs: string[] = []; - let badLibs: string[] = []; + async validateLibraryList(defaultUserLibraries: string[], libraryList: string[]): Promise<{ validLibs: string[], badLibs: string[] }> { - const result = await this.connection.sendQsh({ - command: [ - `liblist -d ` + defaultUserLibraries.join(` `).replace(/\$/g, `\\$`), - ...libraryList.map(lib => `liblist -a ` + lib.replace(/\$/g, `\\$`)) - ].join(`; `) - }); + let validLibs: string[] = []; + let badLibs: string[] = []; - if (result.stderr) { - const lines = result.stderr.split(`\n`); + const result = await this.connection.sendQsh({ + command: [ + `liblist -d ` + defaultUserLibraries.join(` `).replace(/\$/g, `\\$`), + ...libraryList.map(lib => `liblist -a ` + lib.replace(/\$/g, `\\$`)) + ].join(`; `) + }); - lines.forEach(line => { - const badLib = libraryList.find(lib => line.includes(`ibrary ${lib} `)); + if (result.stderr) { + const lines = result.stderr.split(`\n`); - // If there is an error about the library, store it - if (badLib) badLibs.push(badLib); - }); - } + lines.forEach(line => { + const badLib = libraryList.find(lib => line.includes(`ibrary ${lib} `)); - if (result && badLibs.length > 0) { - validLibs = libraryList.filter(lib => !badLibs.includes(lib)); - } - - return Promise.resolve({ validLibs, badLibs }); + // If there is an error about the library, store it + if (badLib) badLibs.push(badLib); + }); + } + if (result && badLibs.length > 0) { + validLibs = libraryList.filter(lib => !badLibs.includes(lib)); } + return Promise.resolve({ validLibs, badLibs }); + + } } \ No newline at end of file From 58bb714e795942cd27124e3127deefe0258ab0e8 Mon Sep 17 00:00:00 2001 From: krethan Date: Thu, 26 Sep 2024 23:15:13 -0600 Subject: [PATCH 25/28] Seperating logic from IBMi.ts into a new class. --- src/api/IBMi.ts | 566 ++++++++++------------------------------ src/api/IBMiApps.ts | 87 ++++++ src/api/IBMiSettings.ts | 515 ++++++++++++++++++++++++++++++++++++ src/typings.ts | 21 +- 4 files changed, 758 insertions(+), 431 deletions(-) create mode 100644 src/api/IBMiApps.ts create mode 100644 src/api/IBMiSettings.ts diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index f8d1d0fab..a39860ef5 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -8,7 +8,7 @@ import { IBMiComponent, IBMiComponentType } from "../components/component"; import { CopyToImport } from "../components/copyToImport"; import { ComponentManager } from "../components/manager"; import { instance } from "../instantiate"; -import { CommandData, CommandResult, ConnectionData, IBMiMember, RemoteCommand, SpecialAuthorities, WrapResult } from "../typings"; +import { CommandData, CommandResult, ConnectionData, MemberParts, RemoteCommand, RemoteFeatures, SpecialAuthorities, WrapResult } from "../typings"; import { CompileTools } from "./CompileTools"; import { ConnectionConfiguration } from "./Configuration"; import IBMiContent from "./IBMiContent"; @@ -17,38 +17,12 @@ import { Tools } from './Tools'; import * as configVars from './configVars'; import { DebugConfiguration } from "./debug/config"; import { debugPTFInstalled } from "./debug/server"; - -export interface MemberParts extends IBMiMember { - basename: string -} +import IBMiSettings from "./IBMiSettings"; +import IBMiApps from "./IBMiApps"; const CCSID_SYSVAL = -2; const bashShellPath = '/QOpenSys/pkgs/bin/bash'; -const remoteApps = [ // All names MUST also be defined as key in 'remoteFeatures' below!! - { - path: `/usr/bin/`, - names: [`setccsid`, `iconv`, `attr`, `tar`, `ls`] - }, - { - path: `/QOpenSys/pkgs/bin/`, - names: [`git`, `grep`, `tn5250`, `md5sum`, `bash`, `chsh`, `stat`, `sort`, `tar`, `ls`, `find`] - }, - { - path: `/QSYS.LIB/`, - // In the future, we may use a generic specific. - // Right now we only need one program - // specific: `*.PGM`, - specific: `QZDFMDB2.PGM`, - names: [`QZDFMDB2.PGM`] - }, - { - path: `/QIBM/ProdData/IBMiDebugService/bin/`, - specific: `startDebugService.sh`, - names: [`startDebugService.sh`] - } -]; - export default class IBMi { private qccsid: number = 65535; private jobCcsid: number = CCSID_SYSVAL; @@ -72,7 +46,7 @@ export default class IBMi { * the root of the IFS, thus why we store it. */ aspInfo: { [id: number]: string } = {}; - remoteFeatures: { [name: string]: string | undefined }; + remoteFeatures: RemoteFeatures; variantChars: { american: string, local: string }; /** @@ -94,30 +68,7 @@ export default class IBMi { constructor() { this.client = new node_ssh.NodeSSH; - this.remoteFeatures = { - git: undefined, - grep: undefined, - tn5250: undefined, - setccsid: undefined, - md5sum: undefined, - bash: undefined, - chsh: undefined, - stat: undefined, - sort: undefined, - 'GETNEWLIBL.PGM': undefined, - 'GETMBRINFO.SQL': undefined, - 'QZDFMDB2.PGM': undefined, - 'startDebugService.sh': undefined, - attr: undefined, - iconv: undefined, - tar: undefined, - ls: undefined, - find: undefined, - jdk80: undefined, - jdk11: undefined, - jdk17: undefined, - openjdk11: undefined - }; + this.remoteFeatures = {}; this.variantChars = { american: `#@$`, @@ -195,17 +146,17 @@ export default class IBMi { // Reload server settings? const quickConnect = (this.config.quickConnect === true && reloadServerSettings === false); + //Initialize IBMiConnectionSettings to be used throughout to get settings + let connSettings = new IBMiSettings(this); + // Check shell output for additional user text - this will confuse Code... progress.report({ message: `Checking shell output.` }); - const checkShellText = `This should be the only text!`; - const checkShellResult = await this.sendCommand({ - command: `echo "${checkShellText}"`, - directory: `.` - }); - if (checkShellResult.stdout.split(`\n`)[0] !== checkShellText) { + const checkShellResult = await connSettings.checkShellOutput(); + + if (!checkShellResult) { const chosen = await vscode.window.showErrorMessage(`Error in shell configuration!`, { detail: [ `This extension can not work with the shell configured on ${this.currentConnectionName},`, @@ -237,74 +188,37 @@ export default class IBMi { message: `Checking home directory.` }); - let defaultHomeDir; - - const echoHomeResult = await this.sendCommand({ - command: `echo $HOME && cd && test -w $HOME`, - directory: `.` - }); - // Note: if the home directory does not exist, the behavior of the echo/cd/test command combo is as follows: - // - stderr contains 'Could not chdir to home directory /home/________: No such file or directory' - // (The output contains 'chdir' regardless of locale and shell, so maybe we could use that - // if we iterate on this code again in the future) - // - stdout contains the name of the home directory (even if it does not exist) - // - The 'cd' command causes an error if the home directory does not exist or otherwise can't be cd'ed into - // - The 'test' command causes an error if the home directory is not writable (one can cd into a non-writable directory) - let isHomeUsable = (0 == echoHomeResult.code); - if (isHomeUsable) { - defaultHomeDir = echoHomeResult.stdout.trim(); - } else { - // Let's try to provide more valuable information to the user about why their home directory - // is bad and maybe even provide the opportunity to create the home directory - - let actualHomeDir = echoHomeResult.stdout.trim(); - - // we _could_ just assume the home directory doesn't exist but maybe there's something more going on, namely mucked-up permissions - let doesHomeExist = (0 === (await this.sendCommand({ command: `test -e ${actualHomeDir}` })).code); - if (doesHomeExist) { - // Note: this logic might look backward because we fall into this (failure) leg on what looks like success (home dir exists). - // But, remember, but we only got here if 'cd $HOME' failed. - // Let's try to figure out why.... - if (0 !== (await this.sendCommand({ command: `test -d ${actualHomeDir}` })).code) { - await vscode.window.showWarningMessage(`Your home directory (${actualHomeDir}) is not a directory! Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: !reconnecting }); - } - else if (0 !== (await this.sendCommand({ command: `test -w ${actualHomeDir}` })).code) { - await vscode.window.showWarningMessage(`Your home directory (${actualHomeDir}) is not writable! Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: !reconnecting }); - } - else if (0 !== (await this.sendCommand({ command: `test -x ${actualHomeDir}` })).code) { - await vscode.window.showWarningMessage(`Your home directory (${actualHomeDir}) is not usable due to permissions! Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: !reconnecting }); + const homeResult = await connSettings.getHomeDirectory(); + if (homeResult.homeMsg) { + if (homeResult.homeExists) { + //Home Directory exists but give informational message + await vscode.window.showWarningMessage(homeResult.homeMsg, { modal: !reconnecting }); + } + else { + //Home Directory does not exist + if (reconnecting) { + vscode.window.showWarningMessage(homeResult.homeMsg, { modal: false }); } else { - // not sure, but get your sys admin involved - await vscode.window.showWarningMessage(`Your home directory (${actualHomeDir}) exists but is unusable. Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: !reconnecting }); - } - } - else if (reconnecting) { - vscode.window.showWarningMessage(`Your home directory (${actualHomeDir}) does not exist. Code for IBM i may not function correctly.`, { modal: false }); - } - else if (await vscode.window.showWarningMessage(`Home directory does not exist`, { - modal: true, - detail: `Your home directory (${actualHomeDir}) does not exist, so Code for IBM i may not function correctly. Would you like to create this directory now?`, - }, `Yes`)) { - this.appendOutput(`creating home directory ${actualHomeDir}`); - let mkHomeCmd = `mkdir -p ${actualHomeDir} && chown ${connectionObject.username.toLowerCase()} ${actualHomeDir} && chmod 0755 ${actualHomeDir}`; - let mkHomeResult = await this.sendCommand({ command: mkHomeCmd, directory: `.` }); - if (0 === mkHomeResult.code) { - defaultHomeDir = actualHomeDir; - } else { - let mkHomeErrs = mkHomeResult.stderr; - // We still get 'Could not chdir to home directory' in stderr so we need to hackily gut that out, as well as the bashisms that are a side effect of our API - mkHomeErrs = mkHomeErrs.substring(1 + mkHomeErrs.indexOf(`\n`)).replace(`bash: line 1: `, ``); - await vscode.window.showWarningMessage(`Error creating home directory (${actualHomeDir}):\n${mkHomeErrs}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); + if (await vscode.window.showWarningMessage(`Home directory does not exist`, { + modal: true, + detail: `Your home directory (${homeResult.homeDir}) does not exist, so Code for IBM i may not function correctly. Would you like to create this directory now?`, + }, `Yes`)) { + this.appendOutput(`creating home directory ${homeResult.homeDir}`); + let homeCreatedResult = await connSettings.createHomeDirectory(homeResult.homeDir, connectionObject.username); + if (!homeCreatedResult.homeCreated) { + await vscode.window.showWarningMessage(homeCreatedResult.homeMsg, { modal: true }); + } + } } } } // Check to see if we need to store a new value for the home directory - if (defaultHomeDir) { - if (this.config.homeDirectory !== defaultHomeDir) { - this.config.homeDirectory = defaultHomeDir; - vscode.window.showInformationMessage(`Configured home directory reset to ${defaultHomeDir}.`); + if (homeResult.homeDir) { + if (this.config.homeDirectory !== homeResult.homeDir) { + this.config.homeDirectory = homeResult.homeDir; + vscode.window.showInformationMessage(`Configured home directory reset to ${homeResult.homeDir}.`); } } else { // New connections always have `.` as the initial value. @@ -315,7 +229,7 @@ export default class IBMi { //Set a default IFS listing if (this.config.ifsShortcuts.length === 0) { - if (defaultHomeDir) { + if (homeResult.homeDir) { this.config.ifsShortcuts = [this.config.homeDirectory]; } else { this.config.ifsShortcuts = [`/`]; @@ -326,42 +240,19 @@ export default class IBMi { message: `Checking library list configuration.` }); - //Since the compiles are stateless, then we have to set the library list each time we use the `SYSTEM` command - //We setup the defaultUserLibraries here so we can remove them later on so the user can setup their own library list - let currentLibrary = `QGPL`; this.defaultUserLibraries = []; - const liblResult = await this.sendQsh({ - command: `liblist` - }); - if (liblResult.code === 0) { - const libraryListString = liblResult.stdout; - if (libraryListString !== ``) { - const libraryList = libraryListString.split(`\n`); - - let lib, type; - for (const line of libraryList) { - lib = line.substring(0, 10).trim(); - type = line.substring(12); - - switch (type) { - case `USR`: - this.defaultUserLibraries.push(lib); - break; + let libraryListResult = await connSettings.getLibraryList(); + if (libraryListResult.libStatus) { + + this.defaultUserLibraries = libraryListResult.defaultUserLibraries; - case `CUR`: - currentLibrary = lib; - break; - } - } - - //If this is the first time the config is made, then these arrays will be empty - if (this.config.currentLibrary.length === 0) { - this.config.currentLibrary = currentLibrary; - } - if (this.config.libraryList.length === 0) { - this.config.libraryList = this.defaultUserLibraries; - } + //If this is the first time the config is made, then these arrays will be empty + if (this.config.currentLibrary.length === 0) { + this.config.currentLibrary = libraryListResult.currentLibrary; + } + if (this.config.libraryList.length === 0) { + this.config.libraryList = libraryListResult.defaultUserLibraries; } } @@ -370,60 +261,20 @@ export default class IBMi { }); //Next, we need to check the temp lib (where temp outfile data lives) exists - const createdTempLib = await this.runCommand({ - command: `CRTLIB LIB(${this.config.tempLibrary}) TEXT('Code for i temporary objects. May be cleared.')`, - noLibList: true - }); - - if (createdTempLib.code === 0) { - tempLibrarySet = true; - } else { - const messages = Tools.parseMessages(createdTempLib.stderr); - if (messages.findId(`CPF2158`) || messages.findId(`CPF2111`)) { //Already exists, hopefully ok :) + tempLibrarySet = await connSettings.setTempLibrary(this.config.tempLibrary); + if (!tempLibrarySet) { + if (libraryListResult.currentLibrary && !libraryListResult.currentLibrary.startsWith(`Q`)) { + //Using ${currentLibrary} as the temporary library for temporary data. + this.config.tempLibrary = this.config.currentLibrary; tempLibrarySet = true; } - else if (messages.findId(`CPD0032`)) { //Can't use CRTLIB - const tempLibExists = await this.runCommand({ - command: `CHKOBJ OBJ(QSYS/${this.config.tempLibrary}) OBJTYPE(*LIB)`, - noLibList: true - }); - - if (tempLibExists.code === 0) { - //We're all good if no errors - tempLibrarySet = true; - } else if (currentLibrary && !currentLibrary.startsWith(`Q`)) { - //Using ${currentLibrary} as the temporary library for temporary data. - this.config.tempLibrary = currentLibrary; - tempLibrarySet = true; - } - } } progress.report({ message: `Checking temporary directory configuration.` }); - let tempDirSet = false; - // Next, we need to check if the temp directory exists - let result = await this.sendCommand({ - command: `[ -d "${this.config.tempDir}" ]` - }); - - if (result.code === 0) { - // Directory exists - tempDirSet = true; - } else { - // Directory does not exist, try to create it - let result = await this.sendCommand({ - command: `mkdir -p ${this.config.tempDir}` - }); - if (result.code === 0) { - // Directory created - tempDirSet = true; - } else { - // Directory not created - } - } + let tempDirSet = await connSettings.setTempDirectory(this.config.tempDir); if (!tempDirSet) { this.config.tempDir = `/tmp`; @@ -433,47 +284,38 @@ export default class IBMi { progress.report({ message: `Clearing temporary data.` }); - - this.runCommand({ - command: `DLTOBJ OBJ(${this.config.tempLibrary}/O_*) OBJTYPE(*FILE)`, - noLibList: true, - }) - .then(result => { - // All good! - if (result && result.stderr) { - const messages = Tools.parseMessages(result.stderr); - if (!messages.findId(`CPF2125`)) { - // @ts-ignore We know the config exists. - vscode.window.showErrorMessage(`Temporary data not cleared from ${this.config.tempLibrary}.`, `View log`).then(async choice => { - if (choice === `View log`) { - this.outputChannel!.show(); - } - }); + //Clear Temporary Library Data + let clearMsg = await connSettings.clearTempLibrary(this.config.tempLibrary); + if (clearMsg) { + // @ts-ignore We know the config exists. + vscode.window.showErrorMessage(clearMsg, `View log`).then + (async choice => { + if (choice === `View log`) { + this.outputChannel!.show(); } - } - }) - - this.sendCommand({ - command: `rm -rf ${path.posix.join(this.config.tempDir, `vscodetemp*`)}` - }) - .then(result => { - // All good! - }) - .catch(e => { - // CPF2125: No objects deleted. - // @ts-ignore We know the config exists. - vscode.window.showErrorMessage(`Temporary data not cleared from ${this.config.tempDir}.`, `View log`).then(async choice => { + }); + } + + //Clear Temporary Directory Data + clearMsg = await connSettings.clearTempDirectory(this.config.tempDir); + if (clearMsg) { + // @ts-ignore We know the config exists. + vscode.window.showErrorMessage(clearMsg, `View log`).then + (async choice => { if (choice === `View log`) { this.outputChannel!.show(); } }); - }); + } + } + //TO DO: why is this required???? const commandShellResult = await this.sendCommand({ command: `echo $SHELL` }); + //TO DO: why is this required???? if (commandShellResult.code === 0) { this.shell = commandShellResult.stdout.trim(); } @@ -486,29 +328,26 @@ export default class IBMi { message: `Checking for bad data areas.` }); - const QCPTOIMPF = await this.runCommand({ - command: `CHKOBJ OBJ(QSYS/QCPTOIMPF) OBJTYPE(*DTAARA)`, - noLibList: true - }); + const QCPTOIMPF = await connSettings.checkObjectExists('QSYS', 'QCPTOIMPF', '*DTAARA'); - if (QCPTOIMPF?.code === 0) { + if (QCPTOIMPF) { vscode.window.showWarningMessage(`The data area QSYS/QCPTOIMPF exists on this system and may impact Code for IBM i functionality.`, { detail: `For V5R3, the code for the command CPYTOIMPF had a major design change to increase functionality and performance. The QSYS/QCPTOIMPF data area lets developers keep the pre-V5R2 version of CPYTOIMPF. Code for IBM i cannot function correctly while this data area exists.`, modal: true, }, `Delete`, `Read more`).then(choice => { switch (choice) { case `Delete`: - this.runCommand({ - command: `DLTOBJ OBJ(QSYS/QCPTOIMPF) OBJTYPE(*DTAARA)`, - noLibList: true - }) - .then((result) => { - if (result?.code === 0) { - vscode.window.showInformationMessage(`The data area QSYS/QCPTOIMPF has been deleted.`); - } else { - vscode.window.showInformationMessage(`Failed to delete the data area QSYS/QCPTOIMPF. Code for IBM i may not work as intended.`); - } - }) + connSettings.deleteObject('QSYS', 'QCPTOIMPF', '*DTAARA').then((result) => { + if (result) { + vscode.window.showInformationMessage(`The data + area QSYS/QCPTOIMPF has been deleted.`); + } + else { + vscode.window.showInformationMessage(`Failed to + delete the data area QSYS/QCPTOIMPF. Code for IBM + i may not work as intended.`); + } + }); break; case `Read more`: vscode.env.openExternal(vscode.Uri.parse(`https://github.com/codefori/vscode-ibmi/issues/476#issuecomment-1018908018`)); @@ -517,23 +356,17 @@ export default class IBMi { }); } - const QCPFRMIMPF = await this.runCommand({ - command: `CHKOBJ OBJ(QSYS/QCPFRMIMPF) OBJTYPE(*DTAARA)`, - noLibList: true - }); + const QCPFRMIMPF = await connSettings.checkObjectExists('QSYS', 'QCPFRMIMPF', '*DTAARA'); - if (QCPFRMIMPF?.code === 0) { + if (QCPFRMIMPF) { vscode.window.showWarningMessage(`The data area QSYS/QCPFRMIMPF exists on this system and may impact Code for IBM i functionality.`, { modal: false, }, `Delete`, `Read more`).then(choice => { switch (choice) { case `Delete`: - this.runCommand({ - command: `DLTOBJ OBJ(QSYS/QCPFRMIMPF) OBJTYPE(*DTAARA)`, - noLibList: true - }) + connSettings.deleteObject('QSYS', 'QCPFRMIMPF', '*DTAARA') .then((result) => { - if (result?.code === 0) { + if (result) { vscode.window.showInformationMessage(`The data area QSYS/QCPFRMIMPF has been deleted.`); } else { vscode.window.showInformationMessage(`Failed to delete the data area QSYS/QCPFRMIMPF. Code for IBM i may not work as intended.`); @@ -557,8 +390,10 @@ export default class IBMi { message: `Checking installed components on host IBM i.` }); + let remoteApps = new IBMiApps(); + // We need to check if our remote programs are installed. - remoteApps.push( + remoteApps.addRemoteApp( { path: `/QSYS.lib/${this.upperCaseName(this.config.tempLibrary)}.lib/`, names: [`GETNEWLIBL.PGM`], @@ -568,59 +403,23 @@ export default class IBMi { //Next, we see what pase features are available (installed via yum) //This may enable certain features in the future. - for (const feature of remoteApps) { + for (const remoteApp of remoteApps.getRemoteApps()) { try { progress.report({ - message: `Checking installed components on host IBM i: ${feature.path}` + message: `Checking installed components on host IBM i: ${remoteApp.path}` }); - const call = await this.sendCommand({ command: `ls -p ${feature.path}${feature.specific || ``}` }); - if (call.stdout) { - const files = call.stdout.split(`\n`); - - if (feature.specific) { - for (const name of feature.names) - this.remoteFeatures[name] = files.find(file => file.includes(name)); - } else { - for (const name of feature.names) - if (files.includes(name)) - this.remoteFeatures[name] = feature.path + name; - } - } + await remoteApps.checkRemoteFeatures(remoteApp, this); + this.remoteFeatures = remoteApps.getRemoteFeatures(); + } catch (e) { console.log(e); } } - - //Specific Java installations check - progress.report({ - message: `Checking installed components on host IBM i: Java` - }); - const javaCheck = async (root: string) => await this.content.testStreamFile(`${root}/bin/java`, 'x') ? root : undefined; - this.remoteFeatures.jdk80 = await javaCheck(`/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit`); - this.remoteFeatures.jdk11 = await javaCheck(`/QOpenSys/QIBM/ProdData/JavaVM/jdk11/64bit`); - this.remoteFeatures.openjdk11 = await javaCheck(`/QOpensys/pkgs/lib/jvm/openjdk-11`); - this.remoteFeatures.jdk17 = await javaCheck(`/QOpenSys/QIBM/ProdData/JavaVM/jdk17/64bit`); } if (this.sqlRunnerAvailable()) { - //Temporary function to run SQL - - // TODO: stop using this runSQL function and this.runSql - const runSQL = async (statement: string) => { - const output = await this.sendCommand({ - command: `LC_ALL=EN_US.UTF-8 system "call QSYS/QZDFMDB2 PARM('-d' '-i')"`, - stdin: statement - }); - - if (output.code === 0) { - return Tools.db2Parse(output.stdout); - } - else { - throw new Error(output.stdout); - } - }; - + // Check for ASP information? if (quickConnect === true && cachedServerSettings?.aspInfo) { this.aspInfo = cachedServerSettings.aspInfo; @@ -631,19 +430,14 @@ 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 runSQL(`SELECT * FROM QSYS2.ASP_INFO`); - resultSet.forEach(row => { - if (row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME !== `null`) { - this.aspInfo[Number(row.ASP_NUMBER)] = String(row.DEVICE_DESCRIPTION_NAME); - } - }); - } catch (e) { - //Oh well + + this.aspInfo = await connSettings.getASPInfo(); + if (Object.keys(this.aspInfo).length === 0) { progress.report({ message: `Failed to get ASP information.` }); } + } // Fetch conversion values? @@ -660,18 +454,11 @@ export default class IBMi { // Next, we're going to see if we can get the CCSID from the user or the system. // Some things don't work without it!!! try { - // we need to grab the system CCSID (QCCSID) - const [systemCCSID] = await runSQL(`select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`); - if (typeof systemCCSID.CURRENT_NUMERIC_VALUE === 'number') { - this.qccsid = systemCCSID.CURRENT_NUMERIC_VALUE; - } + this.qccsid = await connSettings.getQCCSID(); // we grab the users default CCSID - const [userInfo] = await runSQL(`select CHARACTER_CODE_SET_ID from table( QSYS2.QSYUSRINFO( USERNAME => upper('${this.currentUser}') ) )`); - if (userInfo.CHARACTER_CODE_SET_ID !== `null` && typeof userInfo.CHARACTER_CODE_SET_ID === 'number') { - this.jobCcsid = userInfo.CHARACTER_CODE_SET_ID; - } + this.jobCcsid = await connSettings.getjobCCSID(this.currentUser); // if the job ccsid is *SYSVAL, then assign it to sysval if (this.jobCcsid === CCSID_SYSVAL) { @@ -679,36 +466,14 @@ export default class IBMi { } // Let's also get the user's default CCSID - try { - const [activeJob] = await runSQL(`Select DEFAULT_CCSID From Table(QSYS2.ACTIVE_JOB_INFO( JOB_NAME_FILTER => '*', DETAILED_INFO => 'ALL' ))`); - this.userDefaultCCSID = Number(activeJob.DEFAULT_CCSID); - } - catch (error) { - const [defaultCCSID] = (await this.runCommand({ command: "DSPJOB OPTION(*DFNA)" })) - .stdout - .split("\n") - .filter(line => line.includes("DFTCCSID")); - - const defaultCCSCID = Number(defaultCCSID.split("DFTCCSID").at(1)?.trim()); - if (defaultCCSCID && !isNaN(defaultCCSCID)) { - this.userDefaultCCSID = defaultCCSCID; - } - } + this.userDefaultCCSID = await connSettings.getDefaultCCSID(); progress.report({ message: `Fetching local encoding values.` }); - const [variants] = await runSQL(`With VARIANTS ( HASH, AT, DOLLARSIGN ) as (` - + ` values ( cast( x'7B' as varchar(1) )` - + ` , cast( x'7C' as varchar(1) )` - + ` , cast( x'5B' as varchar(1) ) )` - + `)` - + `Select HASH concat AT concat DOLLARSIGN as LOCAL from VARIANTS`); + this.variantChars.local = await connSettings.getLocalEncodingValues(); - if (typeof variants.LOCAL === 'string' && variants.LOCAL !== `null`) { - this.variantChars.local = variants.LOCAL; - } } catch (e) { // Oh well! console.log(e); @@ -751,11 +516,10 @@ export default class IBMi { vscode.window.showInformationMessage(`IBM recommends using bash as your default shell.`, `Set shell to bash`, `Read More`,).then(async choice => { switch (choice) { case `Set shell to bash`: - const commandSetBashResult = await this.sendCommand({ - command: `/QOpenSys/pkgs/bin/chsh -s /QOpenSys/pkgs/bin/bash` - }); - if (!commandSetBashResult.stderr) { + const commandSetBashResult = await connSettings.setBash(); + + if (!commandSetBashResult) { vscode.window.showInformationMessage(`Shell is now bash! Reconnect for change to take effect.`); usesBash = true; } else { @@ -778,72 +542,32 @@ export default class IBMi { }); if ((!quickConnect || !cachedServerSettings?.pathChecked)) { - const currentPaths = (await this.sendCommand({ command: "echo $PATH" })).stdout.split(":"); - const bashrcFile = `${defaultHomeDir}/.bashrc`; - let bashrcExists = (await this.sendCommand({ command: `test -e ${bashrcFile}` })).code === 0; - let reason; - const requiredPaths = ["/QOpenSys/pkgs/bin", "/usr/bin", "/QOpenSys/usr/bin"] - let missingPath; - for (const requiredPath of requiredPaths) { - if (!currentPaths.includes(requiredPath)) { - reason = `Your $PATH shell environment variable does not include ${requiredPath}`; - missingPath = requiredPath - break; - } - } - // If reason is still undefined, then we know the user has all the required paths. Then we don't - // need to check for their existence before checking the order of the required paths. - if (!reason && - (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/usr/bin") - || (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/QOpenSys/usr/bin")))) { - reason = "/QOpenSys/pkgs/bin is not in the right position in your $PATH shell environment variable"; - missingPath = "/QOpenSys/pkgs/bin" - } - if (reason && await vscode.window.showWarningMessage(`${missingPath} not found in $PATH`, { + + const bashrcFile = `${homeResult.homeDir}/.bashrc`; + + let bashrcExists = await connSettings.checkBashRCFile(bashrcFile); + + let checkPathResult = await connSettings.checkPaths(["/QOpenSys/pkgs/bin", "/usr/bin", "/QOpenSys/usr/bin"]); + + if (checkPathResult.reason && await vscode.window.showWarningMessage(`${checkPathResult.missingPath} not found in $PATH`, { modal: true, - detail: `${reason}, so Code for IBM i may not function correctly. Would you like to ${bashrcExists ? "update" : "create"} ${bashrcFile} to fix this now?`, + detail: `${checkPathResult.reason}, so Code for IBM i may not function correctly. Would you like to ${bashrcExists ? "update" : "create"} ${bashrcFile} to fix this now?`, }, `Yes`)) { delayedOperations.push(async () => { this.appendOutput(`${bashrcExists ? "update" : "create"} ${bashrcFile}`); if (!bashrcExists) { - // Add "/usr/bin" and "/QOpenSys/usr/bin" to the end of the path. This way we know that the user has - // all the required paths, but we don't overwrite the priority of other items on their path. - const createBashrc = await this.sendCommand({ command: `echo "# Generated by Code for IBM i\nexport PATH=/QOpenSys/pkgs/bin:\\$PATH:/QOpenSys/usr/bin:/usr/bin" >> ${bashrcFile} && chown ${connectionObject.username.toLowerCase()} ${bashrcFile} && chmod 755 ${bashrcFile}` }); - if (createBashrc.code !== 0) { - vscode.window.showWarningMessage(`Error creating ${bashrcFile}):\n${createBashrc.stderr}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); + //Create bashrc File + let createBashResult = await connSettings.createBashrcFile(bashrcFile,connectionObject.username); + //Error creating bashrc File + if (!createBashResult.createBash) { + vscode.window.showWarningMessage(`Error creating ${bashrcFile}):\n${createBashResult.createBashMsg}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); } } else { - try { - const content = this.content; - if (content) { - const bashrcContent = (await content.downloadStreamfile(bashrcFile)).split("\n"); - let replaced = false; - bashrcContent.forEach((line, index) => { - if (!replaced) { - const pathRegex = /^((?:export )?PATH=)(.*)(?:)$/.exec(line); - if (pathRegex) { - bashrcContent[index] = `${pathRegex[1]}/QOpenSys/pkgs/bin:${pathRegex[2] - .replace("/QOpenSys/pkgs/bin", "") //Removes /QOpenSys/pkgs/bin wherever it is - .replace("::", ":")}:/QOpenSys/usr/bin:/usr/bin`; //Removes double : in case /QOpenSys/pkgs/bin wasn't at the end - replaced = true; - } - } - }); - - if (!replaced) { - bashrcContent.push( - "", - "# Generated by Code for IBM i", - "export PATH=/QOpenSys/pkgs/bin:$PATH:/QOpenSys/usr/bin:/usr/bin" - ); - } - - await content.writeStreamfile(bashrcFile, bashrcContent.join("\n")); - } - } - catch (error) { - vscode.window.showWarningMessage(`Error modifying PATH in ${bashrcFile}):\n${error}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); + //Update bashRC file + let updateBashResult = await connSettings.updateBashrcFile(bashrcFile); + if(!updateBashResult.updateBash) { + vscode.window.showWarningMessage(`Error modifying PATH in ${bashrcFile}):\n${updateBashResult.updateBashMsg}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); } } }); @@ -864,7 +588,7 @@ export default class IBMi { } } - if (defaultHomeDir) { + if (homeResult.homeDir) { if (!tempLibrarySet) { vscode.window.showWarningMessage(`Code for IBM i will not function correctly until the temporary library has been corrected in the settings.`, `Open Settings`) .then(result => { @@ -887,34 +611,14 @@ export default class IBMi { progress.report({ message: `Validate configured library list` }); - let validLibs: string[] = []; - let badLibs: string[] = []; - - const result = await this.sendQsh({ - command: [ - `liblist -d ` + this.defaultUserLibraries.join(` `).replace(/\$/g, `\\$`), - ...this.config.libraryList.map(lib => `liblist -a ` + lib.replace(/\$/g, `\\$`)) - ].join(`; `) - }); - if (result.stderr) { - const lines = result.stderr.split(`\n`); - - lines.forEach(line => { - const badLib = this.config?.libraryList.find(lib => line.includes(`ibrary ${lib} `)); - - // If there is an error about the library, store it - if (badLib) badLibs.push(badLib); - }); - } - - if (result && badLibs.length > 0) { - validLibs = this.config.libraryList.filter(lib => !badLibs.includes(lib)); - const chosen = await vscode.window.showWarningMessage(`The following ${badLibs.length > 1 ? `libraries` : `library`} does not exist: ${badLibs.join(`,`)}. Remove ${badLibs.length > 1 ? `them` : `it`} from the library list?`, `Yes`, `No`); + let libraryListResult = await connSettings.validateLibraryList(this.defaultUserLibraries,this.config.libraryList); + if(libraryListResult.badLibs.length > 0) { + const chosen = await vscode.window.showWarningMessage(`The following ${libraryListResult.badLibs.length > 1 ? `libraries` : `library`} does not exist: ${libraryListResult.badLibs.join(`,`)}. Remove ${libraryListResult.badLibs.length > 1 ? `them` : `it`} from the library list?`, `Yes`, `No`); if (chosen === `Yes`) { - this.config!.libraryList = validLibs; + this.config!.libraryList = libraryListResult.validLibs; } else { - vscode.window.showWarningMessage(`The following libraries does not exist: ${badLibs.join(`,`)}.`); + vscode.window.showWarningMessage(`The following libraries does not exist: ${libraryListResult.badLibs.join(`,`)}.`); } } } @@ -1379,6 +1083,8 @@ export default class IBMi { * @param statements * @returns a Result set */ + + // TODO: stop using this.runSql async runSQL(statements: string): Promise { const { 'QZDFMDB2.PGM': QZDFMDB2 } = this.remoteFeatures; diff --git a/src/api/IBMiApps.ts b/src/api/IBMiApps.ts new file mode 100644 index 000000000..abfc48974 --- /dev/null +++ b/src/api/IBMiApps.ts @@ -0,0 +1,87 @@ +import IBMi from "./IBMi"; +import { RemoteApp, RemoteApps, RemoteFeatures } from "../typings"; + +export default class IBMiApps { + + private remoteApps: RemoteApps; + private remoteFeatures: RemoteFeatures; + + constructor() { + this.remoteApps = [ + { + path: `/usr/bin/`, + names: [`setccsid`, `iconv`, `attr`, `tar`, `ls`] + }, + { + path: `/QOpenSys/pkgs/bin/`, + names: [`git`, `grep`, `tn5250`, `md5sum`, `bash`, `chsh`, `stat`, `sort`, `tar`, `ls`, `find`] + }, + { + path: `/QSYS.LIB/`, + // In the future, we may use a generic specific. + // Right now we only need one program + // specific: `*.PGM`, + specific: `QZDFMDB2.PGM`, + names: [`QZDFMDB2.PGM`] + }, + { + path: `/QIBM/ProdData/IBMiDebugService/bin/`, + specific: `startDebugService.sh`, + names: [`startDebugService.sh`] + } + ]; + + this.remoteFeatures = {}; + this.setRemoteFeatures(); + + } + + addRemoteApp(remoteApp: RemoteApp) { + + //Add remote App + this.remoteApps.push(remoteApp); + + //Add possible features to list + for(const name of remoteApp.names) { + this.remoteFeatures[name] = undefined; + } + + } + + getRemoteApps(): RemoteApps { + return this.remoteApps; + } + + setRemoteFeatures() { + + for (const feature of this.remoteApps) { + for(const name of feature.names) { + this.remoteFeatures[name] = undefined; + } + } + + } + + getRemoteFeatures(): RemoteFeatures { + return this.remoteFeatures; + } + + async checkRemoteFeatures(remoteApp: RemoteApp, connection: IBMi) { + + const call = await connection.sendCommand({ command: `ls -p ${remoteApp.path}${remoteApp.specific || ``}` }); + if (call.stdout) { + const files = call.stdout.split(`\n`); + + if (remoteApp.specific) { + for (const name of remoteApp.names) + this.remoteFeatures[name] = files.find(file => file.includes(name)); + } else { + for (const name of remoteApp.names) + if (files.includes(name)) + this.remoteFeatures[name] = remoteApp.path + name; + } + } + + } + +} \ No newline at end of file diff --git a/src/api/IBMiSettings.ts b/src/api/IBMiSettings.ts new file mode 100644 index 000000000..ad5d07869 --- /dev/null +++ b/src/api/IBMiSettings.ts @@ -0,0 +1,515 @@ +import IBMi from "./IBMi"; +import { Tools } from "./Tools"; +import path from 'path'; +import { aspInfo } from "../typings"; + +const CCSID_SYSVAL = -2; + +export default class IBMiSettings { + + constructor(private connection: IBMi) { + + } + + async checkShellOutput(): Promise { + + const checkShellText = `This should be the only text!`; + const checkShellResult = await this.connection.sendCommand({ + command: `echo "${checkShellText}"`, + directory: `.` + }); + + return Promise.resolve(checkShellResult.stdout.split(`\n`)[0] == checkShellText); + + } + + async getHomeDirectory(): Promise<{ homeExists: boolean, homeDir: string, homeMsg: string }> { + + let homeDir; + let homeMsg = ''; + let homeExists; + + const homeResult = await this.connection.sendCommand({ + command: `echo $HOME && cd && test -w $HOME`, + directory: `.` + }); + + // Note: if the home directory does not exist, the behavior of the echo/cd/test command combo is as follows: + // - stderr contains 'Could not chdir to home directory /home/________: No such file or directory' + // (The output contains 'chdir' regardless of locale and shell, so maybe we could use that + // if we iterate on this code again in the future) + // - stdout contains the name of the home directory (even if it does not exist) + // - The 'cd' command causes an error if the home directory does not exist or otherwise can't be cd'ed into + // - The 'test' command causes an error if the home directory is not writable (one can cd into a non-writable directory) + + homeExists = homeResult.code == 0; + homeDir = homeResult.stdout.trim(); + + if (!homeExists) { + // Let's try to provide more valuable information to the user about why their home directory + // is bad and maybe even provide the opportunity to create the home directory + + // we _could_ just assume the home directory doesn't exist but maybe there's something more going on, namely mucked-up permissions + homeExists = (0 === (await this.connection.sendCommand({ command: `test -e ${homeDir}` })).code); + if (homeExists) { + // Note: this logic might look backward because we fall into this (failure) leg on what looks like success (home dir exists). + // But, remember, but we only got here if 'cd $HOME' failed. + // Let's try to figure out why.... + if (0 !== (await this.connection.sendCommand({ command: `test -d ${homeDir}` })).code) { + homeMsg = `Your home directory (${homeDir}) is not a directory! Code for IBM i may not function correctly. Please contact your system administrator.`; + } + else if (0 !== (await this.connection.sendCommand({ command: `test -w ${homeDir}` })).code) { + homeMsg = `Your home directory (${homeDir}) is not writable! Code for IBM i may not function correctly. Please contact your system administrator.`; + + } + else if (0 !== (await this.connection.sendCommand({ command: `test -x ${homeDir}` })).code) { + homeMsg = `Your home directory (${homeDir}) is not usable due to permissions! Code for IBM i may not function correctly. Please contact your system administrator.`; + } + else { + // not sure, but get your sys admin involved + homeMsg = `Your home directory (${homeDir}) exists but is unusable. Code for IBM i may not function correctly. Please contact your system administrator.`; + } + } + else { + homeMsg = `Your home directory (${homeDir}) does not exist, so Code for IBM i may not function correctly.`; + } + } + + return Promise.resolve({ homeExists, homeDir, homeMsg }); + + } + + async createHomeDirectory(homeDir: string, username: string): Promise<{ homeCreated: boolean, homeMsg: string }> { + + let homeCreated = false; + let homeMsg = ''; + + const homeCmd = `mkdir -p ${homeDir} && chown ${username.toLowerCase()} ${homeDir} && chmod 0755 ${homeDir}`; + + let mkHomeResult = await this.connection.sendCommand({ command: homeCmd, directory: `.` }); + + if (0 === mkHomeResult.code) { + homeCreated = true; + } else { + let mkHomeErrs = mkHomeResult.stderr; + // We still get 'Could not chdir to home directory' in stderr so we need to hackily gut that out, as well as the bashisms that are a side effect of our API + homeMsg = mkHomeErrs.substring(1 + mkHomeErrs.indexOf(`\n`)).replace(`bash: line 1: `, ``); + } + + return Promise.resolve({ homeCreated, homeMsg }); + + } + + async getLibraryList(): Promise<{ libStatus: boolean, currentLibrary: string, defaultUserLibraries: string[] }> { + + + + //Since the compiles are stateless, then we have to set the library list each time we use the `SYSTEM` command + //We setup the defaultUserLibraries here so we can remove them later on so the user can setup their own library list + + let currentLibrary = `QGPL`; + let defaultUserLibraries = []; + let libStatus = false; + + const liblResult = await this.connection.sendQsh({ + command: `liblist` + }); + + if (liblResult.code === 0) { + libStatus = true; + const libraryListString = liblResult.stdout; + if (libraryListString !== ``) { + const libraryList = libraryListString.split(`\n`); + + let lib, type; + for (const line of libraryList) { + lib = line.substring(0, 10).trim(); + type = line.substring(12); + + switch (type) { + case `USR`: + defaultUserLibraries.push(lib); + break; + + case `CUR`: + currentLibrary = lib; + break; + } + } + } + } + + return Promise.resolve({ libStatus, currentLibrary, defaultUserLibraries }); + + } + + async setTempLibrary(tempLibrary: string): Promise { + + let tempLibrarySet = false; + + //Check the temp lib (where temp outfile data lives) exists + const createdTempLib = await this.connection.runCommand({ + command: `CRTLIB LIB(${tempLibrary}) TEXT('Code for i temporary objects. May be cleared.')`, + noLibList: true + }); + + if (createdTempLib.code === 0) { + tempLibrarySet = true; + } + else { + const messages = Tools.parseMessages(createdTempLib.stderr); + if (messages.findId(`CPF2158`) || messages.findId(`CPF2111`)) { //Already exists, hopefully ok :) + tempLibrarySet = true; + } + else if (messages.findId(`CPD0032`)) { //Can't use CRTLIB + const tempLibExists = await this.connection.runCommand({ + command: `CHKOBJ OBJ(QSYS/${tempLibrary}) OBJTYPE(*LIB)`, + noLibList: true + }); + + if (tempLibExists.code === 0) { + //We're all good if no errors + tempLibrarySet = true; + } + else { + tempLibrarySet = false; + } + + } + } + + return Promise.resolve(tempLibrarySet); + + } + + async setTempDirectory(tempDir: string): Promise { + + let tempDirSet = false; + + // Check if the temp directory exists + let result = await this.connection.sendCommand({ + command: `[ -d "${tempDir}" ]` + }); + + if (result.code === 0) { + // Directory exists + tempDirSet = true; + } else { + // Directory does not exist, try to create it + let result = await this.connection.sendCommand({ + command: `mkdir -p ${tempDir}` + }); + if (result.code === 0) { + // Directory created + tempDirSet = true; + } else { + // Directory not created + } + } + + return Promise.resolve(tempDirSet); + + } + + async clearTempLibrary(tempLibrary: string): Promise { + + let clearMsg = ''; + + this.connection.runCommand({ + command: `DLTOBJ OBJ(${tempLibrary}/O_*) OBJTYPE(*FILE)`, + noLibList: true, + }) + .then(result => { + // All good! + if (result && result.stderr) { + const messages = Tools.parseMessages(result.stderr); + if (!messages.findId(`CPF2125`)) { + clearMsg = `Temporary data not cleared from ${tempLibrary}.`; + } + } + }); + + return Promise.resolve(clearMsg); + + } + + async clearTempDirectory(tempDir: string): Promise { + + let clearMsg = ''; + + this.connection.sendCommand({ + command: `rm -rf ${path.posix.join(tempDir, `vscodetemp*`)}` + }) + .then(result => { + // All good! + }) + .catch(e => { + // CPF2125: No objects deleted. + clearMsg = `Temporary data not cleared from ${tempDir}.`; + }); + + return Promise.resolve(clearMsg); + + } + + async checkObjectExists(library: string, object: string, type: string): Promise { + + const objResult = await this.connection.runCommand({ + command: `CHKOBJ OBJ(${library}/${object}) OBJTYPE(${type})`, + noLibList: true + }); + + return Promise.resolve(objResult.code === 0); + + } + + async deleteObject(library: string, object: string, type: string): Promise { + + const deleteResult = await this.connection.runCommand({ + command: `DLTOBJ OBJ(${library}/${object}) OBJTYPE(${type})`, + noLibList: true + }); + + return Promise.resolve(deleteResult.code === 0); + + } + + async getASPInfo(): Promise { + + let aspInfo: aspInfo = {}; + + try { + const resultSet = await this.connection.runSQL(`SELECT * FROM QSYS2.ASP_INFO`); + if (resultSet.length) { + resultSet.forEach(row => { + if (row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME !== `null`) { + aspInfo[Number(row.ASP_NUMBER)] = String(row.DEVICE_DESCRIPTION_NAME); + } + }); + } + } catch (e) { + //Oh well + aspInfo = {}; + } + + return Promise.resolve(aspInfo); + + } + + async getQCCSID(): Promise { + + let qccsid = 0; + + const [systemCCSID] = await this.connection.runSQL(`select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`); + if (typeof systemCCSID.CURRENT_NUMERIC_VALUE === 'number') { + qccsid = systemCCSID.CURRENT_NUMERIC_VALUE; + } + + return Promise.resolve(qccsid); + + } + + async getjobCCSID(userName: string): Promise { + + let jobCCSID = CCSID_SYSVAL; + + const [userInfo] = await this.connection.runSQL(`select CHARACTER_CODE_SET_ID from table( QSYS2.QSYUSRINFO( USERNAME => upper('${userName}') ) )`); + if (userInfo.CHARACTER_CODE_SET_ID !== `null` && typeof userInfo.CHARACTER_CODE_SET_ID === 'number') { + jobCCSID = userInfo.CHARACTER_CODE_SET_ID; + } + + return Promise.resolve(jobCCSID); + + } + + async getDefaultCCSID(): Promise { + + let userDefaultCCSID = 0; + + try { + const [activeJob] = await this.connection.runSQL(`Select DEFAULT_CCSID From Table(QSYS2.ACTIVE_JOB_INFO( JOB_NAME_FILTER => '*', DETAILED_INFO => 'ALL' ))`); + userDefaultCCSID = Number(activeJob.DEFAULT_CCSID); + } + catch (error) { + const [defaultCCSID] = (await this.connection.runCommand({ command: "DSPJOB OPTION(*DFNA)" })) + .stdout + .split("\n") + .filter(line => line.includes("DFTCCSID")); + + const defaultCCSCID = Number(defaultCCSID.split("DFTCCSID").at(1)?.trim()); + if (defaultCCSCID && !isNaN(defaultCCSCID)) { + userDefaultCCSID = defaultCCSCID; + } + } + + return Promise.resolve(userDefaultCCSID); + + } + + async getLocalEncodingValues(): Promise { + + let localEncoding = ''; + + const [variants] = await this.connection.runSQL(`With VARIANTS ( HASH, AT, DOLLARSIGN ) as (` + + ` values ( cast( x'7B' as varchar(1) )` + + ` , cast( x'7C' as varchar(1) )` + + ` , cast( x'5B' as varchar(1) ) )` + + `)` + + `Select HASH concat AT concat DOLLARSIGN as LOCAL from VARIANTS`); + + if (typeof variants.LOCAL === 'string' && variants.LOCAL !== `null`) { + localEncoding = variants.LOCAL; + } + + return Promise.resolve(localEncoding); + + } + + async setBash(): Promise { + + let bashset = false; + + const commandSetBashResult = await this.connection.sendCommand({ + command: `/QOpenSys/pkgs/bin/chsh -s /QOpenSys/pkgs/bin/bash` + }); + + if (!commandSetBashResult.stderr) bashset = true; + + return Promise.resolve(bashset); + + } + + async getEnvironmentVariable(envVar: string): Promise { + return (await this.connection.sendCommand({ command: `echo ${envVar}` })).stdout.split(":"); + } + + async checkPaths(requiredPaths: string[]): Promise<{ reason: string, missingPath: string }> { + + const currentPaths = await this.getEnvironmentVariable('$PATH'); + + let reason = ''; + let missingPath = ''; + + for (const requiredPath of requiredPaths) { + if (!currentPaths.includes(requiredPath)) { + reason = `Your $PATH shell environment variable does not include ${requiredPath}`; + missingPath = requiredPath + break; + } + } + // If reason is still undefined, then we know the user has all the required paths. Then we don't + // need to check for their existence before checking the order of the required paths. + if (!reason) { + for (let x = 1; x <= requiredPaths.length; x++) { + if (currentPaths.indexOf(requiredPaths[0]) > currentPaths.indexOf(requiredPaths[x])) { + reason = `${requiredPaths[0]} is not in the right position in your $PATH shell environment variable`; + missingPath = requiredPaths[0]; + break; + } + } + } + + return Promise.resolve({ reason: reason, missingPath: missingPath }); + + } + + async checkBashRCFile(bashrcFile: string): Promise { + + let bashrcExists = false; + + bashrcExists = (await this.connection.sendCommand({ command: `test -e ${bashrcFile}` })).code === 0; + + return Promise.resolve(bashrcExists); + } + + async createBashrcFile(bashrcFile: string, username: string): Promise<{ createBash: boolean, createBashMsg: string }> { + + let createBash = true; + let createBashMsg = ''; + + // Add "/usr/bin" and "/QOpenSys/usr/bin" to the end of the path. This way we know that the user has + // all the required paths, but we don't overwrite the priority of other items on their path. + const createBashrc = await this.connection.sendCommand({ command: `echo "# Generated by Code for IBM i\nexport PATH=/QOpenSys/pkgs/bin:\\$PATH:/QOpenSys/usr/bin:/usr/bin" >> ${bashrcFile} && chown ${username.toLowerCase()} ${bashrcFile} && chmod 755 ${bashrcFile}` }); + + if (createBashrc.code !== 0) { + createBash = false; + createBashMsg = createBashrc.stderr; + } + + return Promise.resolve({ createBash, createBashMsg }); + + } + + async updateBashrcFile(bashrcFile: string): Promise<{ updateBash: boolean, updateBashMsg: string }> { + + let updateBash = true; + let updateBashMsg = ''; + + try { + const content = this.connection.content; + if (content) { + const bashrcContent = (await content.downloadStreamfile(bashrcFile)).split("\n"); + let replaced = false; + bashrcContent.forEach((line, index) => { + if (!replaced) { + const pathRegex = /^((?:export )?PATH=)(.*)(?:)$/.exec(line); + if (pathRegex) { + bashrcContent[index] = `${pathRegex[1]}/QOpenSys/pkgs/bin:${pathRegex[2] + .replace("/QOpenSys/pkgs/bin", "") //Removes /QOpenSys/pkgs/bin wherever it is + .replace("::", ":")}:/QOpenSys/usr/bin:/usr/bin`; //Removes double : in case /QOpenSys/pkgs/bin wasn't at the end + replaced = true; + } + } + }); + + if (!replaced) { + bashrcContent.push( + "", + "# Generated by Code for IBM i", + "export PATH=/QOpenSys/pkgs/bin:$PATH:/QOpenSys/usr/bin:/usr/bin" + ); + } + + await content.writeStreamfile(bashrcFile, bashrcContent.join("\n")); + } + } + catch (error) { + updateBash = false; + updateBashMsg = error; + } + + return Promise.resolve({ updateBash, updateBashMsg }); + } + + async validateLibraryList(defaultUserLibraries: string[], libraryList: string[]): Promise<{validLibs: string[], badLibs: string[]}> { + + let validLibs: string[] = []; + let badLibs: string[] = []; + + const result = await this.connection.sendQsh({ + command: [ + `liblist -d ` + defaultUserLibraries.join(` `).replace(/\$/g, `\\$`), + ...libraryList.map(lib => `liblist -a ` + lib.replace(/\$/g, `\\$`)) + ].join(`; `) + }); + + if (result.stderr) { + const lines = result.stderr.split(`\n`); + + lines.forEach(line => { + const badLib = libraryList.find(lib => line.includes(`ibrary ${lib} `)); + + // If there is an error about the library, store it + if (badLib) badLibs.push(badLib); + }); + } + + if (result && badLibs.length > 0) { + validLibs = libraryList.filter(lib => !badLibs.includes(lib)); + } + + return Promise.resolve({validLibs,badLibs}); + + } + +} \ No newline at end of file diff --git a/src/typings.ts b/src/typings.ts index 381b4e6cb..e902d7c73 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -240,4 +240,23 @@ export type SearchHit = { export type SearchHitLine = { number: number content: string -} \ No newline at end of file +} + +export interface MemberParts extends IBMiMember { + basename: string +} + + +export type RemoteApp = { + path: string + specific ?: string + names: string[] +} + +export type RemoteApps = RemoteApp[]; + +export type RemoteFeature = string | undefined; + +export type RemoteFeatures = { [name: string]: RemoteFeature }; + +export type aspInfo = { [id: number]: string }; From 73ee37c3d6ec4d8ab96dbfe6ade2d64f504cb2f3 Mon Sep 17 00:00:00 2001 From: krethan Date: Tue, 1 Oct 2024 23:38:26 -0600 Subject: [PATCH 26/28] Fixed few issues introduced with IBMiSettings class. --- src/api/IBMi.ts | 30 +++++++++++++++----------- src/api/IBMiSettings.ts | 48 ++++++++++++++++++++--------------------- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index a39860ef5..5290ba176 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -244,7 +244,7 @@ export default class IBMi { let libraryListResult = await connSettings.getLibraryList(); if (libraryListResult.libStatus) { - + this.defaultUserLibraries = libraryListResult.defaultUserLibraries; //If this is the first time the config is made, then these arrays will be empty @@ -410,12 +410,14 @@ export default class IBMi { }); await remoteApps.checkRemoteFeatures(remoteApp, this); - this.remoteFeatures = remoteApps.getRemoteFeatures(); } catch (e) { console.log(e); } } + + this.remoteFeatures = remoteApps.getRemoteFeatures(); + } if (this.sqlRunnerAvailable()) { @@ -430,9 +432,11 @@ 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. - - this.aspInfo = await connSettings.getASPInfo(); - if (Object.keys(this.aspInfo).length === 0) { + try { + this.aspInfo = await connSettings.getASPInfo(); + } + catch (e) { + //Oh well progress.report({ message: `Failed to get ASP information.` }); @@ -542,13 +546,13 @@ export default class IBMi { }); if ((!quickConnect || !cachedServerSettings?.pathChecked)) { - + const bashrcFile = `${homeResult.homeDir}/.bashrc`; - + let bashrcExists = await connSettings.checkBashRCFile(bashrcFile); - + let checkPathResult = await connSettings.checkPaths(["/QOpenSys/pkgs/bin", "/usr/bin", "/QOpenSys/usr/bin"]); - + if (checkPathResult.reason && await vscode.window.showWarningMessage(`${checkPathResult.missingPath} not found in $PATH`, { modal: true, detail: `${checkPathResult.reason}, so Code for IBM i may not function correctly. Would you like to ${bashrcExists ? "update" : "create"} ${bashrcFile} to fix this now?`, @@ -557,7 +561,7 @@ export default class IBMi { this.appendOutput(`${bashrcExists ? "update" : "create"} ${bashrcFile}`); if (!bashrcExists) { //Create bashrc File - let createBashResult = await connSettings.createBashrcFile(bashrcFile,connectionObject.username); + let createBashResult = await connSettings.createBashrcFile(bashrcFile, connectionObject.username); //Error creating bashrc File if (!createBashResult.createBash) { vscode.window.showWarningMessage(`Error creating ${bashrcFile}):\n${createBashResult.createBashMsg}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); @@ -566,7 +570,7 @@ export default class IBMi { else { //Update bashRC file let updateBashResult = await connSettings.updateBashrcFile(bashrcFile); - if(!updateBashResult.updateBash) { + if (!updateBashResult.updateBash) { vscode.window.showWarningMessage(`Error modifying PATH in ${bashrcFile}):\n${updateBashResult.updateBashMsg}.\n\n Code for IBM i may not function correctly. Please contact your system administrator.`, { modal: true }); } } @@ -612,8 +616,8 @@ export default class IBMi { message: `Validate configured library list` }); - let libraryListResult = await connSettings.validateLibraryList(this.defaultUserLibraries,this.config.libraryList); - if(libraryListResult.badLibs.length > 0) { + let libraryListResult = await connSettings.validateLibraryList(this.defaultUserLibraries, this.config.libraryList); + if (libraryListResult.badLibs.length > 0) { const chosen = await vscode.window.showWarningMessage(`The following ${libraryListResult.badLibs.length > 1 ? `libraries` : `library`} does not exist: ${libraryListResult.badLibs.join(`,`)}. Remove ${libraryListResult.badLibs.length > 1 ? `them` : `it`} from the library list?`, `Yes`, `No`); if (chosen === `Yes`) { this.config!.libraryList = libraryListResult.validLibs; diff --git a/src/api/IBMiSettings.ts b/src/api/IBMiSettings.ts index ad5d07869..c3eb961a9 100644 --- a/src/api/IBMiSettings.ts +++ b/src/api/IBMiSettings.ts @@ -289,7 +289,7 @@ export default class IBMiSettings { } } catch (e) { //Oh well - aspInfo = {}; + return Promise.reject(e); } return Promise.resolve(aspInfo); @@ -399,14 +399,12 @@ export default class IBMiSettings { } // If reason is still undefined, then we know the user has all the required paths. Then we don't // need to check for their existence before checking the order of the required paths. - if (!reason) { - for (let x = 1; x <= requiredPaths.length; x++) { - if (currentPaths.indexOf(requiredPaths[0]) > currentPaths.indexOf(requiredPaths[x])) { - reason = `${requiredPaths[0]} is not in the right position in your $PATH shell environment variable`; - missingPath = requiredPaths[0]; - break; - } - } + + if (!reason && + (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/usr/bin") + || (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/QOpenSys/usr/bin")))) { + reason = "/QOpenSys/pkgs/bin is not in the right position in your $PATH shell environment variable"; + missingPath = "/QOpenSys/pkgs/bin" } return Promise.resolve({ reason: reason, missingPath: missingPath }); @@ -481,34 +479,34 @@ export default class IBMiSettings { return Promise.resolve({ updateBash, updateBashMsg }); } - async validateLibraryList(defaultUserLibraries: string[], libraryList: string[]): Promise<{validLibs: string[], badLibs: string[]}> { - - let validLibs: string[] = []; - let badLibs: string[] = []; + async validateLibraryList(defaultUserLibraries: string[], libraryList: string[]): Promise<{ validLibs: string[], badLibs: string[] }> { + + let validLibs: string[] = []; + let badLibs: string[] = []; - const result = await this.connection.sendQsh({ + const result = await this.connection.sendQsh({ command: [ - `liblist -d ` + defaultUserLibraries.join(` `).replace(/\$/g, `\\$`), - ...libraryList.map(lib => `liblist -a ` + lib.replace(/\$/g, `\\$`)) + `liblist -d ` + defaultUserLibraries.join(` `).replace(/\$/g, `\\$`), + ...libraryList.map(lib => `liblist -a ` + lib.replace(/\$/g, `\\$`)) ].join(`; `) - }); + }); - if (result.stderr) { + if (result.stderr) { const lines = result.stderr.split(`\n`); lines.forEach(line => { - const badLib = libraryList.find(lib => line.includes(`ibrary ${lib} `)); + const badLib = libraryList.find(lib => line.includes(`ibrary ${lib} `)); - // If there is an error about the library, store it - if (badLib) badLibs.push(badLib); + // If there is an error about the library, store it + if (badLib) badLibs.push(badLib); }); - } + } - if (result && badLibs.length > 0) { + if (result && badLibs.length > 0) { validLibs = libraryList.filter(lib => !badLibs.includes(lib)); - } + } - return Promise.resolve({validLibs,badLibs}); + return Promise.resolve({ validLibs, badLibs }); } From 6c5df527531b0f3352bb5241ce22d09817638a46 Mon Sep 17 00:00:00 2001 From: krethan Date: Tue, 1 Oct 2024 23:41:30 -0600 Subject: [PATCH 27/28] Removed unneccessary comments --- src/api/IBMi.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 5290ba176..8b5b8f4d0 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -310,12 +310,10 @@ export default class IBMi { } - //TO DO: why is this required???? const commandShellResult = await this.sendCommand({ command: `echo $SHELL` }); - //TO DO: why is this required???? if (commandShellResult.code === 0) { this.shell = commandShellResult.stdout.trim(); } From f9d0e4c59c6be870bd94f6260581da208b07963a Mon Sep 17 00:00:00 2001 From: krethan Date: Sat, 12 Oct 2024 21:14:24 -0600 Subject: [PATCH 28/28] Reuse checkObject from content and moved deleteObject into content. Fixed indentation from 4 characters to 2. --- src/api/IBMi.ts | 8 +- src/api/IBMiApps.ts | 136 ++++---- src/api/IBMiContent.ts | 14 +- src/api/IBMiSettings.ts | 720 +++++++++++++++++++--------------------- 4 files changed, 433 insertions(+), 445 deletions(-) diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 8b5b8f4d0..28eb4c97c 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -326,7 +326,7 @@ export default class IBMi { message: `Checking for bad data areas.` }); - const QCPTOIMPF = await connSettings.checkObjectExists('QSYS', 'QCPTOIMPF', '*DTAARA'); + const QCPTOIMPF = await this.content.checkObject({ library: 'QSYS', name: 'QCPTOIMPF', type: '*DTAARA' }); if (QCPTOIMPF) { vscode.window.showWarningMessage(`The data area QSYS/QCPTOIMPF exists on this system and may impact Code for IBM i functionality.`, { @@ -335,7 +335,7 @@ export default class IBMi { }, `Delete`, `Read more`).then(choice => { switch (choice) { case `Delete`: - connSettings.deleteObject('QSYS', 'QCPTOIMPF', '*DTAARA').then((result) => { + this.content.deleteObject({ library: 'QSYS', name: 'QCPTOIMPF', type: '*DTAARA' }).then((result) => { if (result) { vscode.window.showInformationMessage(`The data area QSYS/QCPTOIMPF has been deleted.`); @@ -354,7 +354,7 @@ export default class IBMi { }); } - const QCPFRMIMPF = await connSettings.checkObjectExists('QSYS', 'QCPFRMIMPF', '*DTAARA'); + const QCPFRMIMPF = await this.content.checkObject({ library: 'QSYS', name: 'QCPFRMIMPF', type: '*DTAARA' }); if (QCPFRMIMPF) { vscode.window.showWarningMessage(`The data area QSYS/QCPFRMIMPF exists on this system and may impact Code for IBM i functionality.`, { @@ -362,7 +362,7 @@ export default class IBMi { }, `Delete`, `Read more`).then(choice => { switch (choice) { case `Delete`: - connSettings.deleteObject('QSYS', 'QCPFRMIMPF', '*DTAARA') + this.content.deleteObject({ library: 'QSYS', name: 'QCPFRMIMPF', type: '*DTAARA' }) .then((result) => { if (result) { vscode.window.showInformationMessage(`The data area QSYS/QCPFRMIMPF has been deleted.`); diff --git a/src/api/IBMiApps.ts b/src/api/IBMiApps.ts index abfc48974..b5527c122 100644 --- a/src/api/IBMiApps.ts +++ b/src/api/IBMiApps.ts @@ -3,85 +3,85 @@ import { RemoteApp, RemoteApps, RemoteFeatures } from "../typings"; export default class IBMiApps { - private remoteApps: RemoteApps; - private remoteFeatures: RemoteFeatures; - - constructor() { - this.remoteApps = [ - { - path: `/usr/bin/`, - names: [`setccsid`, `iconv`, `attr`, `tar`, `ls`] - }, - { - path: `/QOpenSys/pkgs/bin/`, - names: [`git`, `grep`, `tn5250`, `md5sum`, `bash`, `chsh`, `stat`, `sort`, `tar`, `ls`, `find`] - }, - { - path: `/QSYS.LIB/`, - // In the future, we may use a generic specific. - // Right now we only need one program - // specific: `*.PGM`, - specific: `QZDFMDB2.PGM`, - names: [`QZDFMDB2.PGM`] - }, - { - path: `/QIBM/ProdData/IBMiDebugService/bin/`, - specific: `startDebugService.sh`, - names: [`startDebugService.sh`] - } - ]; - - this.remoteFeatures = {}; - this.setRemoteFeatures(); - + private remoteApps: RemoteApps; + private remoteFeatures: RemoteFeatures; + + constructor() { + this.remoteApps = [ + { + path: `/usr/bin/`, + names: [`setccsid`, `iconv`, `attr`, `tar`, `ls`] + }, + { + path: `/QOpenSys/pkgs/bin/`, + names: [`git`, `grep`, `tn5250`, `md5sum`, `bash`, `chsh`, `stat`, `sort`, `tar`, `ls`, `find`] + }, + { + path: `/QSYS.LIB/`, + // In the future, we may use a generic specific. + // Right now we only need one program + // specific: `*.PGM`, + specific: `QZDFMDB2.PGM`, + names: [`QZDFMDB2.PGM`] + }, + { + path: `/QIBM/ProdData/IBMiDebugService/bin/`, + specific: `startDebugService.sh`, + names: [`startDebugService.sh`] + } + ]; + + this.remoteFeatures = {}; + this.initRemoteFeatures(); + + } + + addRemoteApp(remoteApp: RemoteApp) { + + //Add remote App + this.remoteApps.push(remoteApp); + + //Add possible features to list + for (const name of remoteApp.names) { + this.remoteFeatures[name] = undefined; } - addRemoteApp(remoteApp: RemoteApp) { - - //Add remote App - this.remoteApps.push(remoteApp); + } - //Add possible features to list - for(const name of remoteApp.names) { - this.remoteFeatures[name] = undefined; - } + getRemoteApps(): RemoteApps { + return this.remoteApps; + } - } + initRemoteFeatures() { - getRemoteApps(): RemoteApps { - return this.remoteApps; + for (const feature of this.remoteApps) { + for (const name of feature.names) { + this.remoteFeatures[name] = undefined; + } } - setRemoteFeatures() { + } - for (const feature of this.remoteApps) { - for(const name of feature.names) { - this.remoteFeatures[name] = undefined; - } - } - - } + getRemoteFeatures(): RemoteFeatures { + return this.remoteFeatures; + } - getRemoteFeatures(): RemoteFeatures { - return this.remoteFeatures; - } + async checkRemoteFeatures(remoteApp: RemoteApp, connection: IBMi) { - async checkRemoteFeatures(remoteApp: RemoteApp, connection: IBMi) { - - const call = await connection.sendCommand({ command: `ls -p ${remoteApp.path}${remoteApp.specific || ``}` }); - if (call.stdout) { - const files = call.stdout.split(`\n`); - - if (remoteApp.specific) { - for (const name of remoteApp.names) - this.remoteFeatures[name] = files.find(file => file.includes(name)); - } else { - for (const name of remoteApp.names) - if (files.includes(name)) - this.remoteFeatures[name] = remoteApp.path + name; - } - } + const call = await connection.sendCommand({ command: `ls -p ${remoteApp.path}${remoteApp.specific || ``}` }); + if (call.stdout) { + const files = call.stdout.split(`\n`); + if (remoteApp.specific) { + for (const name of remoteApp.names) + this.remoteFeatures[name] = files.find(file => file.includes(name)); + } else { + for (const name of remoteApp.names) + if (files.includes(name)) + this.remoteFeatures[name] = remoteApp.path + name; + } } + } + } \ No newline at end of file diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 0775b561a..1a5a1c096 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -906,6 +906,18 @@ export default class IBMiContent { })).code === 0; } + async deleteObject(object: { library: string, name: string, type: string }) { + + return (await this.ibmi.runCommand({ + command: this.toCl(`DLTOBJ`, { + obj: `${this.ibmi.upperCaseName(object.library)}/${this.ibmi.upperCaseName(object.name)}`, + objtype: object.type.toLocaleUpperCase() + }), + noLibList: true + })).code === 0; + + } + async testStreamFile(path: string, right: "e" | "f" | "d" | "r" | "w" | "x") { return (await this.ibmi.sendCommand({ command: `test -${right} ${Tools.escapePath(path)}` })).code === 0; } @@ -951,7 +963,7 @@ export default class IBMiContent { return cl; } - async getAttributes(path: string | (QsysPath & { member?: string }), ...operands: AttrOperands[]) { + async getAttributes(path: string | (QsysPath & { member?: string }), ...operands: AttrOperands[]) { const target = (path = typeof path === 'string' ? Tools.escapePath(path) : this.ibmi.sysNameInAmerican(Tools.qualifyPath(path.library, path.name, path.member, path.asp))); const result = await this.ibmi.sendCommand({ command: `${this.ibmi.remoteFeatures.attr} -p ${target} ${operands.join(" ")}` }); diff --git a/src/api/IBMiSettings.ts b/src/api/IBMiSettings.ts index c3eb961a9..e0b3755ab 100644 --- a/src/api/IBMiSettings.ts +++ b/src/api/IBMiSettings.ts @@ -7,507 +7,483 @@ const CCSID_SYSVAL = -2; export default class IBMiSettings { - constructor(private connection: IBMi) { + constructor(private connection: IBMi) { - } + } - async checkShellOutput(): Promise { + async checkShellOutput(): Promise { - const checkShellText = `This should be the only text!`; - const checkShellResult = await this.connection.sendCommand({ - command: `echo "${checkShellText}"`, - directory: `.` - }); + const checkShellText = `This should be the only text!`; + const checkShellResult = await this.connection.sendCommand({ + command: `echo "${checkShellText}"`, + directory: `.` + }); - return Promise.resolve(checkShellResult.stdout.split(`\n`)[0] == checkShellText); + return Promise.resolve(checkShellResult.stdout.split(`\n`)[0] == checkShellText); - } + } - async getHomeDirectory(): Promise<{ homeExists: boolean, homeDir: string, homeMsg: string }> { + async getHomeDirectory(): Promise<{ homeExists: boolean, homeDir: string, homeMsg: string }> { - let homeDir; - let homeMsg = ''; - let homeExists; + let homeDir; + let homeMsg = ''; + let homeExists; - const homeResult = await this.connection.sendCommand({ - command: `echo $HOME && cd && test -w $HOME`, - directory: `.` - }); + const homeResult = await this.connection.sendCommand({ + command: `echo $HOME && cd && test -w $HOME`, + directory: `.` + }); - // Note: if the home directory does not exist, the behavior of the echo/cd/test command combo is as follows: - // - stderr contains 'Could not chdir to home directory /home/________: No such file or directory' - // (The output contains 'chdir' regardless of locale and shell, so maybe we could use that - // if we iterate on this code again in the future) - // - stdout contains the name of the home directory (even if it does not exist) - // - The 'cd' command causes an error if the home directory does not exist or otherwise can't be cd'ed into - // - The 'test' command causes an error if the home directory is not writable (one can cd into a non-writable directory) - - homeExists = homeResult.code == 0; - homeDir = homeResult.stdout.trim(); - - if (!homeExists) { - // Let's try to provide more valuable information to the user about why their home directory - // is bad and maybe even provide the opportunity to create the home directory - - // we _could_ just assume the home directory doesn't exist but maybe there's something more going on, namely mucked-up permissions - homeExists = (0 === (await this.connection.sendCommand({ command: `test -e ${homeDir}` })).code); - if (homeExists) { - // Note: this logic might look backward because we fall into this (failure) leg on what looks like success (home dir exists). - // But, remember, but we only got here if 'cd $HOME' failed. - // Let's try to figure out why.... - if (0 !== (await this.connection.sendCommand({ command: `test -d ${homeDir}` })).code) { - homeMsg = `Your home directory (${homeDir}) is not a directory! Code for IBM i may not function correctly. Please contact your system administrator.`; - } - else if (0 !== (await this.connection.sendCommand({ command: `test -w ${homeDir}` })).code) { - homeMsg = `Your home directory (${homeDir}) is not writable! Code for IBM i may not function correctly. Please contact your system administrator.`; - - } - else if (0 !== (await this.connection.sendCommand({ command: `test -x ${homeDir}` })).code) { - homeMsg = `Your home directory (${homeDir}) is not usable due to permissions! Code for IBM i may not function correctly. Please contact your system administrator.`; - } - else { - // not sure, but get your sys admin involved - homeMsg = `Your home directory (${homeDir}) exists but is unusable. Code for IBM i may not function correctly. Please contact your system administrator.`; - } - } - else { - homeMsg = `Your home directory (${homeDir}) does not exist, so Code for IBM i may not function correctly.`; - } - } + // Note: if the home directory does not exist, the behavior of the echo/cd/test command combo is as follows: + // - stderr contains 'Could not chdir to home directory /home/________: No such file or directory' + // (The output contains 'chdir' regardless of locale and shell, so maybe we could use that + // if we iterate on this code again in the future) + // - stdout contains the name of the home directory (even if it does not exist) + // - The 'cd' command causes an error if the home directory does not exist or otherwise can't be cd'ed into + // - The 'test' command causes an error if the home directory is not writable (one can cd into a non-writable directory) + + homeExists = homeResult.code == 0; + homeDir = homeResult.stdout.trim(); + + if (!homeExists) { + // Let's try to provide more valuable information to the user about why their home directory + // is bad and maybe even provide the opportunity to create the home directory - return Promise.resolve({ homeExists, homeDir, homeMsg }); + // we _could_ just assume the home directory doesn't exist but maybe there's something more going on, namely mucked-up permissions + homeExists = (0 === (await this.connection.sendCommand({ command: `test -e ${homeDir}` })).code); + if (homeExists) { + // Note: this logic might look backward because we fall into this (failure) leg on what looks like success (home dir exists). + // But, remember, but we only got here if 'cd $HOME' failed. + // Let's try to figure out why.... + if (0 !== (await this.connection.sendCommand({ command: `test -d ${homeDir}` })).code) { + homeMsg = `Your home directory (${homeDir}) is not a directory! Code for IBM i may not function correctly. Please contact your system administrator.`; + } + else if (0 !== (await this.connection.sendCommand({ command: `test -w ${homeDir}` })).code) { + homeMsg = `Your home directory (${homeDir}) is not writable! Code for IBM i may not function correctly. Please contact your system administrator.`; + } + else if (0 !== (await this.connection.sendCommand({ command: `test -x ${homeDir}` })).code) { + homeMsg = `Your home directory (${homeDir}) is not usable due to permissions! Code for IBM i may not function correctly. Please contact your system administrator.`; + } + else { + // not sure, but get your sys admin involved + homeMsg = `Your home directory (${homeDir}) exists but is unusable. Code for IBM i may not function correctly. Please contact your system administrator.`; + } + } + else { + homeMsg = `Your home directory (${homeDir}) does not exist, so Code for IBM i may not function correctly.`; + } } - async createHomeDirectory(homeDir: string, username: string): Promise<{ homeCreated: boolean, homeMsg: string }> { + return Promise.resolve({ homeExists, homeDir, homeMsg }); - let homeCreated = false; - let homeMsg = ''; + } - const homeCmd = `mkdir -p ${homeDir} && chown ${username.toLowerCase()} ${homeDir} && chmod 0755 ${homeDir}`; + async createHomeDirectory(homeDir: string, username: string): Promise<{ homeCreated: boolean, homeMsg: string }> { - let mkHomeResult = await this.connection.sendCommand({ command: homeCmd, directory: `.` }); + let homeCreated = false; + let homeMsg = ''; - if (0 === mkHomeResult.code) { - homeCreated = true; - } else { - let mkHomeErrs = mkHomeResult.stderr; - // We still get 'Could not chdir to home directory' in stderr so we need to hackily gut that out, as well as the bashisms that are a side effect of our API - homeMsg = mkHomeErrs.substring(1 + mkHomeErrs.indexOf(`\n`)).replace(`bash: line 1: `, ``); - } + const homeCmd = `mkdir -p ${homeDir} && chown ${username.toLowerCase()} ${homeDir} && chmod 0755 ${homeDir}`; - return Promise.resolve({ homeCreated, homeMsg }); + let mkHomeResult = await this.connection.sendCommand({ command: homeCmd, directory: `.` }); + if (0 === mkHomeResult.code) { + homeCreated = true; + } else { + let mkHomeErrs = mkHomeResult.stderr; + // We still get 'Could not chdir to home directory' in stderr so we need to hackily gut that out, as well as the bashisms that are a side effect of our API + homeMsg = mkHomeErrs.substring(1 + mkHomeErrs.indexOf(`\n`)).replace(`bash: line 1: `, ``); } - async getLibraryList(): Promise<{ libStatus: boolean, currentLibrary: string, defaultUserLibraries: string[] }> { - + return Promise.resolve({ homeCreated, homeMsg }); + } - //Since the compiles are stateless, then we have to set the library list each time we use the `SYSTEM` command - //We setup the defaultUserLibraries here so we can remove them later on so the user can setup their own library list + async getLibraryList(): Promise<{ libStatus: boolean, currentLibrary: string, defaultUserLibraries: string[] }> { - let currentLibrary = `QGPL`; - let defaultUserLibraries = []; - let libStatus = false; - const liblResult = await this.connection.sendQsh({ - command: `liblist` - }); - if (liblResult.code === 0) { - libStatus = true; - const libraryListString = liblResult.stdout; - if (libraryListString !== ``) { - const libraryList = libraryListString.split(`\n`); - - let lib, type; - for (const line of libraryList) { - lib = line.substring(0, 10).trim(); - type = line.substring(12); - - switch (type) { - case `USR`: - defaultUserLibraries.push(lib); - break; - - case `CUR`: - currentLibrary = lib; - break; - } - } - } - } + //Since the compiles are stateless, then we have to set the library list each time we use the `SYSTEM` command + //We setup the defaultUserLibraries here so we can remove them later on so the user can setup their own library list - return Promise.resolve({ libStatus, currentLibrary, defaultUserLibraries }); + let currentLibrary = `QGPL`; + let defaultUserLibraries = []; + let libStatus = false; - } + const liblResult = await this.connection.sendQsh({ + command: `liblist` + }); - async setTempLibrary(tempLibrary: string): Promise { + if (liblResult.code === 0) { + libStatus = true; + const libraryListString = liblResult.stdout; + if (libraryListString !== ``) { + const libraryList = libraryListString.split(`\n`); - let tempLibrarySet = false; + let lib, type; + for (const line of libraryList) { + lib = line.substring(0, 10).trim(); + type = line.substring(12); - //Check the temp lib (where temp outfile data lives) exists - const createdTempLib = await this.connection.runCommand({ - command: `CRTLIB LIB(${tempLibrary}) TEXT('Code for i temporary objects. May be cleared.')`, - noLibList: true - }); + switch (type) { + case `USR`: + defaultUserLibraries.push(lib); + break; - if (createdTempLib.code === 0) { - tempLibrarySet = true; + case `CUR`: + currentLibrary = lib; + break; + } } - else { - const messages = Tools.parseMessages(createdTempLib.stderr); - if (messages.findId(`CPF2158`) || messages.findId(`CPF2111`)) { //Already exists, hopefully ok :) - tempLibrarySet = true; - } - else if (messages.findId(`CPD0032`)) { //Can't use CRTLIB - const tempLibExists = await this.connection.runCommand({ - command: `CHKOBJ OBJ(QSYS/${tempLibrary}) OBJTYPE(*LIB)`, - noLibList: true - }); - - if (tempLibExists.code === 0) { - //We're all good if no errors - tempLibrarySet = true; - } - else { - tempLibrarySet = false; - } + } + } - } - } + return Promise.resolve({ libStatus, currentLibrary, defaultUserLibraries }); - return Promise.resolve(tempLibrarySet); + } - } + async setTempLibrary(tempLibrary: string): Promise { - async setTempDirectory(tempDir: string): Promise { + let tempLibrarySet = false; - let tempDirSet = false; + //Check the temp lib (where temp outfile data lives) exists + const createdTempLib = await this.connection.runCommand({ + command: `CRTLIB LIB(${tempLibrary}) TEXT('Code for i temporary objects. May be cleared.')`, + noLibList: true + }); - // Check if the temp directory exists - let result = await this.connection.sendCommand({ - command: `[ -d "${tempDir}" ]` + if (createdTempLib.code === 0) { + tempLibrarySet = true; + } + else { + const messages = Tools.parseMessages(createdTempLib.stderr); + if (messages.findId(`CPF2158`) || messages.findId(`CPF2111`)) { //Already exists, hopefully ok :) + tempLibrarySet = true; + } + else if (messages.findId(`CPD0032`)) { //Can't use CRTLIB + const tempLibExists = await this.connection.runCommand({ + command: `CHKOBJ OBJ(QSYS/${tempLibrary}) OBJTYPE(*LIB)`, + noLibList: true }); - if (result.code === 0) { - // Directory exists - tempDirSet = true; - } else { - // Directory does not exist, try to create it - let result = await this.connection.sendCommand({ - command: `mkdir -p ${tempDir}` - }); - if (result.code === 0) { - // Directory created - tempDirSet = true; - } else { - // Directory not created - } + if (tempLibExists.code === 0) { + //We're all good if no errors + tempLibrarySet = true; + } + else { + tempLibrarySet = false; } - return Promise.resolve(tempDirSet); + } + } + return Promise.resolve(tempLibrarySet); + + } + + async setTempDirectory(tempDir: string): Promise { + + let tempDirSet = false; + + // Check if the temp directory exists + let result = await this.connection.sendCommand({ + command: `[ -d "${tempDir}" ]` + }); + + if (result.code === 0) { + // Directory exists + tempDirSet = true; + } else { + // Directory does not exist, try to create it + let result = await this.connection.sendCommand({ + command: `mkdir -p ${tempDir}` + }); + if (result.code === 0) { + // Directory created + tempDirSet = true; + } else { + // Directory not created + } } - async clearTempLibrary(tempLibrary: string): Promise { + return Promise.resolve(tempDirSet); - let clearMsg = ''; + } - this.connection.runCommand({ - command: `DLTOBJ OBJ(${tempLibrary}/O_*) OBJTYPE(*FILE)`, - noLibList: true, - }) - .then(result => { - // All good! - if (result && result.stderr) { - const messages = Tools.parseMessages(result.stderr); - if (!messages.findId(`CPF2125`)) { - clearMsg = `Temporary data not cleared from ${tempLibrary}.`; - } - } - }); + async clearTempLibrary(tempLibrary: string): Promise { - return Promise.resolve(clearMsg); + let clearMsg = ''; - } + this.connection.runCommand({ + command: `DLTOBJ OBJ(${tempLibrary}/O_*) OBJTYPE(*FILE)`, + noLibList: true, + }) + .then(result => { + // All good! + if (result && result.stderr) { + const messages = Tools.parseMessages(result.stderr); + if (!messages.findId(`CPF2125`)) { + clearMsg = `Temporary data not cleared from ${tempLibrary}.`; + } + } + }); - async clearTempDirectory(tempDir: string): Promise { + return Promise.resolve(clearMsg); - let clearMsg = ''; + } - this.connection.sendCommand({ - command: `rm -rf ${path.posix.join(tempDir, `vscodetemp*`)}` - }) - .then(result => { - // All good! - }) - .catch(e => { - // CPF2125: No objects deleted. - clearMsg = `Temporary data not cleared from ${tempDir}.`; - }); + async clearTempDirectory(tempDir: string): Promise { - return Promise.resolve(clearMsg); + let clearMsg = ''; + try { + this.connection.sendCommand({ + command: `rm -rf ${path.posix.join(tempDir, `vscodetemp*`)}` + }); + } + catch (e) { + // CPF2125: No objects deleted. + clearMsg = `Temporary data not cleared from ${tempDir}.`; } - async checkObjectExists(library: string, object: string, type: string): Promise { - - const objResult = await this.connection.runCommand({ - command: `CHKOBJ OBJ(${library}/${object}) OBJTYPE(${type})`, - noLibList: true - }); + return Promise.resolve(clearMsg); - return Promise.resolve(objResult.code === 0); + } - } + async getASPInfo(): Promise { - async deleteObject(library: string, object: string, type: string): Promise { + let aspInfo: aspInfo = {}; - const deleteResult = await this.connection.runCommand({ - command: `DLTOBJ OBJ(${library}/${object}) OBJTYPE(${type})`, - noLibList: true + try { + const resultSet = await this.connection.runSQL(`SELECT * FROM QSYS2.ASP_INFO`); + if (resultSet.length) { + resultSet.forEach(row => { + if (row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME !== `null`) { + aspInfo[Number(row.ASP_NUMBER)] = String(row.DEVICE_DESCRIPTION_NAME); + } }); - - return Promise.resolve(deleteResult.code === 0); - + } + } catch (e) { + //Oh well + return Promise.reject(e); } - async getASPInfo(): Promise { + return Promise.resolve(aspInfo); - let aspInfo: aspInfo = {}; + } - try { - const resultSet = await this.connection.runSQL(`SELECT * FROM QSYS2.ASP_INFO`); - if (resultSet.length) { - resultSet.forEach(row => { - if (row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME !== `null`) { - aspInfo[Number(row.ASP_NUMBER)] = String(row.DEVICE_DESCRIPTION_NAME); - } - }); - } - } catch (e) { - //Oh well - return Promise.reject(e); - } + async getQCCSID(): Promise { - return Promise.resolve(aspInfo); + let qccsid = 0; + const [systemCCSID] = await this.connection.runSQL(`select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`); + if (typeof systemCCSID.CURRENT_NUMERIC_VALUE === 'number') { + qccsid = systemCCSID.CURRENT_NUMERIC_VALUE; } - async getQCCSID(): Promise { + return Promise.resolve(qccsid); - let qccsid = 0; + } - const [systemCCSID] = await this.connection.runSQL(`select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`); - if (typeof systemCCSID.CURRENT_NUMERIC_VALUE === 'number') { - qccsid = systemCCSID.CURRENT_NUMERIC_VALUE; - } + async getjobCCSID(userName: string): Promise { - return Promise.resolve(qccsid); + let jobCCSID = CCSID_SYSVAL; + const [userInfo] = await this.connection.runSQL(`select CHARACTER_CODE_SET_ID from table( QSYS2.QSYUSRINFO( USERNAME => upper('${userName}') ) )`); + if (userInfo.CHARACTER_CODE_SET_ID !== `null` && typeof userInfo.CHARACTER_CODE_SET_ID === 'number') { + jobCCSID = userInfo.CHARACTER_CODE_SET_ID; } - async getjobCCSID(userName: string): Promise { + return Promise.resolve(jobCCSID); - let jobCCSID = CCSID_SYSVAL; + } - const [userInfo] = await this.connection.runSQL(`select CHARACTER_CODE_SET_ID from table( QSYS2.QSYUSRINFO( USERNAME => upper('${userName}') ) )`); - if (userInfo.CHARACTER_CODE_SET_ID !== `null` && typeof userInfo.CHARACTER_CODE_SET_ID === 'number') { - jobCCSID = userInfo.CHARACTER_CODE_SET_ID; - } + async getDefaultCCSID(): Promise { - return Promise.resolve(jobCCSID); + let userDefaultCCSID = 0; + try { + const [activeJob] = await this.connection.runSQL(`Select DEFAULT_CCSID From Table(QSYS2.ACTIVE_JOB_INFO( JOB_NAME_FILTER => '*', DETAILED_INFO => 'ALL' ))`); + userDefaultCCSID = Number(activeJob.DEFAULT_CCSID); + } + catch (error) { + const [defaultCCSID] = (await this.connection.runCommand({ command: "DSPJOB OPTION(*DFNA)" })) + .stdout + .split("\n") + .filter(line => line.includes("DFTCCSID")); + + const defaultCCSCID = Number(defaultCCSID.split("DFTCCSID").at(1)?.trim()); + if (defaultCCSCID && !isNaN(defaultCCSCID)) { + userDefaultCCSID = defaultCCSCID; + } } - async getDefaultCCSID(): Promise { + return Promise.resolve(userDefaultCCSID); - let userDefaultCCSID = 0; + } - try { - const [activeJob] = await this.connection.runSQL(`Select DEFAULT_CCSID From Table(QSYS2.ACTIVE_JOB_INFO( JOB_NAME_FILTER => '*', DETAILED_INFO => 'ALL' ))`); - userDefaultCCSID = Number(activeJob.DEFAULT_CCSID); - } - catch (error) { - const [defaultCCSID] = (await this.connection.runCommand({ command: "DSPJOB OPTION(*DFNA)" })) - .stdout - .split("\n") - .filter(line => line.includes("DFTCCSID")); - - const defaultCCSCID = Number(defaultCCSID.split("DFTCCSID").at(1)?.trim()); - if (defaultCCSCID && !isNaN(defaultCCSCID)) { - userDefaultCCSID = defaultCCSCID; - } - } + async getLocalEncodingValues(): Promise { - return Promise.resolve(userDefaultCCSID); + let localEncoding = ''; + const [variants] = await this.connection.runSQL(`With VARIANTS ( HASH, AT, DOLLARSIGN ) as (` + + ` values ( cast( x'7B' as varchar(1) )` + + ` , cast( x'7C' as varchar(1) )` + + ` , cast( x'5B' as varchar(1) ) )` + + `)` + + `Select HASH concat AT concat DOLLARSIGN as LOCAL from VARIANTS`); + + if (typeof variants.LOCAL === 'string' && variants.LOCAL !== `null`) { + localEncoding = variants.LOCAL; } - async getLocalEncodingValues(): Promise { + return Promise.resolve(localEncoding); - let localEncoding = ''; + } - const [variants] = await this.connection.runSQL(`With VARIANTS ( HASH, AT, DOLLARSIGN ) as (` - + ` values ( cast( x'7B' as varchar(1) )` - + ` , cast( x'7C' as varchar(1) )` - + ` , cast( x'5B' as varchar(1) ) )` - + `)` - + `Select HASH concat AT concat DOLLARSIGN as LOCAL from VARIANTS`); + async setBash(): Promise { - if (typeof variants.LOCAL === 'string' && variants.LOCAL !== `null`) { - localEncoding = variants.LOCAL; - } + let bashset = false; - return Promise.resolve(localEncoding); + const commandSetBashResult = await this.connection.sendCommand({ + command: `/QOpenSys/pkgs/bin/chsh -s /QOpenSys/pkgs/bin/bash` + }); - } + if (!commandSetBashResult.stderr) bashset = true; - async setBash(): Promise { + return Promise.resolve(bashset); - let bashset = false; + } - const commandSetBashResult = await this.connection.sendCommand({ - command: `/QOpenSys/pkgs/bin/chsh -s /QOpenSys/pkgs/bin/bash` - }); + async getEnvironmentVariable(envVar: string): Promise { + return (await this.connection.sendCommand({ command: `echo ${envVar}` })).stdout.split(":"); + } - if (!commandSetBashResult.stderr) bashset = true; + async checkPaths(requiredPaths: string[]): Promise<{ reason: string, missingPath: string }> { - return Promise.resolve(bashset); + const currentPaths = await this.getEnvironmentVariable('$PATH'); - } + let reason = ''; + let missingPath = ''; - async getEnvironmentVariable(envVar: string): Promise { - return (await this.connection.sendCommand({ command: `echo ${envVar}` })).stdout.split(":"); + for (const requiredPath of requiredPaths) { + if (!currentPaths.includes(requiredPath)) { + reason = `Your $PATH shell environment variable does not include ${requiredPath}`; + missingPath = requiredPath + break; + } + } + // If reason is still undefined, then we know the user has all the required paths. Then we don't + // need to check for their existence before checking the order of the required paths. + + if (!reason && + (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/usr/bin") + || (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/QOpenSys/usr/bin")))) { + reason = "/QOpenSys/pkgs/bin is not in the right position in your $PATH shell environment variable"; + missingPath = "/QOpenSys/pkgs/bin" } - async checkPaths(requiredPaths: string[]): Promise<{ reason: string, missingPath: string }> { + return Promise.resolve({ reason: reason, missingPath: missingPath }); - const currentPaths = await this.getEnvironmentVariable('$PATH'); + } - let reason = ''; - let missingPath = ''; + async checkBashRCFile(bashrcFile: string): Promise { - for (const requiredPath of requiredPaths) { - if (!currentPaths.includes(requiredPath)) { - reason = `Your $PATH shell environment variable does not include ${requiredPath}`; - missingPath = requiredPath - break; - } - } - // If reason is still undefined, then we know the user has all the required paths. Then we don't - // need to check for their existence before checking the order of the required paths. - - if (!reason && - (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/usr/bin") - || (currentPaths.indexOf("/QOpenSys/pkgs/bin") > currentPaths.indexOf("/QOpenSys/usr/bin")))) { - reason = "/QOpenSys/pkgs/bin is not in the right position in your $PATH shell environment variable"; - missingPath = "/QOpenSys/pkgs/bin" - } + let bashrcExists = false; - return Promise.resolve({ reason: reason, missingPath: missingPath }); + bashrcExists = (await this.connection.sendCommand({ command: `test -e ${bashrcFile}` })).code === 0; - } + return Promise.resolve(bashrcExists); + } - async checkBashRCFile(bashrcFile: string): Promise { + async createBashrcFile(bashrcFile: string, username: string): Promise<{ createBash: boolean, createBashMsg: string }> { - let bashrcExists = false; + let createBash = true; + let createBashMsg = ''; - bashrcExists = (await this.connection.sendCommand({ command: `test -e ${bashrcFile}` })).code === 0; + // Add "/usr/bin" and "/QOpenSys/usr/bin" to the end of the path. This way we know that the user has + // all the required paths, but we don't overwrite the priority of other items on their path. + const createBashrc = await this.connection.sendCommand({ command: `echo "# Generated by Code for IBM i\nexport PATH=/QOpenSys/pkgs/bin:\\$PATH:/QOpenSys/usr/bin:/usr/bin" >> ${bashrcFile} && chown ${username.toLowerCase()} ${bashrcFile} && chmod 755 ${bashrcFile}` }); - return Promise.resolve(bashrcExists); + if (createBashrc.code !== 0) { + createBash = false; + createBashMsg = createBashrc.stderr; } - async createBashrcFile(bashrcFile: string, username: string): Promise<{ createBash: boolean, createBashMsg: string }> { + return Promise.resolve({ createBash, createBashMsg }); - let createBash = true; - let createBashMsg = ''; + } - // Add "/usr/bin" and "/QOpenSys/usr/bin" to the end of the path. This way we know that the user has - // all the required paths, but we don't overwrite the priority of other items on their path. - const createBashrc = await this.connection.sendCommand({ command: `echo "# Generated by Code for IBM i\nexport PATH=/QOpenSys/pkgs/bin:\\$PATH:/QOpenSys/usr/bin:/usr/bin" >> ${bashrcFile} && chown ${username.toLowerCase()} ${bashrcFile} && chmod 755 ${bashrcFile}` }); + async updateBashrcFile(bashrcFile: string): Promise<{ updateBash: boolean, updateBashMsg: string }> { - if (createBashrc.code !== 0) { - createBash = false; - createBashMsg = createBashrc.stderr; - } + let updateBash = true; + let updateBashMsg = ''; - return Promise.resolve({ createBash, createBashMsg }); - - } - - async updateBashrcFile(bashrcFile: string): Promise<{ updateBash: boolean, updateBashMsg: string }> { - - let updateBash = true; - let updateBashMsg = ''; - - try { - const content = this.connection.content; - if (content) { - const bashrcContent = (await content.downloadStreamfile(bashrcFile)).split("\n"); - let replaced = false; - bashrcContent.forEach((line, index) => { - if (!replaced) { - const pathRegex = /^((?:export )?PATH=)(.*)(?:)$/.exec(line); - if (pathRegex) { - bashrcContent[index] = `${pathRegex[1]}/QOpenSys/pkgs/bin:${pathRegex[2] - .replace("/QOpenSys/pkgs/bin", "") //Removes /QOpenSys/pkgs/bin wherever it is - .replace("::", ":")}:/QOpenSys/usr/bin:/usr/bin`; //Removes double : in case /QOpenSys/pkgs/bin wasn't at the end - replaced = true; - } - } - }); - - if (!replaced) { - bashrcContent.push( - "", - "# Generated by Code for IBM i", - "export PATH=/QOpenSys/pkgs/bin:$PATH:/QOpenSys/usr/bin:/usr/bin" - ); - } - - await content.writeStreamfile(bashrcFile, bashrcContent.join("\n")); + try { + const content = this.connection.content; + if (content) { + const bashrcContent = (await content.downloadStreamfile(bashrcFile)).split("\n"); + let replaced = false; + bashrcContent.forEach((line, index) => { + if (!replaced) { + const pathRegex = /^((?:export )?PATH=)(.*)(?:)$/.exec(line); + if (pathRegex) { + bashrcContent[index] = `${pathRegex[1]}/QOpenSys/pkgs/bin:${pathRegex[2] + .replace("/QOpenSys/pkgs/bin", "") //Removes /QOpenSys/pkgs/bin wherever it is + .replace("::", ":")}:/QOpenSys/usr/bin:/usr/bin`; //Removes double : in case /QOpenSys/pkgs/bin wasn't at the end + replaced = true; } - } - catch (error) { - updateBash = false; - updateBashMsg = error; + } + }); + + if (!replaced) { + bashrcContent.push( + "", + "# Generated by Code for IBM i", + "export PATH=/QOpenSys/pkgs/bin:$PATH:/QOpenSys/usr/bin:/usr/bin" + ); } - return Promise.resolve({ updateBash, updateBashMsg }); + await content.writeStreamfile(bashrcFile, bashrcContent.join("\n")); + } + } + catch (error) { + updateBash = false; + updateBashMsg = error; } - async validateLibraryList(defaultUserLibraries: string[], libraryList: string[]): Promise<{ validLibs: string[], badLibs: string[] }> { + return Promise.resolve({ updateBash, updateBashMsg }); + } - let validLibs: string[] = []; - let badLibs: string[] = []; + async validateLibraryList(defaultUserLibraries: string[], libraryList: string[]): Promise<{ validLibs: string[], badLibs: string[] }> { - const result = await this.connection.sendQsh({ - command: [ - `liblist -d ` + defaultUserLibraries.join(` `).replace(/\$/g, `\\$`), - ...libraryList.map(lib => `liblist -a ` + lib.replace(/\$/g, `\\$`)) - ].join(`; `) - }); + let validLibs: string[] = []; + let badLibs: string[] = []; - if (result.stderr) { - const lines = result.stderr.split(`\n`); + const result = await this.connection.sendQsh({ + command: [ + `liblist -d ` + defaultUserLibraries.join(` `).replace(/\$/g, `\\$`), + ...libraryList.map(lib => `liblist -a ` + lib.replace(/\$/g, `\\$`)) + ].join(`; `) + }); - lines.forEach(line => { - const badLib = libraryList.find(lib => line.includes(`ibrary ${lib} `)); + if (result.stderr) { + const lines = result.stderr.split(`\n`); - // If there is an error about the library, store it - if (badLib) badLibs.push(badLib); - }); - } + lines.forEach(line => { + const badLib = libraryList.find(lib => line.includes(`ibrary ${lib} `)); - if (result && badLibs.length > 0) { - validLibs = libraryList.filter(lib => !badLibs.includes(lib)); - } - - return Promise.resolve({ validLibs, badLibs }); + // If there is an error about the library, store it + if (badLib) badLibs.push(badLib); + }); + } + if (result && badLibs.length > 0) { + validLibs = libraryList.filter(lib => !badLibs.includes(lib)); } + return Promise.resolve({ validLibs, badLibs }); + + } } \ No newline at end of file