Skip to content

Commit 3859d14

Browse files
authored
Merge pull request #3071 from codefori/cleanDisconnect
Dispose SSH connection when disconnecting & fixed disconnection events handling
2 parents 432a11a + 09fc3bf commit 3859d14

File tree

9 files changed

+142
-109
lines changed

9 files changed

+142
-109
lines changed

package-lock.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3247,6 +3247,7 @@
32473247
"@types/glob": "^7.1.3",
32483248
"@types/node": "22.19.7",
32493249
"@types/source-map-support": "^0.5.6",
3250+
"@types/ssh2": "^1.15.5",
32503251
"@types/tar": "6.1.13",
32513252
"@types/tmp": "0.2.5",
32523253
"@types/vscode": "^1.90.0",
@@ -3281,4 +3282,4 @@
32813282
"halcyontechltd.vscode-ibmi-walkthroughs",
32823283
"vscode.git"
32833284
]
3284-
}
3285+
}

src/Instance.ts

Lines changed: 46 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { EventEmitter } from "stream";
22
import * as vscode from "vscode";
33
import { ILELibrarySettings } from "./api/CompileTools";
4-
import IBMi, { ConnectionResult } from "./api/IBMi";
4+
import IBMi, { ConnectionResult, DisconnectedCallback } from "./api/IBMi";
55
import { BaseStorage } from "./api/configuration/storage/BaseStorage";
66
import { CodeForIStorage } from "./api/configuration/storage/CodeForIStorage";
77
import { ConnectionStorage } from "./api/configuration/storage/ConnectionStorage";
@@ -79,70 +79,71 @@ export default class Instance {
7979

8080
let result: ConnectionResult;
8181

82-
const timeoutHandler = async (conn: IBMi) => {
83-
if (conn) {
84-
const choice = await vscode.window.showWarningMessage(`Connection lost`, {
85-
modal: true,
86-
detail: `Connection to ${conn.currentConnectionName} has dropped. Would you like to reconnect?`
87-
}, `Yes`, `No, get logs`);
82+
const onDisconnected: DisconnectedCallback = async (connection, error) => {
83+
if (connection.connectionSuccessful) {
84+
this.fire(`disconnected`);
8885

89-
let reconnect = choice === `Yes`;
90-
let collectLogs = choice === `No, get logs`;
86+
if (error) {
87+
const choice = await vscode.window.showWarningMessage(`Connection lost: ${error.description || error.message || error.level}`, {
88+
modal: true,
89+
detail: `Connection to ${connection.currentConnectionName} has dropped. Would you like to reconnect?`
90+
}, `Yes`, `No, get logs`);
9191

92-
if (collectLogs) {
93-
const logs = this.output.content;
94-
vscode.workspace.openTextDocument({ content: logs, language: `plaintext` }).then(doc => {
95-
vscode.window.showTextDocument(doc);
96-
});
97-
}
92+
const reconnect = choice === `Yes`;
93+
const collectLogs = choice === `No, get logs`;
9894

99-
this.disconnect();
95+
if (collectLogs) {
96+
const logs = this.output.content;
97+
vscode.workspace.openTextDocument({ content: logs, language: `plaintext` }).then(doc => {
98+
vscode.window.showTextDocument(doc);
99+
});
100+
}
101+
102+
this.disconnect();
100103

101-
if (reconnect) {
102-
await this.connect({ ...options, reconnecting: true });
104+
if (reconnect) {
105+
await this.connect({ ...options, reconnecting: true });
106+
}
103107
}
104108
}
109+
else {
110+
this.disconnect();
111+
}
105112
};
106113

107114
return VscodeTools.withContext("code-for-ibmi:connecting", async () => {
108115
while (true) {
109116
let customError: string | undefined;
110117
await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: options.data.name, cancellable: true }, async (p, cancelToken) => {
111-
try {
112-
const cancelEmitter = new EventEmitter();
118+
const cancelEmitter = new EventEmitter();
113119

114-
cancelToken.onCancellationRequested(() => {
115-
cancelEmitter.emit(`cancel`);
116-
});
120+
cancelToken.onCancellationRequested(() => {
121+
cancelEmitter.emit(`cancel`);
122+
});
117123

118-
result = await connection.connect(
119-
options.data,
120-
{
121-
callbacks: {
122-
timeoutCallback: timeoutHandler,
123-
onConnectedOperations: options.onConnectedOperations || [],
124-
uiErrorHandler: handleConnectionResults,
125-
progress: (message) => { p.report(message) },
126-
message: messageCallback,
127-
inputBox: async (prompt: string, placeHolder: string, ignoreFocusOut: boolean) => {
128-
return await inputBoxCallback(prompt, placeHolder, ignoreFocusOut, cancelToken)
129-
},
130-
cancelEmitter
124+
result = await connection.connect(
125+
options.data,
126+
{
127+
callbacks: {
128+
onDisconnected,
129+
onConnectedOperations: options.onConnectedOperations,
130+
uiErrorHandler: handleConnectionResults,
131+
progress: (message) => { p.report(message) },
132+
message: messageCallback,
133+
inputBox: async (prompt: string, placeHolder: string, ignoreFocusOut: boolean) => {
134+
return await inputBoxCallback(prompt, placeHolder, ignoreFocusOut, cancelToken)
131135
},
132-
reconnecting: options.reconnecting,
133-
reloadServerSettings: options.reloadServerSettings,
136+
cancelEmitter
134137
},
135-
);
136-
} catch (e: any) {
137-
customError = e.message;
138-
result = { success: false };
139-
}
138+
reconnecting: options.reconnecting,
139+
reloadServerSettings: options.reloadServerSettings,
140+
},
141+
)
140142
});
141143

142144
if (result.success) {
143145
await this.setConnection(connection);
144146
break;
145-
146147
} else {
147148
await this.disconnect();
148149
if (options.reconnecting && await vscode.window.showWarningMessage(`Could not reconnect`, {
@@ -159,10 +160,6 @@ export default class Instance {
159160
}
160161
}
161162

162-
if (result.success === false) {
163-
connection.dispose();
164-
}
165-
166163
return result;
167164
});
168165
}
@@ -179,15 +176,10 @@ export default class Instance {
179176

180177
private async setConnection(connection?: IBMi) {
181178
if (this.connection) {
182-
await this.connection.dispose();
179+
this.connection.disconnect();
183180
}
184181

185182
if (connection) {
186-
connection.setDisconnectedCallback(async () => {
187-
this.setConnection();
188-
this.fire(`disconnected`);
189-
});
190-
191183
this.connection = connection;
192184
this.storage.setConnectionName(connection.currentConnectionName);
193185
await IBMi.GlobalStorage.setLastConnection(connection.currentConnectionName);

src/api/CompileTools.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export namespace CompileTools {
2525
updateProgress?: (message: string) => void
2626
}
2727

28+
export function reset(){
29+
ileQueue.clear();
30+
jobLogOrdinal = 0;
31+
}
32+
2833
/**
2934
* Execute a command
3035
*/

src/api/IBMi.ts

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BindingValue } from "@ibm/mapepire-js";
22
import * as node_ssh from "node-ssh";
33
import path, { parse as parsePath } from 'path';
4+
import { ClientErrorExtensions } from "ssh2";
45
import { EventEmitter } from 'stream';
56
import { CompileTools } from "./CompileTools";
67
import IBMiContent from "./IBMiContent";
@@ -30,6 +31,8 @@ export interface ConnectionResult {
3031
errorCodes?: ConnectionErrorCode[]
3132
}
3233

34+
export type DisconnectedCallback = (conn: IBMi, error?: Error & ClientErrorExtensions) => Promise<void>;
35+
3336
const remoteApps = [ // All names MUST also be defined as key in 'remoteFeatures' below!!
3437
{
3538
path: `/usr/bin/`,
@@ -46,11 +49,9 @@ const remoteApps = [ // All names MUST also be defined as key in 'remoteFeatures
4649
}
4750
];
4851

49-
type DisconnectCallback = (conn: IBMi) => Promise<void>;
50-
5152
interface ConnectionCallbacks {
5253
onConnectedOperations?: Function[],
53-
timeoutCallback?: (conn: IBMi) => Promise<void>,
54+
onDisconnected?: DisconnectedCallback,
5455
uiErrorHandler: (connection: IBMi, error: ConnectionErrorCode, data?: any) => Promise<boolean>,
5556
progress: (detail: { message: string }) => void,
5657
message: (type: ConnectionMessageType, message: string) => void,
@@ -117,6 +118,8 @@ export default class IBMi {
117118
private currentAsp: string | undefined;
118119
private libraryAsps = new Map<string, number>();
119120

121+
connectionSuccessful = false;
122+
120123
/**
121124
* @deprecated Will be replaced with {@link IBMi.getAllIAsps} in v3.0.0
122125
*/
@@ -148,15 +151,6 @@ export default class IBMi {
148151
process.stdout.write(text);
149152
};
150153

151-
private disconnectedCallback: (DisconnectCallback) | undefined;
152-
153-
/**
154-
* Will only be called once per connection.
155-
*/
156-
setDisconnectedCallback(callback: DisconnectCallback) {
157-
this.disconnectedCallback = callback;
158-
}
159-
160154
/**
161155
* getConfigFile can return pre-defined configuration files,
162156
* but can lazy load new configuration files as well.
@@ -268,7 +262,7 @@ export default class IBMi {
268262
if (callbacks.cancelEmitter) {
269263
callbacks.cancelEmitter.once('cancel', () => {
270264
wasCancelled = true;
271-
this.dispose();
265+
this.disconnect();
272266
});
273267
}
274268

@@ -297,7 +291,8 @@ export default class IBMi {
297291
privateKeyPath: connectionObject.privateKeyPath ? Tools.resolvePath(connectionObject.privateKeyPath) : undefined,
298292
passphrase: connectionObject.privateKeyPath ? connectionObject.passphrase : undefined,
299293
debug: connectionObject.sshDebug ? (message: string) => this.appendOutput(`\n[SSH debug] ${message}`) : undefined
300-
} as node_ssh.Config);
294+
});
295+
this.connectionSuccessful = true;
301296

302297
this.currentConnectionName = connectionObject.name;
303298
this.currentHost = connectionObject.host;
@@ -338,18 +333,18 @@ export default class IBMi {
338333
};
339334
}
340335

341-
if (callbacks.timeoutCallback) {
342-
const timeoutCallbackWrapper = () => {
343-
// Don't call the callback function if it was based on a user cancellation request.
344-
if (!wasCancelled) {
345-
callbacks.timeoutCallback!(this);
346-
}
347-
}
336+
// Trigger callbacks unless the connection is cancelled
337+
const onDisconnected = async (error?: Error & ClientErrorExtensions) => {
338+
await this.dispose();
339+
callbacks.onDisconnected?.(this, error);
340+
};
348341

349-
// Register handlers after we might have to abort due to bad configuration.
350-
this.client.connection!.once(`timeout`, timeoutCallbackWrapper);
351-
this.client.connection!.once(`end`, timeoutCallbackWrapper);
352-
this.client.connection!.once(`error`, timeoutCallbackWrapper);
342+
if (this.client.connection) {
343+
//end: Disconnected by the user
344+
this.client.connection.once(`end`, onDisconnected);
345+
//error/tiemout: connection dropped for some reason (details given in the SSHError type)
346+
this.client.connection.once(`error`, onDisconnected);
347+
this.client.connection.once(`timeout`, onDisconnected);
353348
}
354349

355350
callbacks.progress({
@@ -958,8 +953,7 @@ export default class IBMi {
958953
}
959954

960955
if (!options.reconnecting) {
961-
const delayedOperations: Function[] = callbacks.onConnectedOperations ? [...callbacks.onConnectedOperations] : [];
962-
for (const operation of delayedOperations) {
956+
for (const operation of callbacks.onConnectedOperations || []) {
963957
await operation();
964958
}
965959
}
@@ -984,7 +978,7 @@ export default class IBMi {
984978
};
985979

986980
} catch (e: any) {
987-
this.disconnect(true);
981+
this.disconnect();
988982

989983
let error = e.message;
990984
if (wasCancelled) {
@@ -1166,24 +1160,27 @@ export default class IBMi {
11661160
};
11671161
}
11681162

1169-
private disconnect(failedToConnect = false) {
1170-
if (this.sqlJob) {
1171-
this.sqlJob.close();
1172-
this.sqlJob = undefined;
1173-
this.splfUserData = undefined;
1163+
disconnect() {
1164+
if (this.client?.connection) {
1165+
//Close the connection and triggers its 'end' event
1166+
this.client.dispose();
11741167
}
1175-
1176-
if (this.client) {
1177-
this.client = undefined;
1178-
1179-
if (failedToConnect === false && this.disconnectedCallback) {
1180-
this.disconnectedCallback(this);
1181-
}
1168+
else{
1169+
//There is no connection: dispose directly
1170+
this.dispose();
11821171
}
11831172
}
11841173

1185-
async dispose() {
1186-
this.disconnect();
1174+
private async dispose() {
1175+
CompileTools.reset();
1176+
1177+
//Clear connected resources
1178+
if (this.sqlJob) {
1179+
delete this.sqlJob;
1180+
delete this.splfUserData;
1181+
}
1182+
await this.getComponent<Mapepire>(Mapepire.ID)?.endJobs();
1183+
delete this.client;
11871184
}
11881185

11891186
/**

0 commit comments

Comments
 (0)