18.4.2025
While working on enabling ZOWE Explorer to run in VS Code WEB:
Cannot read properties of undefined (reading 'ISession')
because the catch swallowed the exception and this.session
had never been assignedactivate
SharedTreeProviders.initializeProviders
DatasetInit.initDatasetProvider(context)
DatasetFSProvider__WEBPACK_IMPORTED_MODULE_3_.DatasetFSProvider.instance.root.entries
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
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
${layer.path}:${type}
, schema);(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
}
};
imperative.IProfileLoaded .profile: imperative.IProfile = {[string]: any}
BaseProvider types/datasets.ts DsEntryMetadata
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
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);
}
In summary this module is ripe for a rewrite with proper state management (and arguably with API update).
Multiple copies of profiles which get out of sync with each other
For example:
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);
There is no loading going on for loadNamedProfile
. It should be renamed got getProfileByName
.
Instead it uses const cachedProfile = Profiles.getInstance().loadNamedProfile(this.getProfileName());
ZoweDatasetNode
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.
Why creating new profiles cache rather than using the one that is accessible as
?
Number of things could be improved here:
This should be moved at beginning of the function
if (options.profile == null && options.sessionName == null) {
return undefined;
}
It is a common idiom:
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!
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.
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:
mProfileInfo
property! This should be contorled by the cache, not external users and especially not on get
calls!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
get
request causes not one but three mutations of the underlying object (one in the ProfilesCache implementation that is called by the super
call, and two in the code above)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.
const loadProfile = options.sessionName ? await cache.getLoadedProfConfig(options.sessionName)
if (loadProfile == null)
// return undefined;
return;
}
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 …
yet there is code that updates “both”
loadProfile.profile.user = loadSession.user = creds[0];
loadProfile.profile.password = loadSession.password = creds[1];
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.
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 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.
Not available to extenders yet (Slavek mentioned on 25.4.2025)
This class does not need to be a class at all. It should be just a module that exports a set of functions.
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
Zowe Explorer just installed in clean VS Code
Team config with a service and without credentials
List a PDS dataset
List members works
Opening a member for edit fails with wrong credentials
After reloading VS Code the problem disappears
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.
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.
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:
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