Skip to content

Commit

Permalink
Merge branch 'master' into feature/saveAs
Browse files Browse the repository at this point in the history
  • Loading branch information
sebjulliand authored Sep 3, 2024
2 parents b6d9501 + bb439c4 commit f8b0261
Show file tree
Hide file tree
Showing 22 changed files with 256 additions and 118 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.12.2-dev.0",
"version": "2.13.1-dev.0",
"keywords": [
"ibmi",
"rpgle",
Expand Down
4 changes: 4 additions & 0 deletions src/api/CompileTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,10 @@ export namespace CompileTools {
else {
return false;
}
} else if (isProtected) {
//when a member is protected(read only)
vscode.window.showErrorMessage(`Action cannot be applied on a read only member.`);
return false;
} else {
//No compile commands
vscode.window.showErrorMessage(`No compile commands found for ${uri.scheme}-${extension}.`);
Expand Down
57 changes: 37 additions & 20 deletions src/api/IBMi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,19 @@ export default class IBMi {
return await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: `Connecting`,
}, async progress => {
cancellable: true
}, async (progress, cancelToken) => {
progress.report({
message: `Connecting via SSH.`
});
const delayedOperations: Function[] = [...onConnectedOperations];

await this.client.connect(connectionObject as node_ssh.Config);

cancelToken.onCancellationRequested(() => {
this.end();
});

this.currentConnectionName = connectionObject.name;
this.currentHost = connectionObject.host;
this.currentPort = connectionObject.port;
Expand All @@ -153,20 +158,24 @@ export default class IBMi {

let tempLibrarySet = false;

const disconnected = async () => {
const choice = await vscode.window.showWarningMessage(`Connection lost`, {
modal: true,
detail: `Connection to ${this.currentConnectionName} has dropped. Would you like to reconnect?`
}, `Yes`);
const timeoutHandler = async () => {
if (!cancelToken.isCancellationRequested) {
this.disconnect();

let disconnect = true;
if (choice === `Yes`) {
disconnect = !(await this.connect(connectionObject, true)).success;
}
const choice = await vscode.window.showWarningMessage(`Connection lost`, {
modal: true,
detail: `Connection to ${this.currentConnectionName} has dropped. Would you like to reconnect?`
}, `Yes`);

if (disconnect) {
this.end();
};
let disconnect = true;
if (choice === `Yes`) {
disconnect = !(await this.connect(connectionObject, true)).success;
}

if (disconnect) {
this.end();
};
}
};

progress.report({
Expand Down Expand Up @@ -211,9 +220,9 @@ export default class IBMi {
}

// Register handlers after we might have to abort due to bad configuration.
this.client.connection!.once(`timeout`, disconnected);
this.client.connection!.once(`end`, disconnected);
this.client.connection!.once(`error`, disconnected);
this.client.connection!.once(`timeout`, timeoutHandler);
this.client.connection!.once(`end`, timeoutHandler);
this.client.connection!.once(`error`, timeoutHandler);

if (!reconnecting) {
instance.setConnection(this);
Expand Down Expand Up @@ -916,7 +925,7 @@ export default class IBMi {
//Compute the maximum admited length of a command's arguments. Source: Googling and https://www.in-ulm.de/~mascheck/various/argmax/#effectively_usable
this.maximumArgsLength = Number((await this.sendCommand({ command: "/QOpenSys/usr/bin/expr `/QOpenSys/usr/bin/getconf ARG_MAX` - `env|wc -c` - `env|wc -l` \\* 4 - 2048" })).stdout);
}
else{
else {
this.maximumArgsLength = cachedServerSettings.maximumArgsLength;
}

Expand All @@ -929,9 +938,10 @@ export default class IBMi {
for (const operation of delayedOperations) {
await operation();
}
instance.fire("connected");
}

instance.fire(`connected`);

GlobalStorage.get().setServerSettingsCache(this.currentConnectionName, {
aspInfo: this.aspInfo,
qccsid: this.qccsid,
Expand Down Expand Up @@ -1098,9 +1108,17 @@ export default class IBMi {
this.commandsExecuted += 1;
}

async end() {
private async disconnect() {
this.client.connection?.removeAllListeners();
this.client.dispose();
this.client.connection = null;
instance.fire(`disconnected`);
}

async end() {
if (this.client.connection) {
this.disconnect();
}

if (this.outputChannel) {
this.outputChannel.hide();
Expand All @@ -1118,7 +1136,6 @@ export default class IBMi {
]);

instance.setConnection(undefined);
instance.fire(`disconnected`);
await vscode.commands.executeCommand(`setContext`, `code-for-ibmi:connected`, false);
vscode.window.showInformationMessage(`Disconnected from ${this.currentHost}.`);
}
Expand Down
9 changes: 5 additions & 4 deletions src/api/IBMiContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default class IBMiContent {
}

private async convertToUTF8(iconv: string, from: string, to: string, ccsid: string) {
const result = await this.ibmi.sendCommand({ command: `${iconv} -f IBM-${ccsid} -t UTF-8 "${from}" > ${to}` });
const result = await this.ibmi.sendCommand({ command: `${iconv} -f IBM-${ccsid} -t UTF-8 ${Tools.escapePath(from)} > ${Tools.escapePath(to)}` });
if (result.code === 0) {
return result.stdout;
}
Expand Down Expand Up @@ -890,7 +890,7 @@ export default class IBMiContent {
*/
async isDirectory(remotePath: string) {
return (await this.ibmi.sendCommand({
command: `cd ${remotePath}`
command: `cd ${Tools.escapePath(remotePath)}`
})).code === 0;
}

Expand Down Expand Up @@ -952,7 +952,8 @@ export default class IBMiContent {
}

async getAttributes(path: string | (QsysPath & { member?: string }), ...operands: AttrOperands[]) {
const target = (path = typeof path === 'string' ? path : this.ibmi.sysNameInAmerican(Tools.qualifyPath(path.library, path.name, path.member, path.asp)));
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(" ")}` });
if (result.code === 0) {
return result.stdout
Expand All @@ -970,7 +971,7 @@ export default class IBMiContent {
}

async countFiles(directory: string) {
return Number((await this.ibmi.sendCommand({ command: `cd ${directory} && (ls | wc -l)` })).stdout.trim());
return Number((await this.ibmi.sendCommand({ command: `cd "${directory}" && (ls | wc -l)` })).stdout.trim());
}

objectToToolTip(path: string, object: IBMiObject) {
Expand Down
78 changes: 62 additions & 16 deletions src/api/Search.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@

import * as path from 'path';
import { IBMiMember, SearchHit, SearchResults } from '../typings';
import { GlobalConfiguration } from './Configuration';
import Instance from './Instance';
import { Tools } from './Tools';
import { GetMemberInfo } from '../components/getMemberInfo';

export namespace Search {
export async function searchMembers(instance: Instance, library: string, sourceFile: string, searchTerm: string, members: IBMiMember[] | string, readOnly?: boolean,): Promise<SearchResults> {
export async function searchMembers(instance: Instance, library: string, sourceFile: string, searchTerm: string, members: string|IBMiMember[], readOnly?: boolean,): Promise<SearchResults> {
const connection = instance.getConnection();
const config = instance.getConfig();
const content = instance.getContent();

if (connection && config && content) {
let memberFilter = connection.sysNameInAmerican(typeof members === 'string' ? `${members}.MBR` : members.map(member => `${member.name}.MBR`).join(" "));

let postSearchFilter = (hit: SearchHit) => true;
if (Array.isArray(members) && memberFilter.length > connection.maximumArgsLength) {
//Failsafe: when searching on a complex filter, the member list may exceed the maximum admited arguments length (which is around 4.180.000 characters, roughly 298500 members),
// in this case, we fall back to a global search and manually filter the results afterwards/
memberFilter = "*.MBR";
postSearchFilter = (hit) => {
const memberName = hit.path.split("/").at(-1);
return members.find(member => member.name === memberName) !== undefined;
let detailedMembers: IBMiMember[]|undefined;
let memberFilter: string|undefined;

if (typeof members === `string`) {
memberFilter = connection.sysNameInAmerican(`${members}.MBR`);
} else
if (Array.isArray(members)) {
if (members.length > connection.maximumArgsLength) {
detailedMembers = members;
memberFilter = "*.MBR";
} else {
memberFilter = members.map(member => `${member.name}.MBR`).join(` `);
}
}


// First, let's fetch the ASP info
let asp = ``;
if (config.sourceASP) {
asp = `/${config.sourceASP}`;
Expand All @@ -37,17 +41,59 @@ export namespace Search {
} catch (e) { }
}

// Then search the members
const result = await connection.sendQsh({
command: `/usr/bin/grep -inHR -F "${sanitizeSearchTerm(searchTerm)}" ${memberFilter}`,
directory: connection.sysNameInAmerican(`${asp}/QSYS.LIB/${library}.LIB/${sourceFile}.FILE`)
});

if (!result.stderr) {
let hits = parseGrepOutput(
result.stdout || '', readOnly || content.isProtectedPath(library),
path => connection.sysNameInLocal(`${library}/${sourceFile}/${path.replace(/\.MBR$/, '')}`)
);

if (detailedMembers) {
// If the user provided a list of members, we need to filter the results to only include those members
hits = hits.filter(hit => {
const { name, dir } = path.parse(hit.path);
const [lib, spf] = dir.split(`/`);
return detailedMembers!.some(member => member.name === name && member.library === lib && member.file === spf);
});

} else {
// Else, we need to fetch the member info for each hit so we can display the correct extension
const infoComponent = connection?.getComponent<GetMemberInfo>(`GetMemberInfo`);
const memberInfos: IBMiMember[] = hits.map(hit => {
const { name, dir } = path.parse(hit.path);
const [library, file] = dir.split(`/`);

return {
name,
library,
file,
extension: ``
};
});

detailedMembers = await infoComponent?.getMultipleMemberInfo(memberInfos);
}

// Then fix the extensions in the hit
for (const hit of hits) {
const { name, dir } = path.parse(hit.path);
const [lib, spf] = dir.split(`/`);

const foundMember = detailedMembers?.find(member => member.name === name && member.library === lib && member.file === spf);

if (foundMember) {
hit.path = connection.sysNameInLocal(`${lib}/${spf}/${name}.${foundMember.extension}`);
}
}

return {
term: searchTerm,
hits: (parseGrepOutput(result.stdout || '', readOnly || content.isProtectedPath(library),
path => connection.sysNameInLocal(`${library}/${sourceFile}/${path.replace(/\.MBR$/, '')}`)))
.filter(postSearchFilter)
hits: hits
}
}
else {
Expand Down
3 changes: 2 additions & 1 deletion src/api/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { instance } from '../instantiate';
import { GlobalConfiguration } from './Configuration';
import IBMi from './IBMi';
import Instance from './Instance';
import { Tools } from './Tools';

function getOrDefaultToUndefined(value: string) {
if (value && value !== `default`) {
Expand Down Expand Up @@ -56,7 +57,7 @@ export namespace Terminal {
if (content) {
const ifsPath = (await content.isDirectory(ifsNode.path)) ? ifsNode.path : path.dirname(ifsNode.path);
const terminal = await selectAndOpen(context, instance, TerminalType.PASE);
terminal?.sendText(`cd ${ifsPath}`);
terminal?.sendText(`cd ${Tools.escapePath(ifsPath)}`);
}
}),

Expand Down
6 changes: 5 additions & 1 deletion src/api/debug/certificates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export async function setup(connection: IBMi, imported?: ImportedCertificate) {
debugConfig.set("JAVA_HOME", javaHome);
debugConfig.set("DEBUG_SERVICE_KEYSTORE_FILE", certificatePath);
debugConfig.set("DEBUG_SERVICE_KEYSTORE_PASSWORD", encryptResult.stdout);
debugConfig.set("CODE4IDEBUG", `$([ -f $DBGSRV_WRK_DIR/${ENCRYPTION_KEY} ] && cp $DBGSRV_WRK_DIR/${ENCRYPTION_KEY} $DBGSRV_WRK_DIR/key.properties)`);
debugConfig.setCode4iDebug(`$([ -f $DBGSRV_WRK_DIR/${ENCRYPTION_KEY} ] && cp $DBGSRV_WRK_DIR/${ENCRYPTION_KEY} $DBGSRV_WRK_DIR/key.properties)`);
debugConfig.save();
}
else {
Expand All @@ -210,6 +210,10 @@ export async function setup(connection: IBMi, imported?: ImportedCertificate) {
});
}

export async function debugKeyFileExists(connection: IBMi, debugConfig: DebugConfiguration) {
return await connection.content.testStreamFile(`${debugConfig.getRemoteServiceWorkDir()}/.code4i.debug`, "f");
}

export async function remoteCertificatesExists(debugConfig?: DebugConfiguration) {
const content = instance.getContent();
if (content) {
Expand Down
10 changes: 9 additions & 1 deletion src/api/debug/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ export class DebugConfiguration {
getRemoteServiceWorkDir() {
return this.getOrDefault("DBGSRV_WRK_DIR", "/QIBM/UserData/IBMiDebugService");
}

getCode4iDebug() {
return this.get("CODE4IDEBUG");
}

setCode4iDebug(value: string) {
return this.set("CODE4IDEBUG", value);
}
}

interface DebugServiceDetails {
Expand Down Expand Up @@ -145,7 +153,7 @@ export async function getDebugServiceDetails(): Promise<DebugServiceDetails> {
console.log(e);
}
}
else{
else {
details = {
version: `1.0.0`,
java: `8`,
Expand Down
5 changes: 3 additions & 2 deletions src/api/debug/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,9 @@ export async function initialize(context: ExtensionContext) {
const ptfInstalled = server.debugPTFInstalled();

if (ptfInstalled) {
const remoteCertsExists = await certificates.remoteCertificatesExists();
if (remoteCertsExists) {
const debugConfig = await new DebugConfiguration().load()
const remoteCertsExists = await certificates.remoteCertificatesExists(debugConfig);
if (remoteCertsExists && await certificates.debugKeyFileExists(connection, debugConfig) && debugConfig.getCode4iDebug()) {
await certificates.downloadClientCert(connection);
vscode.window.showInformationMessage(`Debug Service Certificate already exist on the server. The client certificate has been downloaded to enable debugging.`);
}
Expand Down
Loading

0 comments on commit f8b0261

Please sign in to comment.