18.4.2025

While working on enabling ZOWE Explorer to run in VS Code WEB:

TODO

Debugging notes

ZoweTreeProvider(profileType ) .loadProfileByPersistedProfile DatasetTree .addDefaultSession(profileType) - profile = Profiles.getInstance().getDefaultProfile(profileType); .addSingleSession(profile) ZoweDatasetNode .constructor({ profile, … }) // profile.name === ‘zosmf’ - url = zowe-ds:/${profile.name}; DatasetFSProvider .createDirectory(‘zowe-ds:/zosmf/’) ._getInfoFromUri(‘zowe-ds:/zosmf/’) - new DsEntryMetadata({profile, path}) FsAbstractUtils .getInfoForUri(‘zowe-ds:/zosmf/’) ProfilesCache .loadNamedProfile(‘zosmf’)

Metadata (variables called profInfo)

ZoweDatasetNode .contextValue “session_type=zosmf_home” .profile .name ‘zosmf’ .profile {profile with updated credentials}

src/configuration/Profiles.ts Profiles extends ProfilesCache

Where are profiles stored?

// Profiles (ProfilesCache) - [singleton]
temp_profilescache_profile = __webpack_module_cache__['./src/configuration/Profiles.ts'].exports.Profiles.getInstance().getProfiles('zosmf')[0].profile
temp_profilescache_profile = __webpack_module_cache__['./src/configuration/Profiles.ts'].exports.Profiles.getInstance().loadNamedProfile('zosmf').profile

// DatasetTree node [SharedTreeProvdier singleton]
temp_node_profile = __webpack_module_cache__['./src/trees/shared/SharedTreeProviders.ts'].exports.SharedTreeProviders.providers.ds.mSessionNodes.find(node=>node.label === 'zosmf').profile.profile

// DatasetFSProvider (FS entry) - [singleton] starting with this.root
temp_fsentry_profile = __webpack_module_cache__['./src/trees/dataset/DatasetFSProvider.ts'].exports.DatasetFSProvider.instance.root.entries.get('zosmf').metadata.profile.profile

// Before refresh
temp_node_profile === temp_profilescache_profile
// After refresh
temp_node_profile === temp_fsentry_profile


// from updateCredentials
            // to see where else the profile (options.session) we are checking is stored:
            //   options.session === __webpack_module_cache__['./src/configuration/Profiles.ts'].exports.Profiles.getInstance().loadNamedProfile('zosmf').profile
            //     if true then the profile is stored in ProfilesCache.allProfiles
            //   options.session === __webpack_module_cache__['./src/configuration/Profiles.ts'].exports.Profiles.getInstance().profilesByType.get('zosmf')[0].profile
            //     if true then the profile is stored in ProfilesCache.profilesByType
            //   options.session === __webpack_module_cache__['./src/configuration/Profiles.ts'].exports.Profiles.getInstance().getDefaultProfile().profile
            //     if true then the profile is stored in ProfilesCache.defaultProfileByType
            //   options.session === __webpack_module_cache__['./src/trees/shared/SharedTreeProviders.ts'].exports.SharedTreeProviders.providers.ds.mSessionNodes.find(node=>node.label === 'zosmf').profile.profile
            //     if true then the profile is stored in DatasetTree

// Updating credentials Profiles.promptCredentials ZoweVsCodeExtension.updateCredentials ProfileInfo (imperative) .updateProperty ProfilesCache .updateCachedProfile

Proposed profile replacement structure:

connectionInfo

credentials

ZE

ZE API

    public async fetchAllProfilesByType(type: string): Promise<imperative.IProfileLoaded[]> {
        const profByType: imperative.IProfileLoaded[] = [];
        const mProfileInfo = await this.getProfileInfo();
        const profilesForType = mProfileInfo.getAllProfiles(type);
        if (profilesForType && profilesForType.length > 0) {
            for (const prof of profilesForType) {
(new profile object) const profAttr = this.getMergedAttrs(mProfileInfo, prof); // ProfilesCache.getMergedAttrs creates profile = {}.
                let profile = this.getProfileLoaded(prof.profName, prof.profType, profAttr);
                profile = this.checkMergingConfigSingleProfile(profile);
                profByType.push(profile);
            }
        }
        return profByType;
    }

Imperative

(new object) const profAttrs: IProfAttrs = {
                    profName: prof,
                    profType: teamConfigProfs[prof].type,
                    isDefaultProfile: this.isDefaultTeamProfile(prof, profileType),
                    profLoc: {
                        locType: ProfLocType.TEAM_CONFIG,
                        osLoc: teamOsLocation,
                        jsonLoc: jsonLocation
                    }
                };

Where are profiles stored

imperative.IProfileLoaded .profile: imperative.IProfile = {[string]: any}

FS Provider

BaseProvider types/datasets.ts DsEntryMetadata

Profiles on disk

ZE ProfileUtils.readConfigFromDisk

DatasetTree extends ZoweTreeProvider ZoweTreeProvider this.mHistory = new ZowePersistentFilters(this.persistenceSchema); // “zowe.ds.history” ZowePersistentFilters .mSessions = [‘zosmf’]

ZoweTreeProvider .addSession .loadProfileByPersistedProfile DatasetTree.getSessions() ZowePersistentFilters.getSessions() .refresh

package.json “zowe.ds.history”.persistence

ZowePersistentFilters Reading ‘zowe’ sesison from ZoweLocalStorage

========== initialize @ ZowePersistentFilters.ts:288 ZowePersistentFilters @ ZowePersistentFilters.ts:44 ZoweTreeProvider @ ZoweTreeProvider.ts:38 DatasetTree @ DatasetTree.ts:71 createDatasetTree @ DatasetInit.ts:28 initDatasetProvider @ DatasetInit.ts:43 ds @ extension.ts:46 initializeProviders @ SharedTreeProviders.ts:25 activate @ extension.ts:44

    private async loadProfileByPersistedProfile(
        treeProvider: IZoweTree<IZoweTreeNode>,
        profileType: string,
        isUsingAutomaticProfileValidation: boolean
    ): Promise<void> {
        // debugger; // are profiles really persisted, or do they just mean zowe.config that would reside on disk?
        const profiles: imperative.IProfileLoaded[] = profileType
            ? await Profiles.getInstance().fetchAllProfilesByType(profileType)
            : await Profiles.getInstance().fetchAllProfiles();
        for (const profile of profiles) {
            const existingSessionNode = treeProvider.mSessionNodes.find((node) => node.label.toString().trim() === profile.name);
            const sessionInHistory = treeProvider.getSessions().some((session) => session?.trim() === profile.name);
            if (!existingSessionNode && sessionInHistory) {
                await treeProvider.addSingleSession(profile);
                for (const node of treeProvider.mSessionNodes) {
                    if (node.label !== vscode.l10n.t("Favorites") && node.getProfileName() === profile.name) {
                        SharedActions.resetValidationSettings(node, isUsingAutomaticProfileValidation);
                        break;
                    }
                }
            }
        }
        await TreeViewUtils.addDefaultSession(treeProvider, profileType);
    }

Profile usage lifecycle

ProfilesCache

In summary this module is ripe for a rewrite with proper state management (and arguably with API update).

State management

Multiple copies of profiles which get out of sync with each other

For example:

API

fetch…

fetchAllProfilesByType and fetchAllProfiles should be replaced with a single method fetchProfiles(type?) with optional argument type.

This will allow replacing the following idiom in consumer code

        const profiles: imperative.IProfileLoaded[] = profileType
            ? await Profiles.getInstance().fetchAllProfilesByType(profileType)
            : await Profiles.getInstance().fetchAllProfiles();

with

  const profiles = await Profiles.getInstance().fetchProfiles(profileType);

load… and get…

There is no loading going on for loadNamedProfile. It should be renamed got getProfileByName.

ZoweDatasetNode

Has .profile but does not use it

Instead it uses const cachedProfile = Profiles.getInstance().loadNamedProfile(this.getProfileName());

ZoweDatasetNode

getChildren

Calls await vscode.workspace.fs.writeFile(dsNode.resourceUri, new Uint8Array()); to create fs nodes for PDS members

Instead it should do a list request and the FS provider should fetch the data and return it.

ZoweVsCodeExtension

new ProfilesCache(imperative.Logger.getAppLogger(), workspacePath);

Why creating new profiles cache rather than using the one that is accessible as

?

updateCredentials

Number of things could be improved here:

Do I have what I need?

This should be moved at beginning of the function

        if (options.profile == null && options.sessionName == null) {
            return undefined;
        }

It is a common idiom:

options.zeProfiles ??

const cache = options.zeProfiles ?? ZoweVsCodeExtension.profilesCache;

Why are profiles being passed in as an option and what is being passed in?

The call to updateCredentials comes from Profiles.promptCredentials, and it is passing itself as options.zeProfiles.

Why is the job of updating credentials delegated to ZoweVsCodeExtension module? Profiles should be the gateway, manager and single source of truth for profiles!

.profilesCache

Why does ZoweVsCodeExtension have .profilesCache? The single source of truth sould, again, be Profiles!

And why does the code check for one profile cache and if it does not exist, uses the other?

What if the other does not exist? Wait it does exist. And each time you access it is different. Really? Yes, it is a getter that creates a new (throwaway) instance of ProfilesCache every time it is accessed.

    public static get profilesCache(): ProfilesCache {
        const workspacePath = this.workspaceRoot?.uri.fsPath;
        return new ProfilesCache(imperative.Logger.getAppLogger(), workspacePath);
    }

An interesting property then holds

(ZoweVsCodeExtension.profilesCache == ZoweVsCodeExtension.profilesCache) === false

It is beneficial to possess certainties in life upon which one can depend.

profInfo

        const profInfo = await cache.getProfileInfo();

Isn’t ProfileInfo meant to be an implementation detail hidden behind Profiles (ProfilesCache)? Of course!

If we ignore that, there is one more issue here. What is (or should be) the semantics of a function called get...? It should be referentially transparent. Or in other words it should have no side effectts. Lets take a look:

    public async getProfileInfo(_envTheia = false): Promise<imperative.ProfileInfo> {
        const mProfileInfo = new imperative.ProfileInfo("zowe", {
            // eslint-disable-next-line @typescript-eslint/no-unsafe-return
            // credMgrOverride: imperative.ProfileCredentials.defaultCredMgrWithKeytar(ProfilesCache.requireKeyring),
        });
        await mProfileInfo.readProfilesFromDisk({ homeDir: FileManagement.getZoweDir(), projectDir: this.cwd ?? undefined });
        return mProfileInfo;
    }

There are two side effects here:

There is one more compliaction here. Profiles extends ProfileCache and it overrides the getProfileInfo. So in case we get Profiles instance rather than ProfileInfo instance, this is what getProfileInfo looks like:

    public async getProfileInfo(): Promise<imperative.ProfileInfo> {
        ZoweLogger.trace("Profiles.getProfileInfo called.");
        if (this.mProfileInfo == null) {
            this.overrideWithEnv = SettingsConfig.getDirectValue(Constants.SETTINGS_OVERRIDE_WITH_ENV_VAR) ?? false;
            this.mProfileInfo = await super.getProfileInfo();
            // Cache profile info object until current thread is done executing
            setImmediate(() => (this.mProfileInfo = null));
        }
        return this.mProfileInfo;
    }

It also has the side effect issues described above. And it adds more issues with setImmediate

options.secure

The next line says

const setSecure = options.secure ?? profInfo.isSecured();

which, again, violates the ownership of profiles cache by Profiles. Why should an external caller have an authority to dictate or decide whether to use secure fields? That should be in the purview of Profiles module.

getLoadedProfConfig

const loadProfile = options.sessionName ? await cache.getLoadedProfConfig(options.sessionName) 

loadProfile check

        if (loadProfile == null)
            // return undefined;
          return;
        }

promptUserPass

const creds = await ZoweVsCodeExtension.promptUserPass({ session: loadSession, ...options });

look inside and you find out that it does not only prompt, it also mutates the profile that is passed to it (and it calls. it ).

And then there is another code that uplates the profile with the hew credentials …

loadProfile.profile and loadSession are the same

yet there is code that updates “both”

            loadProfile.profile.user = loadSession.user = creds[0];
            loadProfile.profile.password = loadSession.password = creds[1];

profileInfo.updatePropery

An in addition to that it is followed by code that (based on a user input) goes directly to profileInfo and modifies its Config class’s layer with the values.

                await profInfo.updateProperty({ ...upd, property: "user", value: creds[0], setSecure });
                await profInfo.updateProperty({ ...upd, property: "password", value: creds[1], setSecure });

Again it would seem more reasonable to use “Profiles” as the gateway to updating anything and everything.

cache.updateCachedProfile

And at the end we have one more call to update a profile.

   await cache.updateCachedProfile(loadProfile, undefined, apiRegister);

which is the proper path (provied cache is actually Profiles singleton), but internally it calls refresh. Which blows up and then rebuilds it

When the cache update is calles this is most likely the place, where a tree node is meant to be updated, but it is not, because the function is called with undefined.

The name and return type of the updateCredentials

The name suggest this function performs certain operation. That is update credentials. So one would expect it operates by a side effect (of modifying credentials). That would imply the return type should be void.

But the return type is imperative.IProfileLoaded. Why? So that the return value could then be used to extract the username and password

But it also has profile undefined why? No strict type checking. And only god know how it will be used (or explode).

So why is the return type imperative.IProfileLoaded? So that the caller could extract the username and password from it.

ssoLogin

Not available to extenders yet (Slavek mentioned on 25.4.2025)

DatasetInit

This class does not need to be a class at all. It should be just a module that exports a set of functions.

TreeViewUtils.addDefaultSession(IZoweTree, string)

Zowe Explorer API package

The API package should just be a set of typescript declarations over the API ZOWE Explore exposes to other extensions in VS Code, not a complete copy of CLI. But that is not the focus here, we will discuss it elsewhere. Back to the code. What does

Root cause analysis

Issue description

Initialization

When the extension is activated it creates an instance of Profiles(Cache).

Just a moment later DatasetInit.initDatasetProvider(context) is called. It creates a DatasetTree and calls addSession() on it. Because the session list is empty (we are starting from scratch), loadProfileByPersistedProfile is called. Here Profiles.getInstance().fetchAllProfiles() is called because the addSession call above did not provide a profile type.

Cache inconsistency

fetchAllProfiles reads all profiles replaces the contents of .allProfiles, but not .profilesByType .defaultProfileByType effectively setting the cache into an inconsistent state where different object reside in .allProviles vs in .profilesByType and .defaultProfileByType. This will later demonstrate itself as a problem.

Let’s call the objects that now reside in .profilesByType and .defaultProfileByType the original profile objects, and the ones residing in .allProfiles the all profiles objects.

There is also a potential for a race condition in case the contents of team config changes in between the Profiles initialization and the call to fetchAllProfiles. This is of course unlikely but not impossible and in case it would happen it is hard to imagine how it could be reproduced and diagnosed.

Profiles in Dataset Tree nodes and in File System nodes

When control returns back to loadProfileByPersistedProfile, the for loop is skipped because we are starting from scratch and there is no session history.

Then TreeViewUtils.addDefaultSession() is called and it gets profile from Profiles.getInstance().getDefaultProfile(). It is called without specifying a profile type which results in a default of zosfm being used and the profile is retrieved from Profiles.defaultProfileByType so it is an original profile object.

This original profile object is used to constuct the dataset tree node.

However, during the process of building the dataset tree node a (root) DatasetFSProvider node is created with the all profiles profile object. Why all profiles istead of original profile? Because the original profile node is used to create a uri which is then passed to the DatasetFSProvider.instance.createDirectory(this.resourceUri) call and that in turn calls _getInfoFromUri(uri) because at this point in time, the root node does not exist yet. This leads to a call to FsAbstractUtils.getInfoForUri(uri, Profiles.getInstance()) which in turn calls Profiles.loadNamedProfile(profilename).

The loadNamedProfile method returns a all profiles profile object. So that is why the DatasetFSProvider nodes receives an all profiles object.

Here is the call stack (upside down)

- activate (extension.ts)
- SharedTreeProviders.initializeProviders
- DatasetInit.initDatasetProvider(context)
  - DatasetFSProvider.instance
  - vscode.workspace.registerFileSystemProvider
  - DatasetInit.createDatasetTree
    - tree.addSession()
      - loadProfileByPersistedProfile
        - TreeViewUtils.addDefaultSession
          - DatasetTree.addSingleSession
            - new ZoweDatasetNode
              - DatasetFSProvider.instance.createDirectory
                - DatasetFSProvider._getInfoFromUri
                  - FsAbstractUtils.getInfoForUri(uri, Profiles.getInstance())
                    - Profiles.loadNamedProfile(profilename)

To summarize:

Updating credentials

Because in my scenario I do not have credentials in my profile (which is the way it should be), I am prompted, this is the call stack (from console.trace)

updateCredentials	@	ZoweVsCodeExtension.ts:84
await in updateCredentials		
promptCredentials	@	Profiles.ts:608
await in promptCredentials		
checkCurrentProfile	@	Profiles.ts:136
await in checkCurrentProfile		
checkCurrentProfile	@	ZoweTreeProvider.ts:270
datasetFilterPrompt	@	DatasetTree.ts:1001
filterPrompt	@	DatasetTree.ts:138

the bottom line is that updateCredentials is called. Then it calls ZoweVsCodeExtension.promptUserPass which prompts for username and password but that is not all.

It also goes in and tries to update the credentials in the profile. But which copy of the profile? Well it is a copy that has been stored in the DatasetTree node during the default session creation and it is the original profile object.

Then it returns the credentials back to updateCredentials function which also updates the credentials directly in the (same) profile. It tries it twice, under loadProfile.profile and loadSession. They are both the same original profile object.

Then, after a confirmation that the credentials should be saved, it goes one step further. It bypases Profiles, goes directly to ProfileInfo and updates the config. That results in changes both in memory and on disk.

Finally updateCache is called which resutls in cache refresh. During this refresh the .allProfiles profiles object are updated with new fields from disk. But .profilesByType objects are replaced with the corresponding objects from .allProfiles. This would have been fine if the cache was internally consitent. But as we have shown before it was not. So during this operation the original profile objects get replaced with a version of .allProfiles from the last fetchAllProfiles call and updated by credentials.

That completes the update code. This is the resulting state

Questions