Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor component management and add state caching #2502

Merged
merged 6 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 99 additions & 73 deletions src/api/IBMi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,6 @@ export default class IBMi {

this.appendOutput(`Code for IBM i, version ${currentExtensionVersion}\n\n`);

let tempLibrarySet = false;

callbacks.progress({
message: `Loading configuration.`
});
Expand Down Expand Up @@ -428,10 +426,17 @@ export default class IBMi {
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`);
[
this.remoteFeatures.jdk80,
this.remoteFeatures.jdk11,
this.remoteFeatures.openjdk11,
this.remoteFeatures.jdk17
] = await Promise.all([
javaCheck(`/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit`),
javaCheck(`/QOpenSys/QIBM/ProdData/JavaVM/jdk11/64bit`),
javaCheck(`/QOpensys/pkgs/lib/jvm/openjdk-11`),
javaCheck(`/QOpenSys/QIBM/ProdData/JavaVM/jdk17/64bit`)
]);
}

if (this.remoteFeatures.uname) {
Expand All @@ -453,9 +458,10 @@ export default class IBMi {
}

callbacks.progress({ message: `Checking Code for IBM i components.` });
await this.componentManager.startup();

const componentStates = await this.componentManager.getState();
await this.componentManager.startup(quickConnect() ? cachedServerSettings?.installedComponents : []);

const componentStates = await this.componentManager.getInstallState();
this.appendOutput(`\nCode for IBM i components:\n`);
for (const state of componentStates) {
this.appendOutput(`\t${state.id.name} (${state.id.version}): ${state.state}\n`);
Expand Down Expand Up @@ -506,64 +512,14 @@ export default class IBMi {
}

callbacks.progress({
message: `Checking temporary library configuration.`
});

//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 = 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;
}
}
}

callbacks.progress({
message: `Checking temporary directory configuration.`
message: `Checking temporary directory and temporary library 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
}
}
const [tempLibrarySet, tempDirSet] = await Promise.all([
this.ensureTempLibraryExists(currentLibrary),
this.ensureTempDirectory()
]);

if (!tempDirSet) {
this.config.tempDir = `/tmp`;
Expand Down Expand Up @@ -618,20 +574,21 @@ 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, QCPFRMIMPF] = await Promise.all([
this.runCommand({
command: `CHKOBJ OBJ(QSYS/QCPTOIMPF) OBJTYPE(*DTAARA)`,
noLibList: true
}),
this.runCommand({
command: `CHKOBJ OBJ(QSYS/QCPFRMIMPF) OBJTYPE(*DTAARA)`,
noLibList: true
})
]);

if (QCPTOIMPF?.code === 0) {
callbacks.uiErrorHandler(this, `QCPTOIMPF_exists`);
}

const QCPFRMIMPF = await this.runCommand({
command: `CHKOBJ OBJ(QSYS/QCPFRMIMPF) OBJTYPE(*DTAARA)`,
noLibList: true
});

if (QCPFRMIMPF?.code === 0) {
callbacks.uiErrorHandler(this, `QCPFRMIMPF_exists`);
}
Expand Down Expand Up @@ -939,6 +896,7 @@ export default class IBMi {
qccsid: this.qccsid,
jobCcsid: this.userJobCcsid,
remoteFeatures: this.remoteFeatures,
installedComponents: componentStates,
remoteFeaturesKeys: Object.keys(this.remoteFeatures).sort().toString(),
badDataAreasChecked: true,
libraryListValidated: true,
Expand Down Expand Up @@ -975,6 +933,74 @@ export default class IBMi {
}
}

private async ensureTempLibraryExists(fallbackTempLib: string) {
let tempLibrarySet: boolean = false;

if (!this.config) {
return false;
}

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 = 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 (fallbackTempLib && !fallbackTempLib.startsWith(`Q`)) {
//Using ${currentLibrary} as the temporary library for temporary data.
this.config.tempLibrary = fallbackTempLib;
tempLibrarySet = true;
}
}
}

return tempLibrarySet;
}

private async ensureTempDirectory() {
let tempDirSet: boolean = false;

if (!this.config) {
return false;
}

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
}
}
return tempDirSet;
}

/**
* Can return 0 if the OS version was not detected.
*/
Expand Down Expand Up @@ -1260,7 +1286,7 @@ export default class IBMi {
}

getComponentStates() {
return this.componentManager.getState();
return this.componentManager.getInstallState();
}

/**
Expand Down
7 changes: 7 additions & 0 deletions src/api/components/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export type ComponentIdentification = {
version: number
}

export type ComponentInstallState = {
id: ComponentIdentification
state: ComponentState
}

/**
* Defines a component that is managed per IBM i.
*
Expand Down Expand Up @@ -38,6 +43,8 @@ export type IBMiComponent = {
*/
getIdentification(): ComponentIdentification;

setInstallDirectory?(installDirectory: string): Promise<void>;

/**
* @returns the component's {@link ComponentState state} on the IBM i
*/
Expand Down
4 changes: 4 additions & 0 deletions src/api/components/cqsh/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export class CustomQSh implements IBMiComponent {
return `${id.name}_${id.version}`;
}

async setInstallDirectory(installDirectory: string): Promise<void> {
this.installPath = path.posix.join(installDirectory, this.getFileName());
}

async getRemoteState(connection: IBMi, installDirectory: string): Promise<ComponentState> {
this.installPath = path.posix.join(installDirectory, this.getFileName());
const result = await connection.content.testStreamFile(this.installPath, "x");
Expand Down
3 changes: 2 additions & 1 deletion src/api/components/getMemberInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class GetMemberInfo implements IBMiComponent {
}

getIdentification() {
return { name: GetMemberInfo.ID, version: this.installedVersion };
return { name: GetMemberInfo.ID, version: this.currentVersion };
}

async getRemoteState(connection: IBMi): Promise<ComponentState> {
Expand Down Expand Up @@ -47,6 +47,7 @@ export class GetMemberInfo implements IBMiComponent {
if (result.code) {
return `Error`;
} else {
this.installedVersion = this.currentVersion;
return `Installed`;
}
});
Expand Down
3 changes: 2 additions & 1 deletion src/api/components/getNewLibl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class GetNewLibl implements IBMiComponent {
}

getIdentification() {
return { name: GetNewLibl.ID, version: this.installedVersion };
return { name: GetNewLibl.ID, version: this.currentVersion };
}

async getRemoteState(connection: IBMi): Promise<ComponentState> {
Expand Down Expand Up @@ -45,6 +45,7 @@ export class GetNewLibl implements IBMiComponent {
});

if (!result.code) {
this.installedVersion = this.currentVersion;
return `Installed`;
} else {
return `Error`;
Expand Down
29 changes: 24 additions & 5 deletions src/api/components/manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import IBMi from "../IBMi";
import { ComponentState, IBMiComponent } from "./component";
import { ComponentIdentification, ComponentInstallState, ComponentState, IBMiComponent } from "./component";

interface ExtensionContextI {
extension: {
Expand Down Expand Up @@ -37,11 +37,13 @@ export const extensionComponentRegistry = new ComponentRegistry();
export class ComponentManager {
private readonly registered: Map<string, IBMiComponentRuntime> = new Map;

constructor(private readonly connection: IBMi) {
constructor(private readonly connection: IBMi) {}

public getComponentIds(): ComponentIdentification[] {
return Array.from(extensionComponentRegistry.getComponents().values()).flatMap(a => a.flat()).map(c => c.getIdentification());
}

public getState() {
public getInstallState(): ComponentInstallState[] {
return Array.from(this.registered.keys()).map(k => {
const comp = this.registered.get(k)!;
return {
Expand All @@ -51,11 +53,22 @@ export class ComponentManager {
});
}

public async startup() {
public async startup(lastInstalled: ComponentInstallState[] = []) {
const components = Array.from(extensionComponentRegistry.getComponents().values()).flatMap(a => a.flat());
for (const component of components) {
await component.reset?.();
this.registered.set(component.getIdentification().name, await new IBMiComponentRuntime(this.connection, component).check());
const newComponent = new IBMiComponentRuntime(this.connection, component);

const installed = lastInstalled.find(i => i.id.name === component.getIdentification().name);
const sameVersion = installed && (installed.id.version === component.getIdentification().version);

if (!installed || !sameVersion) {
await newComponent.check();
} else {
newComponent.overrideState(installed.state);
}

this.registered.set(component.getIdentification().name, newComponent);
}
}

Expand Down Expand Up @@ -92,6 +105,12 @@ class IBMiComponentRuntime {
return this.state;
}

async overrideState(newState: ComponentState) {
const installDir = await this.getInstallDirectory();
await this.component.setInstallDirectory?.(installDir);
this.state = newState;
}

async check() {
try {
const installDirectory = await this.getInstallDirectory();
Expand Down
2 changes: 2 additions & 0 deletions src/api/configuration/storage/CodeForIStorage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ComponentInstallState } from "../../components/component";
import { ConnectionData } from "../../types";
import { BaseStorage } from "./BaseStorage";
const SERVER_SETTINGS_CACHE_PREFIX = `serverSettingsCache_`;
Expand All @@ -20,6 +21,7 @@ export type CachedServerSettings = {
qccsid: number | null;
jobCcsid: number | null
remoteFeatures: { [name: string]: string | undefined }
installedComponents: ComponentInstallState[],
remoteFeaturesKeys: string | null
badDataAreasChecked: boolean | null
libraryListValidated: boolean | null
Expand Down
Loading