diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 8ddcab62c..042577de7 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -4,7 +4,6 @@ jobs: release: name: Validation runs-on: ubuntu-latest - if: contains(github.event.pull_request.labels.*.name, 'build') steps: - name: Use Node.js 20 uses: actions/setup-node@v3 @@ -15,8 +14,10 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 1 - - run: npm install - - run: npm run lint + - run: npm ci + + - name: Build types + run: npm run typings - run: npm install -g @vscode/vsce - name: Create build @@ -27,8 +28,17 @@ jobs: with: name: code-for-ibmi-pr-build path: ./*.vsix - + + - name: Find Comment + uses: peter-evans/find-comment@v1 + id: fc + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: new build is available + - name: Post comment + if: steps.fc.outputs.comment-id == '' uses: actions/github-script@v5 with: script: | @@ -36,8 +46,17 @@ jobs: issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: '👋 A new build is available for this PR based on ${{ github.event.pull_request.head.sha }}.\n * [Download here.](https://github.com/codefori/vscode-ibmi/actions/runs/${{ github.run_id }})\n* [Read more about how to test](https://github.com/codefori/vscode-ibmi/blob/master/.github/pr_testing_template.md)' + body: '👋 A new build is available for this PR based on ${{ github.event.pull_request.head.sha }}.\n * [Download here.](https://github.com/codefori/vscode-ibmi/actions/runs/${{ github.run_id }})\n* [Read more about how to test](https://github.com/codefori/vscode-ibmi/blob/master/.github/pr_testing_template.md)' }) - - name: Build types - run: npm run typings \ No newline at end of file + - name: Update comment + if: steps.fc.outputs.comment-id != '' + uses: peter-evans/create-or-update-comment@v1 + with: + edit-mode: replace + comment-id: ${{ steps.fc.outputs.comment-id }} + body: | + 👋 A new build is available for this PR based on ${{ github.event.pull_request.head.sha }}. + + * [Download here.](https://github.com/codefori/vscode-ibmi/actions/runs/${{ github.run_id }}) + * [Read more about how to test](https://github.com/codefori/vscode-ibmi/blob/master/.github/pr_testing_template.md) \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index e7a30837e..80e12162a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,7 +31,41 @@ "sourceMaps": true, "preLaunchTask": "${defaultBuildTask}", "env": { - "testing": "true" + "base_testing": "true" + } + }, + { + "name": "Extension Tests (All simultaneously)", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ], + "sourceMaps": true, + "preLaunchTask": "${defaultBuildTask}", + "env": { + "base_testing": "true", + "simultaneous": "true" + } + }, + { + "name": "Extension Tests (Encoding suite)", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ], + "sourceMaps": true, + "preLaunchTask": "${defaultBuildTask}", + "env": { + "base_testing": "true", + "specific": "encoding" } }, { @@ -47,7 +81,7 @@ "sourceMaps": true, "preLaunchTask": "${defaultBuildTask}", "env": { - "testing": "true", + "base_testing": "true", "individual": "true" } }, diff --git a/package-lock.json b/package-lock.json index 902514fe1..55bf8c647 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-for-ibmi", - "version": "2.13.10-dev.0", + "version": "2.13.19-dev.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "code-for-ibmi", - "version": "2.13.10-dev.0", + "version": "2.13.19-dev.0", "license": "MIT", "dependencies": { "@bendera/vscode-webview-elements": "^0.12.0", @@ -24,7 +24,7 @@ "devDependencies": { "@types/adm-zip": "^0.5.5", "@types/glob": "^7.1.3", - "@types/node": "^12.20.55", + "@types/node": "^18.0.0", "@types/source-map-support": "^0.5.6", "@types/tar": "^6.1.4", "@types/tmp": "^0.2.3", @@ -679,10 +679,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true + "version": "18.19.64", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.64.tgz", + "integrity": "sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/source-map-support": { "version": "0.5.6", @@ -701,11 +703,6 @@ "@types/node": "^18.11.18" } }, - "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.16.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.16.tgz", - "integrity": "sha512-NpaM49IGQQAUlBhHMF82QH80J08os4ZmyF9MkpCzWAGuOHqE4gTEbhzd7L3l5LmWuZ6E0OiC1FweQ4tsiW35+g==" - }, "node_modules/@types/tar": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.4.tgz", @@ -3605,6 +3602,11 @@ "node": ">=4.2.0" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", @@ -4254,10 +4256,12 @@ "dev": true }, "@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true + "version": "18.19.64", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.64.tgz", + "integrity": "sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==", + "requires": { + "undici-types": "~5.26.4" + } }, "@types/source-map-support": { "version": "0.5.6", @@ -4274,13 +4278,6 @@ "integrity": "sha512-LdnE7UBpvHCgUznvn2fwLt2hkaENcKPFqOyXGkvyTLfxCXBN6roc1RmECNYuzzbHePzD3PaAov5rri9hehzx9Q==", "requires": { "@types/node": "^18.11.18" - }, - "dependencies": { - "@types/node": { - "version": "18.16.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.16.tgz", - "integrity": "sha512-NpaM49IGQQAUlBhHMF82QH80J08os4ZmyF9MkpCzWAGuOHqE4gTEbhzd7L3l5LmWuZ6E0OiC1FweQ4tsiW35+g==" - } } }, "@types/tar": { @@ -6351,6 +6348,11 @@ "integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==", "dev": true }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", diff --git a/package.json b/package.json index 988f29de7..b53db44ee 100644 --- a/package.json +++ b/package.json @@ -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.13.10-dev.0", + "version": "2.13.19-dev.0", "keywords": [ "ibmi", "rpgle", @@ -1692,6 +1692,13 @@ "category": "IBM i", "icon": "$(keyboard)", "enablement": "code-for-ibmi:connected" + }, + { + "command": "code-for-ibmi.testing.connectWithFixture", + "title": "Connect with Fixture", + "category": "IBM i", + "icon": "$(debug)", + "enablement": "code-for-ibmi:testing" } ], "keybindings": [ @@ -1938,6 +1945,10 @@ } ], "commandPalette": [ + { + "command": "code-for-ibmi.testing.connectWithFixture", + "when": "never" + }, { "command": "code-for-ibmi.userLibraryList.enable", "when": "never" @@ -2503,6 +2514,11 @@ "when": "view == connectionBrowser && !listMultiSelection && viewItem == server", "group": "inline" }, + { + "command": "code-for-ibmi.testing.connectWithFixture", + "when": "view == connectionBrowser && code-for-ibmi:testing && !listMultiSelection && viewItem == server", + "group": "inline" + }, { "command": "code-for-ibmi.connectToAndReload", "when": "view == connectionBrowser && !listMultiSelection && viewItem == server", @@ -2949,7 +2965,7 @@ "devDependencies": { "@types/adm-zip": "^0.5.5", "@types/glob": "^7.1.3", - "@types/node": "^12.20.55", + "@types/node": "^18.0.0", "@types/source-map-support": "^0.5.6", "@types/tar": "^6.1.4", "@types/tmp": "^0.2.3", @@ -2969,14 +2985,14 @@ "@bendera/vscode-webview-elements": "^0.12.0", "@ibm/ibmi-eventf-parser": "^1.0.2", "adm-zip": "^0.5.14", + "crc-32": "https://cdn.sheetjs.com/crc-32-latest/crc-32-latest.tgz", "csv": "^6.2.1", + "escape-string-regexp": "^5.0.0", "ignore": "^5.1.9", "node-ssh": "^13.1.0", "tar": "^6.2.1", "tmp": "^0.2.1", - "vscode-diff": "^2.0.2", - "crc-32": "https://cdn.sheetjs.com/crc-32-latest/crc-32-latest.tgz", - "escape-string-regexp": "^5.0.0" + "vscode-diff": "^2.0.2" }, "extensionDependencies": [ "barrettotte.ibmi-languages", diff --git a/src/api/CompileTools.ts b/src/api/CompileTools.ts index ec62e8869..bb3a74afa 100644 --- a/src/api/CompileTools.ts +++ b/src/api/CompileTools.ts @@ -682,7 +682,7 @@ export namespace CompileTools { command: [ ...options.noLibList? [] : buildLiblistCommands(connection, ileSetup), ...commands.map(command => - `${`system ${GlobalConfiguration.get(`logCompileOutput`) ? `` : `-s`} "${command.replace(/[$]/g, `\\$&`)}"`}`, + `${`system "${IBMi.escapeForShell(command)}"`}`, ) ].join(` && `), directory: cwd, @@ -815,9 +815,9 @@ export namespace CompileTools { function buildLiblistCommands(connection: IBMi, config: ILELibrarySettings): string[] { return [ - `liblist -d ${Tools.sanitizeLibraryNames(connection.defaultUserLibraries).join(` `)}`, - `liblist -c ${Tools.sanitizeLibraryNames([config.currentLibrary])}`, - `liblist -a ${Tools.sanitizeLibraryNames(buildLibraryList(config)).join(` `)}` + `liblist -d ${IBMi.escapeForShell(Tools.sanitizeObjNamesForPase(connection.defaultUserLibraries).join(` `))}`, + `liblist -c ${IBMi.escapeForShell(Tools.sanitizeObjNamesForPase([config.currentLibrary])[0])}`, + `liblist -a ${IBMi.escapeForShell(Tools.sanitizeObjNamesForPase(buildLibraryList(config)).join(` `))}` ]; } } diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index f8d1d0fab..b73262d39 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -6,6 +6,7 @@ import path, { parse as parsePath } from 'path'; import * as vscode from "vscode"; import { IBMiComponent, IBMiComponentType } from "../components/component"; import { CopyToImport } from "../components/copyToImport"; +import { cqsh } from '../components/cqsh'; import { ComponentManager } from "../components/manager"; import { instance } from "../instantiate"; import { CommandData, CommandResult, ConnectionData, IBMiMember, RemoteCommand, SpecialAuthorities, WrapResult } from "../typings"; @@ -17,18 +18,20 @@ import { Tools } from './Tools'; import * as configVars from './configVars'; import { DebugConfiguration } from "./debug/config"; import { debugPTFInstalled } from "./debug/server"; +import { r } from 'tar'; export interface MemberParts extends IBMiMember { basename: string } +const CCSID_NOCONVERSION = 65535; const CCSID_SYSVAL = -2; const bashShellPath = '/QOpenSys/pkgs/bin/bash'; const remoteApps = [ // All names MUST also be defined as key in 'remoteFeatures' below!! { path: `/usr/bin/`, - names: [`setccsid`, `iconv`, `attr`, `tar`, `ls`] + names: [`setccsid`, `iconv`, `attr`, `tar`, `ls`, `uname`] }, { path: `/QOpenSys/pkgs/bin/`, @@ -50,10 +53,12 @@ const remoteApps = [ // All names MUST also be defined as key in 'remoteFeatures ]; export default class IBMi { - private qccsid: number = 65535; - private jobCcsid: number = CCSID_SYSVAL; + private systemVersion: number = 0; + private qccsid: number = CCSID_NOCONVERSION; + private userJobCcsid: number = CCSID_SYSVAL; /** User default CCSID is job default CCSID */ private userDefaultCCSID: number = 0; + private sshdCcsid: number|undefined; private componentManager = new ComponentManager(this); @@ -73,7 +78,12 @@ export default class IBMi { */ aspInfo: { [id: number]: string } = {}; remoteFeatures: { [name: string]: string | undefined }; - variantChars: { american: string, local: string }; + + variantChars: { + american: string, + local: string, + qsysNameRegex?: RegExp + }; /** * Strictly for storing errors from sendCommand. @@ -89,7 +99,33 @@ export default class IBMi { //Maximum admited length for command's argument - any command whose arguments are longer than this won't be executed by the shell maximumArgsLength = 0; - dangerousVariants = false; + get canUseCqsh() { + return this.getComponent(cqsh) !== undefined; + } + + /** + * Primarily used for running SQL statements. + */ + get userCcsidInvalid() { + return this.userJobCcsid === CCSID_NOCONVERSION; + } + + /** + * Determines if the client should do variant translation. + * False when cqsh should be used. + * True when cqsh is not available and the job CCSID is not the same as the SSHD CCSID. + */ + get requiresTranslation() { + if (this.canUseCqsh) { + return false; + } else { + return this.getCcsid() !== this.sshdCcsid; + } + } + + get dangerousVariants() { + return this.variantChars.local !== this.variantChars.local.toLocaleUpperCase(); + }; constructor() { this.client = new node_ssh.NodeSSH; @@ -116,7 +152,8 @@ export default class IBMi { jdk80: undefined, jdk11: undefined, jdk17: undefined, - openjdk11: undefined + openjdk11: undefined, + uname: undefined, }; this.variantChars = { @@ -129,6 +166,7 @@ export default class IBMi { * @returns {Promise<{success: boolean, error?: any}>} Was succesful at connecting or not. */ async connect(connectionObject: ConnectionData, reconnecting?: boolean, reloadServerSettings: boolean = false, onConnectedOperations: Function[] = []): Promise<{ success: boolean, error?: any }> { + const currentExtensionVersion = process.env.VSCODEIBMI_VERSION; return await Tools.withContext("code-for-ibmi:connecting", async () => { try { connectionObject.keepaliveInterval = 35000; @@ -159,6 +197,7 @@ export default class IBMi { if (!reconnecting) { this.outputChannel = vscode.window.createOutputChannel(`Code for IBM i: ${this.currentConnectionName}`); this.outputChannelContent = ''; + this.appendOutput(`Code for IBM i, version ${currentExtensionVersion}\n\n`); } let tempLibrarySet = false; @@ -192,8 +231,11 @@ export default class IBMi { // Load cached server settings. const cachedServerSettings: CachedServerSettings = GlobalStorage.get().getServerSettingsCache(this.currentConnectionName); + // Reload server settings? - const quickConnect = (this.config.quickConnect === true && reloadServerSettings === false); + const quickConnect = () => { + return (this.config!.quickConnect === true && reloadServerSettings === false); + } // Check shell output for additional user text - this will confuse Code... progress.report({ @@ -322,6 +364,94 @@ export default class IBMi { } } + // If the version has changed (by update for example), then fetch everything again + if (cachedServerSettings?.lastCheckedOnVersion !== currentExtensionVersion) { + reloadServerSettings = true; + } + + // Check for installed components? + // For Quick Connect to work here, 'remoteFeatures' MUST have all features defined and no new properties may be added! + if (quickConnect() && cachedServerSettings?.remoteFeaturesKeys && cachedServerSettings.remoteFeaturesKeys === Object.keys(this.remoteFeatures).sort().toString()) { + Object.assign(this.remoteFeatures, cachedServerSettings.remoteFeatures); + } else { + progress.report({ + message: `Checking installed components on host IBM i.` + }); + + // We need to check if our remote programs are installed. + remoteApps.push( + { + path: `/QSYS.lib/${this.upperCaseName(this.config.tempLibrary)}.lib/`, + names: [`GETNEWLIBL.PGM`], + specific: `GE*.PGM` + } + ); + + //Next, we see what pase features are available (installed via yum) + //This may enable certain features in the future. + for (const feature of remoteApps) { + try { + progress.report({ + message: `Checking installed components on host IBM i: ${feature.path}` + }); + + const call = await this.sendCommand({ command: `ls -p ${feature.path}${feature.specific || ``}` }); + if (call.stdout) { + const files = call.stdout.split(`\n`); + + if (feature.specific) { + for (const name of feature.names) + this.remoteFeatures[name] = files.find(file => file.includes(name)); + } else { + for (const name of feature.names) + if (files.includes(name)) + this.remoteFeatures[name] = feature.path + name; + } + } + } catch (e) { + console.log(e); + } + } + + //Specific Java installations check + progress.report({ + 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`); + } + + if (this.remoteFeatures.uname) { + progress.report({ + message: `Checking OS version.` + }); + const systemVersionResult = await this.sendCommand({ command: `${this.remoteFeatures.uname} -rv` }); + + if (systemVersionResult.code === 0) { + const version = systemVersionResult.stdout.trim().split(` `); + this.systemVersion = Number(`${version[1]}.${version[0]}`); + } + } + + if (!this.systemVersion) { + vscode.window.showWarningMessage(`Unable to determine system version. Code for IBM i only supports 7.3 and above. Some features may not work correctly.`); + } else if (this.systemVersion < 7.3) { + vscode.window.showWarningMessage(`IBM i ${this.systemVersion} is not supported. Code for IBM i only supports 7.3 and above. Some features may not work correctly.`); + } + + progress.report({ message: `Checking Code for IBM i components.` }); + await this.componentManager.startup(); + + const componentStates = await this.componentManager.getState(); + 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`); + } + this.appendOutput(`\n`); + progress.report({ message: `Checking library list configuration.` }); @@ -479,7 +609,7 @@ export default class IBMi { } // Check for bad data areas? - if (quickConnect === true && cachedServerSettings?.badDataAreasChecked === true) { + if (quickConnect() && cachedServerSettings?.badDataAreasChecked === true) { // Do nothing, bad data areas are already checked. } else { progress.report({ @@ -548,196 +678,6 @@ export default class IBMi { } } - // Check for installed components? - // For Quick Connect to work here, 'remoteFeatures' MUST have all features defined and no new properties may be added! - if (quickConnect === true && cachedServerSettings?.remoteFeaturesKeys && cachedServerSettings.remoteFeaturesKeys === Object.keys(this.remoteFeatures).sort().toString()) { - Object.assign(this.remoteFeatures, cachedServerSettings.remoteFeatures); - } else { - progress.report({ - message: `Checking installed components on host IBM i.` - }); - - // We need to check if our remote programs are installed. - remoteApps.push( - { - path: `/QSYS.lib/${this.upperCaseName(this.config.tempLibrary)}.lib/`, - names: [`GETNEWLIBL.PGM`], - specific: `GE*.PGM` - } - ); - - //Next, we see what pase features are available (installed via yum) - //This may enable certain features in the future. - for (const feature of remoteApps) { - try { - progress.report({ - message: `Checking installed components on host IBM i: ${feature.path}` - }); - - const call = await this.sendCommand({ command: `ls -p ${feature.path}${feature.specific || ``}` }); - if (call.stdout) { - const files = call.stdout.split(`\n`); - - if (feature.specific) { - for (const name of feature.names) - this.remoteFeatures[name] = files.find(file => file.includes(name)); - } else { - for (const name of feature.names) - if (files.includes(name)) - this.remoteFeatures[name] = feature.path + name; - } - } - } catch (e) { - console.log(e); - } - } - - //Specific Java installations check - progress.report({ - 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`); - } - - if (this.sqlRunnerAvailable()) { - //Temporary function to run SQL - - // TODO: stop using this runSQL function and this.runSql - const runSQL = async (statement: string) => { - const output = await this.sendCommand({ - command: `LC_ALL=EN_US.UTF-8 system "call QSYS/QZDFMDB2 PARM('-d' '-i')"`, - stdin: statement - }); - - if (output.code === 0) { - return Tools.db2Parse(output.stdout); - } - else { - throw new Error(output.stdout); - } - }; - - // Check for ASP information? - if (quickConnect === true && cachedServerSettings?.aspInfo) { - this.aspInfo = cachedServerSettings.aspInfo; - } else { - progress.report({ - message: `Checking for ASP information.` - }); - - //This is mostly a nice to have. We grab the ASP info so user's do - //not have to provide the ASP in the settings. - try { - const resultSet = await runSQL(`SELECT * FROM QSYS2.ASP_INFO`); - resultSet.forEach(row => { - if (row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME !== `null`) { - this.aspInfo[Number(row.ASP_NUMBER)] = String(row.DEVICE_DESCRIPTION_NAME); - } - }); - } catch (e) { - //Oh well - progress.report({ - message: `Failed to get ASP information.` - }); - } - } - - // Fetch conversion values? - if (quickConnect === true && cachedServerSettings?.jobCcsid !== null && cachedServerSettings?.variantChars && cachedServerSettings?.userDefaultCCSID && cachedServerSettings?.qccsid) { - this.qccsid = cachedServerSettings.qccsid; - this.jobCcsid = cachedServerSettings.jobCcsid; - this.variantChars = cachedServerSettings.variantChars; - this.userDefaultCCSID = cachedServerSettings.userDefaultCCSID; - } else { - progress.report({ - message: `Fetching conversion values.` - }); - - // Next, we're going to see if we can get the CCSID from the user or the system. - // Some things don't work without it!!! - try { - - // we need to grab the system CCSID (QCCSID) - const [systemCCSID] = await runSQL(`select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`); - if (typeof systemCCSID.CURRENT_NUMERIC_VALUE === 'number') { - this.qccsid = systemCCSID.CURRENT_NUMERIC_VALUE; - } - - // we grab the users default CCSID - const [userInfo] = await runSQL(`select CHARACTER_CODE_SET_ID from table( QSYS2.QSYUSRINFO( USERNAME => upper('${this.currentUser}') ) )`); - if (userInfo.CHARACTER_CODE_SET_ID !== `null` && typeof userInfo.CHARACTER_CODE_SET_ID === 'number') { - this.jobCcsid = userInfo.CHARACTER_CODE_SET_ID; - } - - // if the job ccsid is *SYSVAL, then assign it to sysval - if (this.jobCcsid === CCSID_SYSVAL) { - this.jobCcsid = this.qccsid; - } - - // Let's also get the user's default CCSID - try { - const [activeJob] = await runSQL(`Select DEFAULT_CCSID From Table(QSYS2.ACTIVE_JOB_INFO( JOB_NAME_FILTER => '*', DETAILED_INFO => 'ALL' ))`); - this.userDefaultCCSID = Number(activeJob.DEFAULT_CCSID); - } - catch (error) { - const [defaultCCSID] = (await this.runCommand({ command: "DSPJOB OPTION(*DFNA)" })) - .stdout - .split("\n") - .filter(line => line.includes("DFTCCSID")); - - const defaultCCSCID = Number(defaultCCSID.split("DFTCCSID").at(1)?.trim()); - if (defaultCCSCID && !isNaN(defaultCCSCID)) { - this.userDefaultCCSID = defaultCCSCID; - } - } - - progress.report({ - message: `Fetching local encoding values.` - }); - - const [variants] = await runSQL(`With VARIANTS ( HASH, AT, DOLLARSIGN ) as (` - + ` values ( cast( x'7B' as varchar(1) )` - + ` , cast( x'7C' as varchar(1) )` - + ` , cast( x'5B' as varchar(1) ) )` - + `)` - + `Select HASH concat AT concat DOLLARSIGN as LOCAL from VARIANTS`); - - if (typeof variants.LOCAL === 'string' && variants.LOCAL !== `null`) { - this.variantChars.local = variants.LOCAL; - } - } catch (e) { - // Oh well! - console.log(e); - } - } - } else { - // Disable it if it's not found - if (this.enableSQL) { - progress.report({ - message: `SQL program not installed. Disabling SQL.` - }); - } - } - - if (!this.enableSQL) { - const encoding = this.getEncoding(); - // Show a message if the system CCSID is bad - const ccsidMessage = this.qccsid === 65535 ? `The system QCCSID is not set correctly. We recommend changing the CCSID on your user profile first, and then changing your system QCCSID.` : undefined; - - // Show a message if the runtime CCSID is bad (which means both runtime and default CCSID are bad) - in theory should never happen - const encodingMessage = encoding.invalid ? `Runtime CCSID detected as ${encoding.ccsid} and is invalid. Please change the CCSID or default CCSID in your user profile.` : undefined; - - vscode.window.showErrorMessage([ - ccsidMessage, - encodingMessage, - `Using fallback methods to access the IBM i file systems.` - ].filter(x => x).join(` `)); - } - // give user option to set bash as default shell. if (this.remoteFeatures[`bash`]) { try { @@ -880,7 +820,7 @@ export default class IBMi { } // Validate configured library list. - if (quickConnect === true && cachedServerSettings?.libraryListValidated === true) { + if (quickConnect() && cachedServerSettings?.libraryListValidated === true) { // Do nothing, library list is already checked. } else { if (this.config.libraryList) { @@ -892,8 +832,8 @@ export default class IBMi { const result = await this.sendQsh({ command: [ - `liblist -d ` + this.defaultUserLibraries.join(` `).replace(/\$/g, `\\$`), - ...this.config.libraryList.map(lib => `liblist -a ` + lib.replace(/\$/g, `\\$`)) + `liblist -d ` + IBMi.escapeForShell(this.defaultUserLibraries.join(` `)), + ...this.config.libraryList.map(lib => `liblist -a ` + IBMi.escapeForShell(lib)) ].join(`; `) }); @@ -944,8 +884,168 @@ export default class IBMi { this.maximumArgsLength = cachedServerSettings.maximumArgsLength; } - progress.report({ message: `Checking Code for IBM i components.` }); - await this.componentManager.startup(); + if (this.sqlRunnerAvailable()) { + // Check for ASP information? + if (quickConnect() && cachedServerSettings?.aspInfo) { + this.aspInfo = cachedServerSettings.aspInfo; + } else { + progress.report({ + message: `Checking for ASP information.` + }); + + //This is mostly a nice to have. We grab the ASP info so user's do + //not have to provide the ASP in the settings. + try { + const resultSet = await this.runSQL(`SELECT * FROM QSYS2.ASP_INFO`); + resultSet.forEach(row => { + if (row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME !== `null`) { + this.aspInfo[Number(row.ASP_NUMBER)] = String(row.DEVICE_DESCRIPTION_NAME); + } + }); + } catch (e) { + //Oh well + progress.report({ + message: `Failed to get ASP information.` + }); + } + } + + // Fetch conversion values? + if (quickConnect() && cachedServerSettings?.jobCcsid !== null && cachedServerSettings?.userDefaultCCSID && cachedServerSettings?.qccsid) { + this.qccsid = cachedServerSettings.qccsid; + this.userJobCcsid = cachedServerSettings.jobCcsid; + this.userDefaultCCSID = cachedServerSettings.userDefaultCCSID; + } else { + progress.report({ + message: `Fetching conversion values.` + }); + + // Next, we're going to see if we can get the CCSID from the user or the system. + // Some things don't work without it!!! + try { + + // we need to grab the system CCSID (QCCSID) + const [systemCCSID] = await this.runSQL(`select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`); + if (typeof systemCCSID.CURRENT_NUMERIC_VALUE === 'number') { + this.qccsid = systemCCSID.CURRENT_NUMERIC_VALUE; + } + + // we grab the users default CCSID + const [userInfo] = await this.runSQL(`select CHARACTER_CODE_SET_ID from table( QSYS2.QSYUSRINFO( USERNAME => upper('${this.currentUser}') ) )`); + if (userInfo.CHARACTER_CODE_SET_ID !== `null` && typeof userInfo.CHARACTER_CODE_SET_ID === 'number') { + this.userJobCcsid = userInfo.CHARACTER_CODE_SET_ID; + } + + // if the job ccsid is *SYSVAL, then assign it to sysval + if (this.userJobCcsid === CCSID_SYSVAL) { + this.userJobCcsid = this.qccsid; + } + + // Let's also get the user's default CCSID + try { + const [activeJob] = await this.runSQL(`Select DEFAULT_CCSID From Table(QSYS2.ACTIVE_JOB_INFO( JOB_NAME_FILTER => '*', DETAILED_INFO => 'ALL' ))`); + this.userDefaultCCSID = Number(activeJob.DEFAULT_CCSID); + } + catch (error) { + const [defaultCCSID] = (await this.runCommand({ command: "DSPJOB OPTION(*DFNA)" })) + .stdout + .split("\n") + .filter(line => line.includes("DFTCCSID")); + + const defaultCCSCID = Number(defaultCCSID.split("DFTCCSID").at(1)?.trim()); + if (defaultCCSCID && !isNaN(defaultCCSCID)) { + this.userDefaultCCSID = defaultCCSCID; + } + } + + } catch (e) { + // Oh well! + console.log(e); + } + } + + let userCcsidNeedsFixing = false; + let sshdCcsidMismatch = false; + + const showCcsidWarning = (message: string) => { + vscode.window.showWarningMessage(message, `Show documentation`).then(choice => { + if (choice === `Show documentation`) { + vscode.commands.executeCommand(`vscode.open`, `https://codefori.github.io/docs/tips/ccsid/`); + } + }); + } + + if (this.canUseCqsh) { + // If cqsh is available, but the user profile CCSID is bad, then cqsh won't work + if (this.getCcsid() === CCSID_NOCONVERSION) { + userCcsidNeedsFixing = true; + } + } + + else { + // If cqsh is not available, then we need to check the SSHD CCSID + this.sshdCcsid = await this.getSshCcsid(); + if (this.sshdCcsid === this.getCcsid()) { + // If the SSHD CCSID matches the job CCSID (not the user profile!), then we're good. + // This means we can use regular qsh without worrying about translation because the SSHD and job CCSID match. + userCcsidNeedsFixing = false; + } else { + // If the SSHD CCSID does not match the job CCSID, then we need to warn the user + sshdCcsidMismatch = true; + } + } + + if (userCcsidNeedsFixing) { + showCcsidWarning(`The job CCSID is set to ${CCSID_NOCONVERSION}. This may cause issues with objects with variant characters. Please use CHGUSRPRF USER(${this.currentUser.toUpperCase()}) CCSID(${this.userDefaultCCSID}) to set your profile to the current default CCSID.`); + } else if (sshdCcsidMismatch) { + showCcsidWarning(`The CCSID of the SSH connection (${this.sshdCcsid}) does not match the job CCSID (${this.getCcsid()}). This may cause issues with objects with variant characters.`); + } + + this.appendOutput(`\nCCSID information:\n`); + this.appendOutput(`\tQCCSID: ${this.qccsid}\n`); + this.appendOutput(`\tUser Job CCSID: ${this.userJobCcsid}\n`); + this.appendOutput(`\tUser Default CCSID: ${this.userDefaultCCSID}\n`); + if (this.sshdCcsid) { + this.appendOutput(`\tSSHD CCSID: ${this.sshdCcsid}\n`); + } + + // We only do this check if we're on 7.3 or below. + if (this.systemVersion && this.systemVersion <= 7.3) { + progress.report({ + message: `Checking PASE locale environment variables.` + }); + + const systemEnvVars = await this.getSysEnvVars(); + + const paseLang = systemEnvVars.PASE_LANG; + const paseCcsid = systemEnvVars.QIBM_PASE_CCSID; + + if (paseLang === undefined || paseCcsid === undefined) { + showCcsidWarning(`The PASE environment variables PASE_LANG and QIBM_PASE_CCSID are not set correctly and is required for this OS version (${this.systemVersion}). This may cause issues with objects with variant characters.`); + } else if (paseCcsid !== `1208`) { + showCcsidWarning(`The PASE environment variable QIBM_PASE_CCSID is not set to 1208 and is required for this OS version (${this.systemVersion}). This may cause issues with objects with variant characters.`); + } + } + + // We always need to fetch the local variants because + // now we pickup CCSID changes faster due to cqsh + progress.report({ + message: `Fetching local encoding values.` + }); + + const [variants] = await this.runSQL(`With VARIANTS ( HASH, AT, DOLLARSIGN ) as (` + + ` values ( cast( x'7B' as varchar(1) )` + + ` , cast( x'7C' as varchar(1) )` + + ` , cast( x'5B' as varchar(1) ) )` + + `)` + + `Select HASH concat AT concat DOLLARSIGN as LOCAL from VARIANTS`); + + if (typeof variants.LOCAL === 'string' && variants.LOCAL !== `null`) { + this.variantChars.local = variants.LOCAL; + } + } else { + vscode.window.showWarningMessage(`The SQL runner is not available. This could mean that VS Code will not work for this connection. See our documentation for more information.`) + } if (!reconnecting) { vscode.workspace.getConfiguration().update(`workbench.editor.enablePreview`, false, true); @@ -958,15 +1058,12 @@ export default class IBMi { instance.fire(`connected`); GlobalStorage.get().setServerSettingsCache(this.currentConnectionName, { + lastCheckedOnVersion: currentExtensionVersion, aspInfo: this.aspInfo, qccsid: this.qccsid, - jobCcsid: this.jobCcsid, + jobCcsid: this.userJobCcsid, remoteFeatures: this.remoteFeatures, remoteFeaturesKeys: Object.keys(this.remoteFeatures).sort().toString(), - variantChars: { - american: this.variantChars.american, - local: this.variantChars.local, - }, badDataAreasChecked: true, libraryListValidated: true, pathChecked: true, @@ -975,9 +1072,6 @@ export default class IBMi { maximumArgsLength: this.maximumArgsLength }); - //Keep track of variant characters that can be uppercased - this.dangerousVariants = this.variantChars.local !== this.variantChars.local.toLocaleUpperCase(); - return { success: true }; @@ -1018,6 +1112,13 @@ export default class IBMi { }); } + /** + * Can return 0 if the OS version was not detected. + */ + getSystemVersion(): number { + return this.systemVersion; + } + usingBash() { return this.shell === bashShellPath; } @@ -1034,12 +1135,28 @@ export default class IBMi { return CompileTools.runCommand(instance, data); } + static escapeForShell(command: string) { + return command.replace(/\$/g, `\\$`) + } + async sendQsh(options: CommandData) { options.stdin = options.command; + let qshExecutable = `/QOpenSys/usr/bin/qsh`; + + if (this.canUseCqsh) { + const customQsh = this.getComponent(cqsh)!; + qshExecutable = await customQsh.getPath(); + } + + if (this.requiresTranslation) { + options.stdin = this.sysNameInAmerican(options.stdin); + options.directory = options.directory ? this.sysNameInAmerican(options.directory) : undefined; + } + return this.sendCommand({ ...options, - command: `/QOpenSys/usr/bin/qsh` + command: qshExecutable }); } @@ -1050,8 +1167,7 @@ export default class IBMi { async sendCommand(options: CommandData): Promise { let commands: string[] = []; if (options.env) { - commands.push(...Object.entries(options.env).map(([key, value]) => `export ${key}="${value?.replace(/\$/g, `\\$`).replace(/"/g, `\\"`) || `` - }"`)) + commands.push(...Object.entries(options.env).map(([key, value]) => `export ${key}="${value ? IBMi.escapeForShell(value) : ``}"`)); } commands.push(options.command); @@ -1160,23 +1276,40 @@ export default class IBMi { */ get enableSQL(): boolean { const sqlRunner = this.sqlRunnerAvailable(); - const encodings = this.getEncoding(); - return sqlRunner && encodings.invalid === false; - } - - /** - * Do not use this API directly. - * It exists to support some backwards compatability. - * @deprecated - */ - set enableSQL(value: boolean) { - this.remoteFeatures[`QZDFMDB2.PGM`] = value ? `/QSYS.LIB/QZDFMDB2.PGM` : undefined; + return sqlRunner; } public sqlRunnerAvailable() { return this.remoteFeatures[`QZDFMDB2.PGM`] !== undefined; } + private async getSshCcsid() { + const sql = ` + with SSH_DETAIL (id, iid) as ( + select substring(job_name, locate('/', job_name, 15)+1, 10) as id, internal_job_id as iid from qsys2.netstat_job_info j where local_address = '0.0.0.0' and local_port = 22 + ) + select DEFAULT_CCSID, CCSID from table(QSYS2.ACTIVE_JOB_INFO( JOB_NAME_FILTER => (select id from SSH_DETAIL), DETAILED_INFO => 'ALL')) where INTERNAL_JOB_ID = (select iid from SSH_DETAIL) + `; + + const [result] = await this.runSQL(sql); + return Number(result.CCSID === CCSID_NOCONVERSION ? result.DEFAULT_CCSID : result.CCSID); + } + + async getSysEnvVars() { + const systemEnvVars = await this.runSQL([ + `select ENVIRONMENT_VARIABLE_NAME, ENVIRONMENT_VARIABLE_VALUE`, + `from qsys2.environment_variable_info where environment_variable_type = 'SYSTEM'` + ].join(` `)) as { ENVIRONMENT_VARIABLE_NAME: string, ENVIRONMENT_VARIABLE_VALUE: string }[]; + + let result: { [name: string]: string; } = {}; + + systemEnvVars.forEach(row => { + result[row.ENVIRONMENT_VARIABLE_NAME] = row.ENVIRONMENT_VARIABLE_VALUE; + }); + + return result; + } + /** * Generates path to a temp file on the IBM i * @param {string} key Key to the temp file to be re-used @@ -1195,9 +1328,6 @@ export default class IBMi { } parserMemberPath(string: string, checkExtension?: boolean): MemberParts { - const variant_chars_local = this.variantChars.local; - const validQsysName = new RegExp(`^[A-Z0-9${variant_chars_local}][A-Z0-9_${variant_chars_local}.]{0,9}$`); - // Remove leading slash const upperCasedString = this.upperCaseName(string); const path = upperCasedString.startsWith(`/`) ? upperCasedString.substring(1).split(`/`) : upperCasedString.split(`/`); @@ -1211,13 +1341,13 @@ export default class IBMi { if (!library || !file || !name) { throw new Error(`Invalid path: ${string}. Use format LIB/SPF/NAME.ext`); } - if (asp && !validQsysName.test(asp)) { + if (asp && !this.validQsysName(asp)) { throw new Error(`Invalid ASP name: ${asp}`); } - if (!validQsysName.test(library)) { + if (!this.validQsysName(library)) { throw new Error(`Invalid Library name: ${library}`); } - if (!validQsysName.test(file)) { + if (!this.validQsysName(file)) { throw new Error(`Invalid Source File name: ${file}`); } @@ -1226,7 +1356,7 @@ export default class IBMi { throw new Error(`Source Type extension is required.`); } - if (!validQsysName.test(name)) { + if (!this.validQsysName(name)) { throw new Error(`Invalid Source Member name: ${name}`); } // The extension/source type has nearly the same naming rules as @@ -1235,7 +1365,7 @@ export default class IBMi { // the final period (so we know it won't contain a period). // But, a blank extension is valid. const extension = parsedPath.ext.substring(1); - if (extension && !validQsysName.test(extension)) { + if (extension && !this.validQsysName(extension)) { throw new Error(`Invalid Source Member Extension: ${extension}`); } @@ -1280,7 +1410,7 @@ export default class IBMi { result = result.replace(new RegExp(`[${fromChars[i]}]`, `g`), toChars[i]); }; - return result; + return result } async uploadFiles(files: { local: string | vscode.Uri, remote: string }[], options?: node_ssh.SSHPutFilesOptions) { await this.client.putFiles(files.map(f => { return { local: this.fileToPath(f.local), remote: f.remote } }), options); @@ -1379,46 +1509,80 @@ export default class IBMi { * @param statements * @returns a Result set */ - async runSQL(statements: string): Promise { + async runSQL(statements: string, options: { fakeBindings?: (string | number)[], forceSafe?: boolean } = {}): Promise { const { 'QZDFMDB2.PGM': QZDFMDB2 } = this.remoteFeatures; + const possibleChangeCommand = (this.userCcsidInvalid ? `@CHGJOB CCSID(${this.getCcsid()});\n` : ''); if (QZDFMDB2) { - const ccsidDetail = this.getEncoding(); - const useCcsid = ccsidDetail.fallback && !ccsidDetail.invalid ? ccsidDetail.ccsid : undefined; - const possibleChangeCommand = (useCcsid ? `@CHGJOB CCSID(${useCcsid});\n` : ''); - + // CHGJOB not required here. It will use the job CCSID, or the runtime CCSID. let input = Tools.fixSQL(`${possibleChangeCommand}${statements}`, true); - let returningAsCsv: WrapResult | undefined; + let command = `LC_ALL=EN_US.UTF-8 system "call QSYS/QZDFMDB2 PARM('-d' '-i' '-t')"` + let useCsv = options.forceSafe; + + // Use custom QSH if available + if (this.canUseCqsh) { + const customQsh = this.getComponent(cqsh)!; + command = `${await customQsh.getPath()} -c "system \\"call QSYS/QZDFMDB2 PARM('-d' '-i' '-t')\\""`; + } + + if (this.requiresTranslation) { + // If we can't fix the input, then we can attempt to convert ourselves and then use the CSV. + input = this.sysNameInAmerican(input); + useCsv = true; + } - if (this.qccsid === 65535) { - let list = input.split(`\n`).join(` `).split(`;`).filter(x => x.trim().length > 0); - const lastStmt = list.pop()?.trim(); - const asUpper = lastStmt?.toUpperCase(); - - if (lastStmt) { - if ((asUpper?.startsWith(`SELECT`) || asUpper?.startsWith(`WITH`))) { - const copyToImport = this.getComponent(CopyToImport); - if (copyToImport) { - returningAsCsv = copyToImport.wrap(lastStmt); - list.push(...returningAsCsv.newStatements); - input = list.join(`;\n`); + // Fix up the parameters + let list = input.split(`\n`).join(` `).split(`;`).filter(x => x.trim().length > 0); + let lastStmt = list.pop()?.trim(); + const asUpper = lastStmt?.toUpperCase(); + + // We always need to use the CSV to get the values back correctly from the database. + if (lastStmt) { + const fakeBindings = options.fakeBindings; + if (lastStmt.includes(`?`) && fakeBindings && fakeBindings.length > 0) { + const parts = lastStmt.split(`?`); + + lastStmt = ``; + for (let partsIndex = 0; partsIndex < parts.length; partsIndex++) { + lastStmt += parts[partsIndex]; + if (fakeBindings[partsIndex] !== undefined) { + switch (typeof fakeBindings[partsIndex]) { + case `number`: + lastStmt += fakeBindings[partsIndex]; + break; + + case `string`: + lastStmt += Tools.bufferToUx(fakeBindings[partsIndex] as string); + break; + } } } + } - if (!returningAsCsv) { - list.push(lastStmt); + // Return as CSV when needed + if (useCsv && (asUpper?.startsWith(`SELECT`) || asUpper?.startsWith(`WITH`))) { + const copyToImport = this.getComponent(CopyToImport); + if (copyToImport) { + returningAsCsv = copyToImport.wrap(lastStmt); + list.push(...returningAsCsv.newStatements); } } + + if (!returningAsCsv) { + list.push(lastStmt); + } + + input = list.join(`;\n`); } const output = await this.sendCommand({ - command: `LC_ALL=EN_US.UTF-8 system "call QSYS/QZDFMDB2 PARM('-d' '-i' '-t')"`, + command, stdin: input }) if (output.stdout) { - Tools.db2Parse(output.stdout, input); + const fromStdout = Tools.db2Parse(output.stdout, input); if (returningAsCsv) { // Will throw an error if stdout contains an error @@ -1441,7 +1605,7 @@ export default class IBMi { throw new Error(`There was an error fetching the SQL result set.`) } else { - return Tools.db2Parse(output.stdout); + return fromStdout; } } } @@ -1449,21 +1613,31 @@ export default class IBMi { throw new Error(`There is no way to run SQL on this system.`); } - getEncoding() { - const fallbackToDefault = ((this.jobCcsid < 1 || this.jobCcsid === 65535) && this.userDefaultCCSID > 0); - const ccsid = fallbackToDefault ? this.userDefaultCCSID : this.jobCcsid; - return { - fallback: fallbackToDefault, - ccsid, - invalid: (ccsid < 1 || ccsid === 65535) - }; + validQsysName(name: string): boolean { + // First character can only be A-Z, or a variant character + // The rest can be A-Z, 0-9, _, ., or a variant character + if (!this.variantChars.qsysNameRegex) { + const regexTest = `^[A-Z${this.variantChars.local}][A-Z0-9_.${this.variantChars.local}]{0,9}$`; + this.variantChars.qsysNameRegex = new RegExp(regexTest); + } + + if (name.length > 10) return false; + name = this.upperCaseName(name); + return this.variantChars.qsysNameRegex.test(name); + } + + getCcsid() { + const fallbackToDefault = ((this.userJobCcsid < 1 || this.userJobCcsid === CCSID_NOCONVERSION) && this.userDefaultCCSID > 0); + const ccsid = fallbackToDefault ? this.userDefaultCCSID : this.userJobCcsid; + return ccsid; } getCcsids() { return { qccsid: this.qccsid, - runtimeCcsid: this.jobCcsid, + runtimeCcsid: this.userJobCcsid, userDefaultCCSID: this.userDefaultCCSID, + sshdCcsid: this.sshdCcsid }; } diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 65c5956c6..d4f2dbde6 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -4,6 +4,7 @@ import path from 'path'; import tmp from 'tmp'; import util from 'util'; import { MarkdownString, window } from 'vscode'; +import { GetMemberInfo } from '../components/getMemberInfo'; import { ObjectTypes } from '../filesystems/qsys/Objects'; import { AttrOperands, CommandResult, IBMiError, IBMiMember, IBMiObject, IFSFile, QsysPath } from '../typings'; import { ConnectionConfiguration } from './Configuration'; @@ -117,7 +118,7 @@ export default class IBMiContent { ccsid = await this.getNotUTF8CCSID(features.attr, originalPath); } - await writeFileAsync(tmpobj, content, {encoding: encoding as BufferEncoding}); + await writeFileAsync(tmpobj, content, { encoding: encoding as BufferEncoding }); if (ccsid && features.iconv) { // Upload our file to the same temp file, then write convert it back to the original ccsid @@ -428,25 +429,26 @@ export default class IBMiContent { async validateLibraryList(newLibl: string[]): Promise { let badLibs: string[] = []; - newLibl = newLibl.filter(lib => { - if (lib.match(/^\d/)) { - badLibs.push(lib); - return false; - } + newLibl = newLibl + .filter(lib => { + if (lib.match(/^\d/)) { + badLibs.push(lib); + return false; + } - if (lib.length > 10) { - badLibs.push(lib); - return false; - } + if (lib.length > 10) { + badLibs.push(lib); + return false; + } - return true; - }); + return true; + }); - const sanitized = Tools.sanitizeLibraryNames(newLibl); + const sanitized = Tools.sanitizeObjNamesForPase(newLibl); const result = await this.ibmi.sendQsh({ command: [ - `liblist -d ` + Tools.sanitizeLibraryNames(this.ibmi.defaultUserLibraries).join(` `), + `liblist -d ` + Tools.sanitizeObjNamesForPase(this.ibmi.defaultUserLibraries).join(` `), ...sanitized.map(lib => `liblist -a ` + lib) ].join(`; `) }); @@ -480,17 +482,17 @@ export default class IBMiContent { * @returns an array of IBMiObject */ async getObjectList(filters: { library: string; object?: string; types?: string[]; filterType?: FilterType }, sortOrder?: SortOrder): Promise { - const library = this.ibmi.upperCaseName(filters.library); - const americanLibrary = this.ibmi.sysNameInAmerican(library); - if (!await this.checkObject({ library: "QSYS", name: library, type: "*LIB" })) { - throw new Error(`Library ${library} does not exist.`); + const localLibrary = this.ibmi.upperCaseName(filters.library); + + if (localLibrary !== `QSYS`) { + if (!await this.checkObject({ library: "QSYS", name: localLibrary, type: "*LIB" })) { + throw new Error(`Library ${localLibrary} does not exist.`); + } } const singleEntry = filters.filterType !== 'regex' ? singleGenericName(filters.object) : undefined; const nameFilter = parseFilter(filters.object, filters.filterType); const objectFilter = filters.object && (nameFilter.noFilter || singleEntry) && filters.object !== `*` ? this.ibmi.upperCaseName(filters.object) : undefined; - const objectNameLike = () => objectFilter ? ` and t.SYSTEM_TABLE_NAME ${(objectFilter.includes('*') ? ` like ` : ` = `)} '${this.ibmi.sysNameInAmerican(objectFilter).replace('*', '%')}'` : ''; - const objectName = () => objectFilter ? `, OBJECT_NAME => '${objectFilter}'` : ''; const typeFilter = filters.types && filters.types.length > 1 ? (t: string) => filters.types?.includes(t) : undefined; const type = filters.types && filters.types.length === 1 && filters.types[0] !== '*' ? filters.types[0] : '*ALL'; @@ -498,20 +500,33 @@ export default class IBMiContent { const sourceFilesOnly = filters.types && filters.types.length === 1 && filters.types.includes(`*SRCPF`); const withSourceFiles = ['*ALL', '*SRCPF', '*FILE'].includes(type); + // Here's the downlow on CCSIDs here. + // SYSTABLES takes the name in the local format (with the local variant characters) + // OBJECT_STATISTICS takes the name in the system format + + const sourceFileNameLike = () => objectFilter ? ` and f.NAME ${(objectFilter.includes('*') ? ` like ` : ` = `)} '${objectFilter.replace('*', '%')}'` : ''; + + const objectName = () => objectFilter ? `, OBJECT_NAME => '${objectFilter}'` : ''; + let createOBJLIST: string[]; if (sourceFilesOnly) { //DSPFD only createOBJLIST = [ - `select `, - ` t.SYSTEM_TABLE_NAME as NAME,`, - ` '*FILE' as TYPE,`, - ` 'PF' as ATTRIBUTE,`, - ` t.TABLE_TEXT as TEXT,`, - ` 1 as IS_SOURCE,`, - ` t.ROW_LENGTH as SOURCE_LENGTH,`, - ` t.IASP_NUMBER as IASP_NUMBER`, - `from QSYS2.SYSTABLES as t`, - `where t.SYSTEM_TABLE_SCHEMA = '${americanLibrary}' and t.FILE_TYPE = 'S'${objectNameLike()}`, + `with SRCFILES as (`, + ` select `, + ` rtrim(cast(t.SYSTEM_TABLE_SCHEMA as char(10) for bit data)) as LIBRARY,`, + ` rtrim(cast(t.SYSTEM_TABLE_NAME as char(10) for bit data)) as NAME,`, + ` '*FILE' as TYPE,`, + ` 'PF' as ATTRIBUTE,`, + ` t.TABLE_TEXT as TEXT,`, + ` 1 as IS_SOURCE,`, + ` t.ROW_LENGTH as SOURCE_LENGTH,`, + ` t.IASP_NUMBER as IASP_NUMBER`, + ` from QSYS2.SYSTABLES as t`, + ` where t.FILE_TYPE = 'S'`, + `)`, + `SELECT * FROM SRCFILES as f`, + `where f.LIBRARY = '${localLibrary}'${sourceFileNameLike()}`, ]; } else if (!withSourceFiles) { //DSPOBJD only @@ -528,22 +543,27 @@ export default class IBMiContent { ` extract(epoch from (CHANGE_TIMESTAMP))*1000 as CHANGED,`, ` OBJOWNER as OWNER,`, ` OBJDEFINER as CREATED_BY`, - `from table(QSYS2.OBJECT_STATISTICS(OBJECT_SCHEMA => '${library.padEnd(10)}', OBJTYPELIST => '${type}'${objectName()}))`, + `from table(QSYS2.OBJECT_STATISTICS(OBJECT_SCHEMA => '${localLibrary}', OBJTYPELIST => '${type}'${objectName()}))`, ]; } else { //Both DSPOBJD and DSPFD createOBJLIST = [ - `with SRCPF as (`, + `with SRCFILES as (`, ` select `, - ` replace(replace(replace(t.SYSTEM_TABLE_NAME, '${this.ibmi.variantChars.american[2]}','${this.ibmi.variantChars.local[2]}'), '${this.ibmi.variantChars.american[1]}', '${this.ibmi.variantChars.local[1]}'), '${this.ibmi.variantChars.american[0]}', '${this.ibmi.variantChars.local[0]}') as NAME,`, + ` rtrim(cast(t.SYSTEM_TABLE_SCHEMA as char(10) for bit data)) as LIBRARY,`, + ` rtrim(cast(t.SYSTEM_TABLE_NAME as char(10) for bit data)) as NAME,`, ` '*FILE' as TYPE,`, ` 'PF' as ATTRIBUTE,`, ` t.TABLE_TEXT as TEXT,`, ` 1 as IS_SOURCE,`, - ` t.ROW_LENGTH as SOURCE_LENGTH`, + ` t.ROW_LENGTH as SOURCE_LENGTH,`, + ` t.IASP_NUMBER as IASP_NUMBER`, ` from QSYS2.SYSTABLES as t`, - ` where t.SYSTEM_TABLE_SCHEMA = '${americanLibrary}' and t.FILE_TYPE = 'S'${objectNameLike()}`, + ` where t.FILE_TYPE = 'S'`, + `), SRCPF as (`, + ` SELECT * FROM SRCFILES as f`, + ` where f.LIBRARY = '${localLibrary}'${sourceFileNameLike()}`, `), OBJD as (`, ` select `, ` OBJNAME as NAME,`, @@ -557,7 +577,7 @@ export default class IBMiContent { ` extract(epoch from (CHANGE_TIMESTAMP))*1000 as CHANGED,`, ` OBJOWNER as OWNER,`, ` OBJDEFINER as CREATED_BY`, - ` from table(QSYS2.OBJECT_STATISTICS(OBJECT_SCHEMA => '${library.padEnd(10)}', OBJTYPELIST => '${type}'${objectName()}))`, + ` from table(QSYS2.OBJECT_STATISTICS(OBJECT_SCHEMA => '${localLibrary}', OBJTYPELIST => '${type}'${objectName()}))`, ` )`, `select`, ` o.NAME,`, @@ -579,8 +599,8 @@ export default class IBMiContent { const objects = (await this.runStatements(createOBJLIST.join(`\n`))); return objects.map(object => ({ - library, - name: sourceFilesOnly ? this.ibmi.sysNameInLocal(String(object.NAME)) : String(object.NAME), + library: localLibrary, + name: Boolean(object.IS_SOURCE) ? this.ibmi.sysNameInLocal(String(object.NAME)) : String(object.NAME), type: String(object.TYPE), attribute: String(object.ATTRIBUTE), text: String(object.TEXT || ""), @@ -642,10 +662,10 @@ export default class IBMiContent { on ( b.SYSTEM_TABLE_SCHEMA, b.SYSTEM_TABLE_NAME ) = ( a.SYSTEM_TABLE_SCHEMA, a.SYSTEM_TABLE_NAME ) ) select * from MEMBERS - where LIBRARY = '${this.ibmi.sysNameInAmerican(library)}' - ${sourceFile !== `*ALL` ? `and SOURCE_FILE = '${this.ibmi.sysNameInAmerican(sourceFile)}'` : ``} - ${singleMember ? `and NAME like '${this.ibmi.sysNameInAmerican(singleMember)}'` : ''} - ${singleMemberExtension ? `and TYPE like '${this.ibmi.sysNameInAmerican(singleMemberExtension)}'` : ''} + where LIBRARY = '${library}' + ${sourceFile !== `*ALL` ? `and SOURCE_FILE = '${sourceFile}'` : ``} + ${singleMember ? `and NAME like '${singleMember}'` : ''} + ${singleMemberExtension ? `and TYPE like '${singleMemberExtension}'` : ''} order by ${sort.order === 'name' ? 'NAME' : 'CHANGED'} ${!sort.ascending ? 'DESC' : 'ASC'}`; const results = await this.ibmi.runSQL(statement); @@ -676,38 +696,9 @@ export default class IBMiContent { * @param filter: the criterias used to list the members * @returns */ - async getMemberInfo(library: string, sourceFile: string, member: string): Promise { - if (this.ibmi.remoteFeatures[`GETMBRINFO.SQL`]) { - const tempLib = this.config.tempLibrary; - const statement = `select * from table(${tempLib}.GETMBRINFO('${library}', '${sourceFile}', '${member}'))`; - - let results: Tools.DB2Row[] = []; - if (this.config.enableSQL) { - try { - results = await this.runSQL(statement); - } catch (e) { }; // Ignore errors, will return undefined. - } - else { - results = await this.getQTempTable([`create table QTEMP.MEMBERINFO as (${statement}) with data`], "MEMBERINFO"); - } - - if (results.length === 1 && results[0].ISSOURCE === 'Y') { - const result = results[0]; - const asp = this.ibmi.aspInfo[Number(results[0].ASP)]; - return { - library: result.LIBRARY, - file: result.FILE, - name: result.MEMBER, - extension: result.EXTENSION, - text: result.DESCRIPTION, - created: new Date(result.CREATED ? Number(result.CREATED) : 0), - changed: new Date(result.CHANGED ? Number(result.CHANGED) : 0) - } as IBMiMember - } - else { - return undefined; - } - } + getMemberInfo(library: string, sourceFile: string, member: string) { + const component = this.ibmi.getComponent(GetMemberInfo)!; + return component.getMemberInfo(library, sourceFile, member); } /** @@ -857,7 +848,7 @@ export default class IBMiContent { } async streamfileResolve(names: string[], directories: string[]): Promise { - const command = `for f in ${directories.flatMap(dir => names.map(name => `"${path.posix.join(dir, name)}"`)).join(` `)}; do if [ -f "$f" ]; then echo $f; break; fi; done`; + const command = `for f in ${directories.flatMap(dir => names.map(name => `"${Tools.escapePath(path.posix.join(dir, name), true)}"`)).join(` `)}; do if [ -f "$f" ]; then echo $f; break; fi; done`; const result = await this.ibmi.sendCommand({ command, @@ -950,10 +941,33 @@ export default class IBMiContent { return cl; } - async getAttributes(path: string | (QsysPath & { member?: string }), ...operands: AttrOperands[]) { - const target = Tools.escapePath(path = typeof path === 'string' ? path : this.ibmi.sysNameInAmerican(Tools.qualifyPath(path.library, path.name, path.member, path.asp))); + async getAttributes(path: string | (QsysPath & { member?: string }), ...operands: AttrOperands[]) { + const localPath = typeof path === `string` ? path : {...path}; + const assumeMember = typeof localPath === `object`; + let target: string; + + if (assumeMember) { + // If it's an object, we assume it's a member, therefore let's let qsh handle it (better for variants) + localPath.asp = localPath.asp ? this.ibmi.sysNameInAmerican(localPath.asp) : undefined; + localPath.library = this.ibmi.sysNameInAmerican(localPath.library); + localPath.name = this.ibmi.sysNameInAmerican(localPath.name); + localPath.member = localPath.member ? this.ibmi.sysNameInAmerican(localPath.member) : undefined; + target = Tools.qualifyPath(localPath.library, localPath.name, localPath.member || '', localPath.asp || '', true); + } else { + target = localPath; + } + + target = IBMi.escapeForShell(target); + + let result: CommandResult; + + if (assumeMember) { + result = await this.ibmi.sendQsh({ command: `${this.ibmi.remoteFeatures.attr} -p ${target} ${operands.join(" ")}`}); + } else { + // Take {DOES_THIS_WORK: `YESITDOES`} away, and all of a sudden names with # aren't found. + result = await this.ibmi.sendCommand({ command: `/QOpenSys/${this.ibmi.remoteFeatures.attr} -p ${target} ${operands.join(" ")}`, env: {DOES_THIS_WORK: `YESITDOES`}}); + } - const result = await this.ibmi.sendCommand({ command: `${this.ibmi.remoteFeatures.attr} -p ${target} ${operands.join(" ")}` }); if (result.code === 0) { return result.stdout .split('\n') @@ -966,7 +980,7 @@ export default class IBMiContent { } async countMembers(path: QsysPath) { - return this.countFiles(Tools.qualifyPath(path.library, path.name, undefined, path.asp)) + return this.countFiles(this.ibmi.sysNameInAmerican(Tools.qualifyPath(path.library, path.name, undefined, path.asp))) } async countFiles(directory: string) { diff --git a/src/api/Storage.ts b/src/api/Storage.ts index e994653a6..869a28c2e 100644 --- a/src/api/Storage.ts +++ b/src/api/Storage.ts @@ -52,12 +52,12 @@ export type LastConnection = { }; export type CachedServerSettings = { + lastCheckedOnVersion: string | undefined; aspInfo: { [id: number]: string } qccsid: number | null; jobCcsid: number | null remoteFeatures: { [name: string]: string | undefined } remoteFeaturesKeys: string | null - variantChars: { american: string, local: string } badDataAreasChecked: boolean | null libraryListValidated: boolean | null pathChecked?: boolean diff --git a/src/api/Tools.ts b/src/api/Tools.ts index f60ab4a17..6b68d3402 100644 --- a/src/api/Tools.ts +++ b/src/api/Tools.ts @@ -5,9 +5,6 @@ import vscode from "vscode"; import { IBMiMessage, IBMiMessages, QsysPath } from '../typings'; import { API, GitExtension } from "./import/git"; -const MONTHS = [undefined, "Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; -const DAYS = [undefined,"Mon","Tue","Wed","Thu","Fri","Sat","Sun"]; - export namespace Tools { export class SqlError extends Error { public sqlstate: string = "0"; @@ -22,9 +19,7 @@ export namespace Tools { length: number } - export interface DB2Row extends Record { - - } + export interface DB2Row extends Record { } /** * Parse standard out for `/usr/bin/db2` @@ -156,6 +151,13 @@ export namespace Tools { return rows; } + export function bufferToUx(input: string) { + const hexString = Array.from(input) + .map(char => char.charCodeAt(0).toString(16).padStart(4, '0').toUpperCase()) + .join(''); + return `UX'${hexString}'`; + } + export function makeid(length: number = 8) { let text = `O_`; const possible = @@ -175,12 +177,16 @@ export namespace Tools { * @param iasp Optional: an iASP name */ export function qualifyPath(library: string, object: string, member?: string, iasp?: string, noEscape?: boolean) { - const libraryPath = library === `QSYS` ? `QSYS.LIB` : `QSYS.LIB/${Tools.sanitizeLibraryNames([library]).join(``)}.LIB`; + [library, object] = Tools.sanitizeObjNamesForPase([library, object]); + member = member ? Tools.sanitizeObjNamesForPase([member])[0] : undefined; + iasp = iasp ? Tools.sanitizeObjNamesForPase([iasp])[0] : undefined; + + const libraryPath = library === `QSYS` ? `QSYS.LIB` : `QSYS.LIB/${library}.LIB`; const filePath = object ? `${object}.FILE` : ''; const memberPath = member ? `/${member}.MBR` : ''; - const subPath = `${filePath}${memberPath}`; + const fullPath = `${libraryPath}/${filePath}${memberPath}`; - const result = (iasp && iasp.length > 0 ? `/${iasp}` : ``) + `/${libraryPath}/${noEscape ? subPath : Tools.escapePath(subPath)}`; + const result = (iasp && iasp.length > 0 ? `/${iasp}` : ``) + `/${noEscape ? fullPath : Tools.escapePath(fullPath)}`; return result; } @@ -214,9 +220,12 @@ export namespace Tools { * @param Path * @returns the escaped path */ - export function escapePath(Path: string): string { - const path = Path.replace(/'|"|\$|\\| /g, matched => `\\`.concat(matched)); - return path; + export function escapePath(Path: string, alreadyQuoted = false): string { + if (alreadyQuoted) { + return Path.replace(/"|\$|\\/g, matched => `\\`.concat(matched)); + } else { + return Path.replace(/'|"|\$|\\| /g, matched => `\\`.concat(matched)); + } } let gitLookedUp: boolean; @@ -252,11 +261,9 @@ export namespace Tools { return text.charAt(0).toUpperCase() + text.slice(1); } - export function sanitizeLibraryNames(libraries: string[]): string[] { + export function sanitizeObjNamesForPase(libraries: string[]): string[] { return libraries .map(library => { - // Escape any $ signs - library = library.replace(/\$/g, `\\$`); // Quote libraries starting with # return library.startsWith(`#`) ? `"${library}"` : library; }); @@ -337,9 +344,9 @@ export namespace Tools { group.tabs.filter(tab => (tab.input instanceof vscode.TabInputText) && (uriToFind instanceof vscode.Uri ? areEquivalentUris(tab.input.uri, uriToFind) : tab.input.uri.path.startsWith(`${uriToFind}/`)) - ).forEach(tab => { - resourceTabs.push(tab); - }); + ).forEach(tab => { + resourceTabs.push(tab); + }); } return resourceTabs; } @@ -393,10 +400,18 @@ export namespace Tools { } export function assumeType(str: string) { + if (str.trim().length === 0) return ``; + // The number is already generated on the server. // So, we assume that if the string starts with a 0, it is a string. - if (str[0] === `0` || str.length > 10) return str; - return Number(str) || str; + if (/^0.+/.test(str) || str.length > 10) { + return str + } + const number = Number(str); + if(isNaN(number)){ + return str; + } + return number; } const activeContexts: Map = new Map; @@ -441,9 +456,9 @@ export namespace Tools { * @param timestamp an attr timestamp string * @returns a Date object */ - export function parseAttrDate(timestamp:string){ + export function parseAttrDate(timestamp: string) { const parts = /^([\w]{3}) ([\w]{3}) +([\d]+) ([\d]+:[\d]+:[\d]+) ([\d]+)$/.exec(timestamp); - if(parts){ + if (parts) { return Date.parse(`${parts[3].padStart(2, "0")} ${parts[2]} ${parts[5]} ${parts[4]} GMT`); } return 0; diff --git a/src/components/component.ts b/src/components/component.ts index 74e2d1e47..4c988a4e7 100644 --- a/src/components/component.ts +++ b/src/components/component.ts @@ -33,12 +33,26 @@ export type IBMiComponentType = new (c: IBMi) => T; * */ export abstract class IBMiComponent { + public static readonly InstallDirectory = `$HOME/.vscode/`; private state: ComponentState = `NotChecked`; + private cachedInstallDirectory: string | undefined; constructor(protected readonly connection: IBMi) { } + async getInstallDirectory() { + if (!this.cachedInstallDirectory) { + const result = await this.connection.sendCommand({ + command: `echo "${IBMiComponent.InstallDirectory}"`, + }); + + this.cachedInstallDirectory = result.stdout.trim() || `/home/${this.connection.currentUser.toLowerCase()}/.vscode/`; + } + + return this.cachedInstallDirectory; + } + getState() { return this.state; } diff --git a/src/components/cqsh/cqsh b/src/components/cqsh/cqsh new file mode 100644 index 000000000..363c5291e Binary files /dev/null and b/src/components/cqsh/cqsh differ diff --git a/src/components/cqsh/cqsh.c b/src/components/cqsh/cqsh.c new file mode 100644 index 000000000..b38bcaa74 --- /dev/null +++ b/src/components/cqsh/cqsh.c @@ -0,0 +1,14 @@ +#include +#include + +// gcc cqsh.c -Wl,-blibpath:/QOpenSys/usr/lib -o cqsh + +int main(int argc, char **argv) +{ + //re - initialize cached PASE converters + _SETCCSID(Qp2paseCCSID()); + + argv[0] = "/QOpenSys/usr/bin/qsh"; + + return execv(argv[0], &argv[0]); +} \ No newline at end of file diff --git a/src/components/cqsh/index.ts b/src/components/cqsh/index.ts new file mode 100644 index 000000000..5e803ab88 --- /dev/null +++ b/src/components/cqsh/index.ts @@ -0,0 +1,88 @@ + +import { stat } from "fs/promises"; +import { ComponentState, IBMiComponent } from "../component"; +import path from "path"; +import { extensions } from "vscode"; + +export class cqsh extends IBMiComponent { + getIdentification() { + return { name: 'cqsh', version: 1 }; + } + + getFileName() { + const id = this.getIdentification(); + return `${id.name}_${id.version}`; + } + + public async getPath() { + const installDir = await this.getInstallDirectory(); + return path.posix.join(installDir, this.getFileName()); + } + + protected async getRemoteState(): Promise { + const remotePath = await this.getPath(); + const result = await this.connection.content.testStreamFile(remotePath, "x"); + + if (!result) { + return `NotInstalled`; + } + + const testResult = await this.testCommand(); + + if (!testResult) { + return `Error`; + } + + return `Installed`; + } + + protected async update(): Promise { + const extensionPath = extensions.getExtension(`halcyontechltd.code-for-ibmi`)!.extensionPath; + const remotePath = await this.getPath(); + + const assetPath = path.join(extensionPath, `dist`, this.getFileName()); + const assetExistsLocally = await exists(assetPath); + + if (!assetExistsLocally) { + return `Error`; + } + + await this.connection.uploadFiles([{ local: assetPath, remote: remotePath }]); + + await this.connection.sendCommand({ + command: `chmod +x ${remotePath}`, + }); + + const testResult = await this.testCommand(); + + if (!testResult) { + return `Error`; + } + + return `Installed`; + } + + async testCommand() { + const remotePath = await this.getPath(); + const text = `Hello world`; + const result = await this.connection.sendCommand({ + stdin: `echo "${text}"`, + command: remotePath, + }); + + if (result.code !== 0 || result.stdout !== text) { + return false; + } + + return true; + } +} + +async function exists(path: string) { + try { + await stat(path); + return true; + } catch (e) { + return false; + } +} \ No newline at end of file diff --git a/src/components/manager.ts b/src/components/manager.ts index 60b8d9e0e..7f2ec36a8 100644 --- a/src/components/manager.ts +++ b/src/components/manager.ts @@ -30,6 +30,16 @@ export class ComponentManager { } + public getState() { + return Array.from(this.registered.keys()).map(k => { + const comp = this.registered.get(k)!; + return { + id: comp.getIdentification(), + state: comp.getState() + } + }); + } + public async startup() { const components = Array.from(extensionComponentRegistry.getComponents().values()).flatMap(a => a.flat()); for (const Component of components) { diff --git a/src/extension.ts b/src/extension.ts index 9f79c2d8e..a12f54704 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -34,6 +34,7 @@ import { initializeIFSBrowser } from "./views/ifsBrowser"; import { initializeObjectBrowser } from "./views/objectBrowser"; import { initializeSearchView } from "./views/searchView"; import { SettingsUI } from "./webviews/settings"; +import { cqsh } from "./components/cqsh"; export async function activate(context: ExtensionContext): Promise { // Use the console to output diagnostic information (console.log) and errors (console.error) @@ -127,6 +128,7 @@ export async function activate(context: ExtensionContext): Promise commands.executeCommand("code-for-ibmi.refreshProfileView"); }); + extensionComponentRegistry.registerComponent(context, cqsh); extensionComponentRegistry.registerComponent(context, GetNewLibl); extensionComponentRegistry.registerComponent(context, GetMemberInfo); extensionComponentRegistry.registerComponent(context, CopyToImport); diff --git a/src/filesystems/qsys/QSysFs.ts b/src/filesystems/qsys/QSysFs.ts index 46898f74c..39d649f97 100644 --- a/src/filesystems/qsys/QSysFs.ts +++ b/src/filesystems/qsys/QSysFs.ts @@ -85,10 +85,6 @@ export class QSysFS implements vscode.FileSystemProvider { if (connection.sqlRunnerAvailable()) { this.extendedMemberSupport = true; this.sourceDateHandler.changeSourceDateMode(config.sourceDateMode); - const ccsidDetail = connection.getEncoding(); - if (ccsidDetail.invalid) { - vscode.window.showWarningMessage(`Source date support is enabled, but CCSID is 65535. If you encounter problems with source date support, please disable it in the settings.`); - } } else { vscode.window.showErrorMessage(`Source date support is enabled, but the remote system does not support SQL. Source date support will be disabled.`); } diff --git a/src/filesystems/qsys/extendedContent.ts b/src/filesystems/qsys/extendedContent.ts index 42497cb2f..f7a13fc56 100644 --- a/src/filesystems/qsys/extendedContent.ts +++ b/src/filesystems/qsys/extendedContent.ts @@ -52,11 +52,13 @@ export class ExtendedIBMiContent { let rows; if (sourceColourSupport) rows = await connection.runSQL( - `select srcdat, rtrim(translate(srcdta, ${SEU_GREEN_UL_RI_temp}, ${SEU_GREEN_UL_RI})) as srcdta from ${aliasPath}` + `select srcdat, rtrim(translate(srcdta, ${SEU_GREEN_UL_RI_temp}, ${SEU_GREEN_UL_RI})) as srcdta from ${aliasPath}`, + {forceSafe: true} ); else rows = await connection.runSQL( - `select srcdat, srcdta from ${aliasPath}` + `select srcdat, srcdta from ${aliasPath}`, + {forceSafe: true} ); if (rows.length === 0) { @@ -143,6 +145,7 @@ export class ExtendedIBMiContent { sequence = 0; for (let i = 0; i < sourceData.length; i++) { sequence = decimalSequence ? ((i + 1) / 100) : i + 1; + sourceData[i] = sourceData[i].trimEnd(); if (sourceData[i].length > recordLength) { sourceData[i] = sourceData[i].substring(0, recordLength); } diff --git a/src/testing/action.ts b/src/testing/action.ts index b2e3b3611..69a5bc5c0 100644 --- a/src/testing/action.ts +++ b/src/testing/action.ts @@ -32,6 +32,7 @@ let currentLibrary: string; export const ActionSuite: TestSuite = { name: `Action tests`, + notConcurrent: true, before: async () => { const config = instance.getConfig(); const storage = instance.getStorage(); diff --git a/src/testing/connection.ts b/src/testing/connection.ts index cbe3a556d..e876543da 100644 --- a/src/testing/connection.ts +++ b/src/testing/connection.ts @@ -309,17 +309,14 @@ export const ConnectionSuite: TestSuite = { const variantsBackup = connection.variantChars.local; try { - const checkVariants = () => connection.variantChars.local !== connection.variantChars.local.toLocaleUpperCase(); //CCSID 297 variants connection.variantChars.local = '£à$'; - connection.dangerousVariants = checkVariants(); assert.strictEqual(connection.dangerousVariants, true); assert.strictEqual(connection.upperCaseName("àTesT£ye$"), "àTEST£YE$"); assert.strictEqual(connection.upperCaseName("test_cAsE"), "TEST_CASE"); //CCSID 37 variants connection.variantChars.local = '#@$'; - connection.dangerousVariants = checkVariants(); assert.strictEqual(connection.dangerousVariants, false); assert.strictEqual(connection.upperCaseName("@TesT#ye$"), "@TEST#YE$"); assert.strictEqual(connection.upperCaseName("test_cAsE"), "TEST_CASE"); diff --git a/src/testing/content.ts b/src/testing/content.ts index acbef4a9b..eb13ded66 100644 --- a/src/testing/content.ts +++ b/src/testing/content.ts @@ -249,35 +249,6 @@ export const ContentSuite: TestSuite = { } }, - { - name: `Test downloadMemberContent with dollar`, test: async () => { - const content = instance.getContent(); - const config = instance.getConfig(); - const connection = instance.getConnection(); - const tempLib = config!.tempLibrary, - tempSPF = `TESTINGS`, - tempMbr = Tools.makeid(2) + `$` + Tools.makeid(2); - - await connection!.runCommand({ - command: `CRTSRCPF ${tempLib}/${tempSPF} MBR(*NONE)`, - environment: `ile` - }); - - await connection!.runCommand({ - command: `ADDPFM FILE(${tempLib}/${tempSPF}) MBR(${tempMbr}) `, - environment: `ile` - }); - - const baseContent = `Hello world\r\n`; - - const uploadResult = await content?.uploadMemberContent(undefined, tempLib, tempSPF, tempMbr, baseContent); - assert.ok(uploadResult); - - const memberContent = await content?.downloadMemberContent(undefined, tempLib, tempSPF, tempMbr); - assert.strictEqual(memberContent, baseContent); - }, - }, - {name: `Ensure source lines are correct`, test: async () => { const connection = instance.getConnection(); const config = instance.getConfig()!; @@ -366,12 +337,10 @@ export const ContentSuite: TestSuite = { }, { - name: `Test getTable (SQL disabled)`, test: async () => { + name: `Test getTable`, test: async () => { const connection = instance.getConnection(); const content = instance.getContent(); - // SQL needs to be disabled for this test. - connection!.enableSQL = false; const rows = await content?.getTable(`qiws`, `qcustcdt`, `*all`); assert.notStrictEqual(rows?.length, 0); @@ -556,53 +525,6 @@ export const ContentSuite: TestSuite = { assert.strictEqual(actbpgm?.file, `MIH`); } }, - - { - name: `getMemberList (SQL compared to nosql)`, test: async () => { - const connection = instance.getConnection(); - const content = instance.getContent(); - - // First we fetch the members in SQL mode - const membersA = await content?.getMemberList({ library: `qsysinc`, sourceFile: `mih` }); - - assert.notStrictEqual(membersA?.length, 0); - - // Then we fetch the members without SQL - connection!.enableSQL = false; - - try { - await content?.getMemberList({ library: `qsysinc`, sourceFile: `mih` }); - assert.fail(`Should have thrown an error`); - } catch (e) { - // This fails because getMemberList has no ability to fetch members without SQL - assert.ok(e); - } - } - }, - - { - name: `getMemberList (name filter, SQL compared to nosql)`, test: async () => { - const connection = instance.getConnection(); - const content = instance.getContent(); - - // First we fetch the members in SQL mode - connection!.enableSQL = true; - const membersA = await content?.getMemberList({ library: `qsysinc`, sourceFile: `mih`, members: 'C*' }); - - assert.notStrictEqual(membersA?.length, 0); - - // Then we fetch the members without SQL - connection!.enableSQL = false; - - try { - await content?.getMemberList({ library: `qsysinc`, sourceFile: `mih`, members: 'C*' }); - assert.fail(`Should have thrown an error`); - } catch (e) { - // This fails because getMemberList has no ability to fetch members without SQL - assert.ok(e); - } - } - }, { name: `getMemberList (advanced filtering)`, test: async () => { const content = instance.getContent(); @@ -709,8 +631,13 @@ export const ContentSuite: TestSuite = { assert.strictEqual(memberInfoB?.extension === `CPP`, true); assert.strictEqual(memberInfoB?.text === `C++ HEADER`, true); - const memberInfoC = await content?.getMemberInfo(`QSYSINC`, `H`, `OH_NONO`); - assert.ok(!memberInfoC); + try{ + await content?.getMemberInfo(`QSYSINC`, `H`, `OH_NONO`) + } + catch(error: any){ + assert.ok(error instanceof Tools.SqlError); + assert.strictEqual(error.sqlstate, "38501"); + } } }, { @@ -848,74 +775,6 @@ export const ContentSuite: TestSuite = { }); } }, - { - name: "Listing objects with variants", - test: async () => { - const connection = instance.getConnection(); - const content = instance.getConnection()?.content; - if (connection && content && connection.getEncoding().ccsid !== 37) { - const ccsid = connection.getEncoding().ccsid; - let library = `TESTLIB${connection.variantChars.local}`; - let skipLibrary = false; - const sourceFile = `TESTFIL${connection.variantChars.local}`; - const members: string[] = []; - for (let i = 0; i < 5; i++) { - members.push(`TSTMBR${connection.variantChars.local}${i}`); - } - try { - const crtLib = await connection.runCommand({ command: `CRTLIB LIB(${library}) TYPE(*PROD)`, noLibList: true }); - if (Tools.parseMessages(crtLib.stderr).findId("CPD0032")) { - //Not authorized: carry on, skip library name test - library = connection.config?.tempLibrary!; - skipLibrary = true - } - const crtSrcPF = await connection.runCommand({ command: `CRTSRCPF FILE(${library}/${sourceFile}) RCDLEN(112) CCSID(${ccsid})`, noLibList: true }); - if ((crtLib.code === 0 || skipLibrary) && crtSrcPF.code === 0) { - for (const member of members) { - const addPFM = await connection.runCommand({ command: `ADDPFM FILE(${library}/${sourceFile}) MBR(${member}) SRCTYPE(TXT)`, noLibList: true }); - if (addPFM.code !== 0) { - throw new Error(`Failed to create member ${member}: ${addPFM.stderr}`); - } - } - } - else { - throw new Error(`Failed to create library and source file: ${crtLib.stderr || crtLib.stderr}`) - } - - if (!skipLibrary) { - const [expectedLibrary] = await content.getLibraries({ library }); - assert.ok(expectedLibrary); - assert.strictEqual(library, expectedLibrary.name); - } - - const checkFile = (expectedObject: IBMiObject) => { - assert.ok(expectedObject); - assert.ok(expectedObject.sourceFile, `${expectedObject.name} not a source file`); - assert.strictEqual(expectedObject.name, sourceFile); - assert.strictEqual(expectedObject.library, library); - }; - - const [expectedObject] = await content.getObjectList({ library, object: sourceFile, types: ["*ALL"] }); - checkFile(expectedObject); - - const [expectedSourceFile] = await content.getObjectList({ library, object: sourceFile, types: ["*SRCPF"] }); - checkFile(expectedSourceFile); - - const expectedMembers = await content.getMemberList({ library, sourceFile }); - assert.ok(expectedMembers); - assert.ok(expectedMembers.every(member => members.find(m => m === member.name))); - } - finally { - if (!skipLibrary && await content.checkObject({ library: "QSYS", name: library, type: "*LIB" })) { - await connection.runCommand({ command: `DLTLIB LIB(${library})`, noLibList: true }) - } - if (skipLibrary && await content.checkObject({ library, name: sourceFile, type: "*FILE" })) { - await connection.runCommand({ command: `DLTF FILE(${library}/${sourceFile})`, noLibList: true }) - } - } - } - } - }, { name: `Test long library name`, test: async () => { const connection = instance.getConnection()!; diff --git a/src/testing/deployTools.ts b/src/testing/deployTools.ts index 96f75c7c7..1fe76c6ad 100644 --- a/src/testing/deployTools.ts +++ b/src/testing/deployTools.ts @@ -82,6 +82,7 @@ export const fakeProject: Folder = { export const DeployToolsSuite: TestSuite = { name: `Deploy Tools API tests`, + notConcurrent: true, before: async () => { const features = instance.getConnection()?.remoteFeatures; assert.ok(features?.stat, "stat is required to run deploy tools test suite"); diff --git a/src/testing/encoding.ts b/src/testing/encoding.ts index fe3fa8083..65f5e1923 100644 --- a/src/testing/encoding.ts +++ b/src/testing/encoding.ts @@ -1,12 +1,13 @@ import assert from "assert"; -import tmp from 'tmp'; -import util, { TextDecoder } from 'util'; +import os from "os"; import { Uri, workspace } from "vscode"; import { TestSuite } from "."; +import IBMi from "../api/IBMi"; import { Tools } from "../api/Tools"; -import { instance } from "../instantiate"; -import { CommandResult } from "../typings"; import { getMemberUri } from "../filesystems/qsys/QSysFs"; +import { instance } from "../instantiate"; +import { IBMiObject } from "../typings"; +import path from "path"; const contents = { '37': [`Hello world`], @@ -18,17 +19,404 @@ const contents = { '420': [`Hello world`, `ص ث ب`], } +const SHELL_CHARS = [`$`, `#`]; + const rtlEncodings = [`420`]; +async function runCommandsWithCCSID(connection: IBMi, commands: string[], ccsid: number) { + const testPgmSrcFile = `TESTING`; + const config = connection.config!; + + const tempLib = config.tempLibrary; + const testPgmName = `T${commands.length}${ccsid}`; + const sourceFileCreated = await connection!.runCommand({ command: `CRTSRCPF FILE(${tempLib}/${testPgmSrcFile}) RCDLEN(112) CCSID(${ccsid})`, noLibList: true }); + + await connection.content.uploadMemberContent(undefined, tempLib, testPgmSrcFile, testPgmName, commands.join(`\n`)); + + const compileCommand = `CRTBNDCL PGM(${tempLib}/${testPgmName}) SRCFILE(${tempLib}/${testPgmSrcFile}) SRCMBR(${testPgmName}) REPLACE(*YES)`; + const compileResult = await connection.runCommand({ command: compileCommand, noLibList: true }); + + if (compileResult.code !== 0) { + return compileResult; + } + + const callCommand = `CALL ${tempLib}/${testPgmName}`; + const result = await connection.runCommand({ command: callCommand, noLibList: true }); + + return result; +} + export const EncodingSuite: TestSuite = { name: `Encoding tests`, before: async () => { - const config = instance.getConfig()!; - assert.ok(config.enableSourceDates, `Source dates must be enabled for this test.`); + const config = instance.getConfig(); + if (config) { + assert.ok(config.enableSourceDates, `Source dates must be enabled for this test.`); + } }, - tests: Object.keys(contents).map(ccsid => { - return { + tests: [ + { + name: `Prove that input strings are messed up by CCSID`, test: async () => { + const connection = instance.getConnection(); + let howManyTimesItMessedUpTheResult = 0; + + for (const strCcsid in contents) { + const data = contents[strCcsid as keyof typeof contents].join(``); + + // Note that it always works with the buffer! + const sqlA = `select ? as THEDATA from sysibm.sysdummy1`; + const resultA = await connection?.runSQL(sqlA, { fakeBindings: [data], forceSafe: true }); + assert.ok(resultA?.length); + + const sqlB = `select '${data}' as THEDATA from sysibm.sysdummy1`; + const resultB = await connection?.runSQL(sqlB, { forceSafe: true }); + assert.ok(resultB?.length); + + assert.strictEqual(resultA![0].THEDATA, data); + if (resultB![0].THEDATA !== data) { + howManyTimesItMessedUpTheResult++; + } + } + + assert.ok(howManyTimesItMessedUpTheResult); + } + }, + { + name: `Compare Unicode to EBCDIC successfully`, test: async () => { + const connection = instance.getConnection(); + + const sql = `select table_name, table_owner from qsys2.systables where table_schema = ? and table_name = ?`; + const result = await connection?.runSQL(sql, { fakeBindings: [`QSYS2`, `SYSCOLUMNS`] }); + assert.ok(result?.length); + } + }, + { + name: `Run variants through shells`, test: async () => { + const connection = instance.getConnection(); + + const text = `Hello${connection?.variantChars.local}world`; + const basicCommandA = `echo "${IBMi.escapeForShell(text)}"`; + const basicCommandB = `echo '${text}'`; + const basicCommandC = `echo 'abc'\\''123'`; + const printEscapeChar = `echo "\\\\"`; + const setCommand = `set`; + + const setResult = await connection?.sendQsh({ command: setCommand }); + + const qshEscapeResult = await connection?.sendQsh({ command: printEscapeChar }); + const paseEscapeResult = await connection?.sendCommand({ command: printEscapeChar }); + + console.log(qshEscapeResult?.stdout); + console.log(paseEscapeResult?.stdout); + + const qshTextResultA = await connection?.sendQsh({ command: basicCommandA }); + const paseTextResultA = await connection?.sendCommand({ command: basicCommandA }); + + const qshTextResultB = await connection?.sendQsh({ command: basicCommandB }); + const paseTextResultB = await connection?.sendCommand({ command: basicCommandB }); + + const qshTextResultC = await connection?.sendQsh({ command: basicCommandC }); + const paseTextResultC = await connection?.sendCommand({ command: basicCommandC }); + + assert.strictEqual(paseEscapeResult?.stdout, `\\`); + assert.strictEqual(qshTextResultA?.stdout, text); + assert.strictEqual(paseTextResultA?.stdout, text); + assert.strictEqual(qshTextResultB?.stdout, text); + assert.strictEqual(paseTextResultB?.stdout, text); + } + }, + { + name: `streamfileResolve with dollar`, test: async () => { + const connection = instance.getConnection()!; + + await connection.withTempDirectory(async tempDir => { + const tempFile = path.posix.join(tempDir, `$hello`); + await connection.content.createStreamFile(tempFile); + + const resolved = await connection.content.streamfileResolve([tempFile], [`/`]); + + assert.strictEqual(resolved, tempFile); + }); + } + }, + ...SHELL_CHARS.map(char => ({ + name: `Test streamfiles with shell character ${char}`, test: async () => { + const connection = instance.getConnection()!; + + const nameCombos = [`${char}ABC`, `ABC${char}`, `${char}ABC${char}`, `A${char}C`]; + + await connection.withTempDirectory(async tempDir => { + for (const name of nameCombos) { + const tempFile = path.posix.join(tempDir, `${name}.txt`); + await connection.content.createStreamFile(tempFile); + + const resolved = await connection.content.streamfileResolve([tempFile], [`/`]); + assert.strictEqual(resolved, tempFile); + + const attributes = await connection.content.getAttributes(resolved, `CCSID`); + assert.ok(attributes); + + const uri = Uri.from({scheme: `streamfile`, path: tempFile}); + + await workspace.fs.writeFile(uri, Buffer.from(`Hello world`, `utf8`)); + + const streamfileContents = await workspace.fs.readFile(uri); + assert.strictEqual(streamfileContents.toString(), `Hello world`); + } + }); + } + })), + ...SHELL_CHARS.map(char => ({ + name: `Test members with shell character ${char}`, test: async () => { + const content = instance.getContent(); + const config = instance.getConfig(); + const connection = instance.getConnection()!; + + if (!connection.variantChars.local.includes(char)) { + // This test will fail if $ is not a variant character, + // since we're testing object names here + return; + } + + const tempLib = config!.tempLibrary, + tempSPF = `TESTINGS`, + tempMbr = char + Tools.makeid(4) + + await connection!.runCommand({ + command: `CRTSRCPF ${tempLib}/${tempSPF} MBR(*NONE)`, + environment: `ile` + }); + + await connection!.runCommand({ + command: `ADDPFM FILE(${tempLib}/${tempSPF}) MBR(${tempMbr}) `, + environment: `ile` + }); + + const baseContent = `Hello world\r\n`; + + const attributes = await content?.getAttributes({ library: tempLib, name: tempSPF, member: tempMbr }, `CCSID`); + assert.ok(attributes); + + const uploadResult = await content?.uploadMemberContent(undefined, tempLib, tempSPF, tempMbr, baseContent); + assert.ok(uploadResult); + + const memberContentA = await content?.downloadMemberContent(undefined, tempLib, tempSPF, tempMbr); + assert.strictEqual(memberContentA, baseContent); + + const memberUri = getMemberUri({ library: tempLib, file: tempSPF, name: tempMbr, extension: `TXT` }); + + const memberContentB = await workspace.fs.readFile(memberUri); + let contentStr = new TextDecoder().decode(memberContentB); + assert.ok(contentStr.includes(`Hello world`)); + + await workspace.fs.writeFile(memberUri, Buffer.from(`Woah`, `utf8`)); + + const memberContentBuf = await workspace.fs.readFile(memberUri); + let fileContent = new TextDecoder().decode(memberContentBuf); + + assert.ok(fileContent.includes(`Woah`)); + } + })), + { + name: "Listing objects with variants", + test: async () => { + const connection = instance.getConnection(); + const content = instance.getConnection()?.content; + if (connection && content) { + const tempLib = connection.config?.tempLibrary!; + const ccsid = connection.getCcsid(); + + let library = `TESTLIB${connection.variantChars.local}`; + let skipLibrary = false; + const sourceFile = `${connection.variantChars.local}TESTFIL`; + const dataArea = `TSTDTA${connection.variantChars.local}`; + const members: string[] = []; + + for (let i = 0; i < 5; i++) { + members.push(`TSTMBR${connection.variantChars.local}${i}`); + } + + await connection.runCommand({ command: `DLTLIB LIB(${library})`, noLibList: true }); + + const crtLib = await connection.runCommand({ command: `CRTLIB LIB(${library}) TYPE(*PROD)`, noLibList: true }); + if (Tools.parseMessages(crtLib.stderr).findId("CPD0032")) { + //Not authorized: carry on, skip library name test + library = tempLib; + skipLibrary = true + } + + let commands: string[] = []; + + commands.push(`CRTSRCPF FILE(${library}/${sourceFile}) RCDLEN(112) CCSID(${ccsid})`); + for (const member of members) { + commands.push(`ADDPFM FILE(${library}/${sourceFile}) MBR(${member}) SRCTYPE(TXT)`); + } + + commands.push(`CRTDTAARA DTAARA(${library}/${dataArea}) TYPE(*CHAR) LEN(50) VALUE('hi')`); + + // runCommandsWithCCSID proves that using variant characters in runCommand works! + const result = await runCommandsWithCCSID(connection, commands, ccsid); + assert.strictEqual(result.code, 0); + + if (!skipLibrary) { + const [expectedLibrary] = await content.getLibraries({ library }); + assert.ok(expectedLibrary); + assert.strictEqual(library, expectedLibrary.name); + + const validated = await connection.content.validateLibraryList([tempLib, library]); + assert.strictEqual(validated.length, 0); + + const libl = await content.getLibraryList([library]); + assert.strictEqual(libl.length, 1); + assert.strictEqual(libl[0].name, library); + } + + const checkFile = (expectedObject: IBMiObject) => { + assert.ok(expectedObject); + assert.ok(expectedObject.sourceFile, `${expectedObject.name} not a source file`); + assert.strictEqual(expectedObject.name, sourceFile); + assert.strictEqual(expectedObject.library, library); + }; + + const nameFilter = await content.getObjectList({ library, types: ["*ALL"], object: `${connection.variantChars.local[0]}*` }); + assert.strictEqual(nameFilter.length, 1); + assert.ok(nameFilter.some(obj => obj.library === library && obj.type === `*FILE` && obj.name === sourceFile)); + + const objectList = await content.getObjectList({ library, types: ["*ALL"] }); + assert.ok(objectList.some(obj => obj.library === library && obj.type === `*FILE` && obj.name === sourceFile && obj.sourceFile === true)); + assert.ok(objectList.some(obj => obj.library === library && obj.type === `*DTAARA` && obj.name === dataArea)); + + const expectedMembers = await content.getMemberList({ library, sourceFile }); + assert.ok(expectedMembers); + assert.ok(expectedMembers.every(member => members.find(m => m === member.name))); + + const sourceFilter = await content.getObjectList({ library, types: ["*SRCPF"], object: `${connection.variantChars.local[0]}*` }); + assert.strictEqual(sourceFilter.length, 1); + assert.ok(sourceFilter.some(obj => obj.library === library && obj.type === `*FILE` && obj.name === sourceFile)); + + const [expectDataArea] = await content.getObjectList({ library, object: dataArea, types: ["*DTAARA"] }); + assert.strictEqual(expectDataArea.name, dataArea); + assert.strictEqual(expectDataArea.library, library); + assert.strictEqual(expectDataArea.type, `*DTAARA`); + + const [expectedSourceFile] = await content.getObjectList({ library, object: sourceFile, types: ["*SRCPF"] }); + checkFile(expectedSourceFile); + + } + } + }, + { + name: `Library list supports dollar sign variant`, test: async () => { + const connection = instance.getConnection()!; + const library = `TEST${connection.variantChars.local}LIB`; + const sourceFile = `TEST${connection.variantChars.local}FIL`; + const member = `TEST${connection.variantChars.local}MBR`; + const ccsid = connection.getCcsid(); + + if (library.includes(`$`)) { + await connection.runCommand({ command: `DLTLIB LIB(${library})`, noLibList: true }); + + const crtLib = await connection.runCommand({ command: `CRTLIB LIB(${library}) TYPE(*PROD)`, noLibList: true }); + if (Tools.parseMessages(crtLib.stderr).findId("CPD0032")) { + return; + } + + const createSourceFileCommand = await connection.runCommand({ command: `CRTSRCPF FILE(${library}/${sourceFile}) RCDLEN(112) CCSID(${ccsid})`, noLibList: true }); + assert.strictEqual(createSourceFileCommand.code, 0); + + const addPf = await connection.runCommand({ command: `ADDPFM FILE(${library}/${sourceFile}) MBR(${member}) SRCTYPE(TXT)`, noLibList: true }); + assert.strictEqual(addPf.code, 0); + + await connection.content.uploadMemberContent(undefined, library, sourceFile, member, [`**free`, `dsply 'Hello world';`, `return;`].join(`\n`)); + + // Ensure program compiles with dollar sign in current library + const compileResultA = await connection.runCommand({ command: `CRTBNDRPG PGM(${library}/${member}) SRCFILE(${library}/${sourceFile}) SRCMBR(${member})`, env: {'&CURLIB': library} }); + assert.strictEqual(compileResultA.code, 0); + + // Ensure program compiles with dollar sign in current library + const compileResultB = await connection.runCommand({ command: `CRTBNDRPG PGM(${library}/${member}) SRCFILE(${library}/${sourceFile}) SRCMBR(${member})`, env: {'&LIBL': library} }); + assert.strictEqual(compileResultB.code, 0); + } + } + }, + { + name: `Variant character in source names and commands`, test: async () => { + // CHGUSRPRF X CCSID(284) CNTRYID(ES) LANGID(ESP) + const connection = instance.getConnection()!; + const config = instance.getConfig()!; + + const ccsidData = connection.getCcsids()!; + + const tempLib = config.tempLibrary; + const varChar = connection.variantChars.local[1]; + + const testFile = `${varChar}SCOBBY`; + const testMember = `${varChar}MEMBER`; + const variantMember = `${connection.variantChars.local}MBR`; + + const attemptDelete = await connection.runCommand({ command: `DLTF FILE(${tempLib}/${testFile})`, noLibList: true }); + + const createResult = await runCommandsWithCCSID(connection, [`CRTSRCPF FILE(${tempLib}/${testFile}) RCDLEN(112) CCSID(${ccsidData.userDefaultCCSID})`], ccsidData.userDefaultCCSID); + assert.strictEqual(createResult.code, 0); + + const addPf = await connection.runCommand({ command: `ADDPFM FILE(${tempLib}/${testFile}) MBR(${testMember}) SRCTYPE(TXT)`, noLibList: true }); + assert.strictEqual(addPf.code, 0); + + const attributes = await connection.content.getAttributes({ library: tempLib, name: testFile, member: testMember }, `CCSID`); + assert.ok(attributes); + assert.strictEqual(attributes[`CCSID`], String(ccsidData.userDefaultCCSID)); + + /// Test for getAttributes on member with all variants + + const addPfB = await connection.runCommand({ command: `ADDPFM FILE(${tempLib}/${testFile}) MBR(${variantMember}) SRCTYPE(TXT)`, noLibList: true }); + assert.strictEqual(addPfB.code, 0); + + const attributesB = await connection.content.getAttributes({ library: tempLib, name: testFile, member: variantMember }, `CCSID`); + assert.ok(attributesB); + assert.strictEqual(attributesB[`CCSID`], String(ccsidData.userDefaultCCSID)); + + /// ----- + + const objects = await connection.content.getObjectList({ library: tempLib, types: [`*SRCPF`] }); + assert.ok(objects.length); + assert.ok(objects.some(obj => obj.name === testFile)); + + const members = await connection.content.getMemberList({ library: tempLib, sourceFile: testFile }); + assert.ok(members.length); + assert.ok(members.some(m => m.name === testMember)); + assert.ok(members.some(m => m.file === testFile)); + + const smallFilter = await connection.content.getMemberList({ library: tempLib, sourceFile: testFile, members: `${varChar}*` }); + assert.ok(smallFilter.length); + + const files = await connection.content.getFileList(`/QSYS.LIB/${tempLib}.LIB/${connection.sysNameInAmerican(testFile)}.FILE`); + assert.ok(files.length); + assert.strictEqual(files[0].name, connection.sysNameInAmerican(testMember) + `.MBR`); + + await connection.content.uploadMemberContent(undefined, tempLib, testFile, testMember, [`**free`, `dsply 'Hello world';`, ` `, ` `, `return;`].join(`\n`)); + + const compileResult = await connection.runCommand({ command: `CRTBNDRPG PGM(${tempLib}/${testMember}) SRCFILE(${tempLib}/${testFile}) SRCMBR(${testMember})`, noLibList: true }); + assert.strictEqual(compileResult.code, 0); + + const memberUri = getMemberUri({ library: tempLib, file: testFile, name: testMember, extension: `RPGLE` }); + + const content = await workspace.fs.readFile(memberUri); + let contentStr = new TextDecoder().decode(content); + assert.ok(!contentStr.includes(`0`)); + assert.ok(contentStr.includes(`dsply 'Hello world';`)); + + await workspace.fs.writeFile(memberUri, Buffer.from([`**free`, `dsply 'Woah';`, ` `, ` `, `return;`].join(`\n`), `utf8`)); + + const memberContentBuf = await workspace.fs.readFile(memberUri); + let fileContent = new TextDecoder().decode(memberContentBuf); + + assert.ok(fileContent.includes(`Woah`)); + assert.ok(!fileContent.includes(`0`)); + }, + }, + + ...Object.keys(contents).map(ccsid => ({ name: `Encoding ${ccsid}`, test: async () => { const connection = instance.getConnection(); const config = instance.getConfig()!; @@ -42,19 +430,19 @@ export const EncodingSuite: TestSuite = { await connection!.runCommand({ command: `CRTSRCPF FILE(${tempLib}/${file}) RCDLEN(112) CCSID(${ccsid})`, noLibList: true }); await connection!.runCommand({ command: `ADDPFM FILE(${tempLib}/${file}) MBR(THEMEMBER) SRCTYPE(TXT)`, noLibList: true }); - const theBadOneUri = getMemberUri({ library: tempLib, file, name: `THEMEMBER`, extension: `TXT` }); + // Initial read to create the alias await workspace.fs.readFile(theBadOneUri); await workspace.fs.writeFile(theBadOneUri, Buffer.from(lines, `utf8`)); const memberContentBuf = await workspace.fs.readFile(theBadOneUri); - let fileContent = new TextDecoder().decode(memberContentBuf); - + const fileContent = new TextDecoder().decode(memberContentBuf).trimEnd(); + if (rtlEncodings.includes(ccsid)) { const newLines = fileContent.split(`\n`); - + assert.strictEqual(newLines.length, 2); assert.ok(newLines[1].startsWith(` `)); // RTL @@ -64,6 +452,6 @@ export const EncodingSuite: TestSuite = { assert.deepStrictEqual(fileContent, lines); } } - } - }) + })) + ] }; diff --git a/src/testing/index.ts b/src/testing/index.ts index b4c76b3c3..6f5da3b1c 100644 --- a/src/testing/index.ts +++ b/src/testing/index.ts @@ -14,6 +14,7 @@ import { SearchSuite } from "./search"; import { StorageSuite } from "./storage"; import { TestSuitesTreeProvider } from "./testCasesTree"; import { ToolsSuite } from "./tools"; +import { Server } from "../typings"; const suites: TestSuite[] = [ ActionSuite, @@ -32,6 +33,7 @@ const suites: TestSuite[] = [ export type TestSuite = { name: string + notConcurrent?: boolean tests: TestCase[] before?: () => Promise after?: () => Promise @@ -47,8 +49,24 @@ export interface TestCase { duration?: number } -const testingEnabled = env.testing === `true`; +export interface ConnectionFixture { name: string, user: { [parm: string]: string | number }, commands?: string[] }; + +// https://www.ibm.com/docs/en/i/7.5?topic=information-language-identifiers-associated-default-ccsids +const TestConnectionFixtures: ConnectionFixture[] = [ + { name: `American`, user: { CCSID: 37, CNTRYID: `US`, LANGID: `ENU` } }, + { name: `American (CCSID *SYSVAL)`, user: { CCSID: '*SYSVAL', CNTRYID: `US`, LANGID: `ENU` } }, + { name: `French`, user: { CCSID: 297, CNTRYID: `FR`, LANGID: `FRA` } }, + { name: `Spanish`, user: { CCSID: 284, CNTRYID: `ES`, LANGID: `ESP` } }, + { name: `Spanish (CCSID *SYSVAL)`, user: { CCSID: '*SYSVAL', CNTRYID: `ES`, LANGID: `ESP` } }, + { name: `Danish`, user: { CCSID: 277, CNTRYID: `DK`, LANGID: `DAN` } } +] + +let configuringFixture = false; +let lastChosenFixture: ConnectionFixture; +const testingEnabled = env.base_testing === `true`; +const testSuitesSimultaneously = env.simultaneous === `true`; const testIndividually = env.individual === `true`; +const testSpecific = env.specific; let testSuitesTreeProvider: TestSuitesTreeProvider; export function initialise(context: vscode.ExtensionContext) { @@ -56,7 +74,7 @@ export function initialise(context: vscode.ExtensionContext) { vscode.commands.executeCommand(`setContext`, `code-for-ibmi:testing`, true); if (!testIndividually) { - instance.subscribe(context, 'connected', 'Run tests', runTests); + instance.subscribe(context, 'connected', 'Run tests', () => configuringFixture ? console.log(`Not running tests as configuring fixture`) : runTests(testSuitesSimultaneously)); } instance.subscribe(context, 'disconnected', 'Reset tests', resetTests); @@ -75,72 +93,177 @@ export function initialise(context: vscode.ExtensionContext) { } } } - }) + }), + vscode.commands.registerCommand(`code-for-ibmi.testing.connectWithFixture`, connectWithFixture), ); } } -async function runTests() { - for (const suite of suites) { - try { - suite.status = "running"; - testSuitesTreeProvider.refresh(suite); - if (suite.before) { - console.log(`Pre-processing suite ${suite.name}`); - await suite.before(); - } +export async function connectWithFixture(server?: Server) { + if (server) { + const connectionName = server.name; + const chosenFixture = await vscode.window.showQuickPick(TestConnectionFixtures.map(f => ({label: f.name, description: Object.values(f.user).join(`, `)})), { title: vscode.l10n.t(`Select connection fixture`) }); - console.log(`Running suite ${suite.name} (${suite.tests.length})`); - console.log(); - for (const test of suite.tests) { - await runTest(test); + if (chosenFixture) { + const fixture = TestConnectionFixtures.find(f => f.name === chosenFixture.label); + if (fixture) { + configuringFixture = true; + const error = await setupUserFixture(connectionName, fixture); + + if (error) { + vscode.window.showErrorMessage(`Failed to setup connection fixture: ${error}`); + } else { + lastChosenFixture = fixture; + vscode.window.showInformationMessage(`Successfully setup connection fixture for ${chosenFixture.label}`); + vscode.commands.executeCommand(`code-for-ibmi.connectTo`, connectionName, true); + configuringFixture = false; + } } } - catch (error: any) { - console.log(error); - suite.failure = `${error.message ? error.message : error}`; - } - finally { - suite.status = "done"; - testSuitesTreeProvider.refresh(suite); - if (suite.after) { - console.log(); - console.log(`Post-processing suite ${suite.name}`); - try { - await suite.after(); - } - catch (error: any) { - console.log(error); - suite.failure = `${error.message ? error.message : error}`; - } + } +} + +async function setupUserFixture(connectionName: string, fixture: ConnectionFixture) { + let error: string | undefined; + + await vscode.commands.executeCommand(`code-for-ibmi.connectTo`, connectionName, true); + + let connection = instance.getConnection(); + if (!connection) { + configuringFixture = false; + return `Failed to connect to ${connectionName}`; + } + + const user = connection.currentUser; + fixture.user.USRPRF = user.toUpperCase(); + + const changeUserCommand = connection.content.toCl(`CHGUSRPRF`, fixture.user); + const changeResult = await connection.runCommand({ command: changeUserCommand }); + + if (changeResult.code > 0) { + error = changeResult.stderr; + } + + if (!error && fixture.commands) { + for (const command of fixture.commands) { + let commandResult = await connection.runCommand({ command }); + if (commandResult.code > 0) { + error = commandResult.stderr; } - testSuitesTreeProvider.refresh(suite); } } + + vscode.commands.executeCommand(`code-for-ibmi.disconnect`); + + return error; } -async function runTest(test: TestCase) { - const connection = instance.getConnection(); +async function runTests(simultaneously?: boolean) { + let nonConcurrentSuites: Function[] = []; + let concurrentSuites: Function[] = []; - console.log(`\tRunning ${test.name}`); - test.status = "running"; - testSuitesTreeProvider.refresh(test); - const start = +(new Date()); - try { - connection!.enableSQL = true; + for (const suite of suites) { + if (testSpecific) { + if (!suite.name.toLowerCase().includes(testSpecific.toLowerCase())) { + continue; + } + } - await test.test(); - test.status = "pass"; + const runner = async () => testSuiteRunner(suite, true); + + if (suite.notConcurrent) { + nonConcurrentSuites.push(runner); + } else { + concurrentSuites.push(runner); + } + } + + if (simultaneously) { + await Promise.all(concurrentSuites.map(async suite => suite())); + } + + else { + for (const suite of concurrentSuites) { + await suite(); + } + } + + for (const suite of nonConcurrentSuites) { + await suite(); } + console.log(`All tests completed`); +} + +async function testSuiteRunner(suite: TestSuite, withGap?: boolean) { + try { + suite.status = "running"; + testSuitesTreeProvider.refresh(suite); + if (suite.before) { + console.log(`Pre-processing suite ${suite.name}`); + await suite.before(); + } + + console.log(`Running suite ${suite.name} (${suite.tests.length})`); + console.log(); + for (const test of suite.tests) { + await runTest(test); + testSuitesTreeProvider.refresh(suite); + + if (withGap) { + // Add a little break as to not overload the system + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + } catch (error: any) { console.log(error); - test.status = "failed"; - test.failure = `${error.message ? error.message : error}`; + suite.failure = `${error.message ? error.message : error}`; } finally { - test.duration = +(new Date()) - start; + suite.status = "done"; + testSuitesTreeProvider.refresh(suite); + if (suite.after) { + console.log(); + console.log(`Post-processing suite ${suite.name}`); + try { + await suite.after(); + } + catch (error: any) { + console.log(error); + suite.failure = `${error.message ? error.message : error}`; + } + } + testSuitesTreeProvider.refresh(suite); + } +}; + +async function runTest(test: TestCase) { + const connection = instance.getConnection(); + + if (connection) { + console.log(`Running ${test.name}`); + test.status = "running"; testSuitesTreeProvider.refresh(test); + const start = +(new Date()); + try { + await test.test(); + test.status = "pass"; + } + + catch (error: any) { + console.log(error); + test.status = "failed"; + test.failure = `${error.message ? error.message : error}`; + } + finally { + test.duration = +(new Date()) - start; + testSuitesTreeProvider.refresh(test); + } + } else { + test.status = undefined; + test.failure = undefined; + test.duration = undefined; } } diff --git a/src/testing/tools.ts b/src/testing/tools.ts index c8038e291..41b94f082 100644 --- a/src/testing/tools.ts +++ b/src/testing/tools.ts @@ -5,6 +5,70 @@ import { Tools } from "../api/Tools"; export const ToolsSuite: TestSuite = { name: `Tools API tests`, tests: [ + { + name: `assumeType tests (simple)`, test: async () => { + const row = {"CUSNUM":"938472","LSTNAM":"Henning","INIT":"G K","STREET":"4859 Elm Ave","CITY":"Dallas","STATE":"TX","ZIPCOD":"75217","CDTLMT":"5000","CHGCOD":"3","BALDUE":"37.00","CDTDUE":".00"}; + + let newRow: any = {}; + + for (const key in row) { + newRow[key] = Tools.assumeType(row[key as keyof typeof row]); + } + + assert.strictEqual(newRow.CUSNUM, 938472); + assert.strictEqual(newRow.LSTNAM, `Henning`); + assert.strictEqual(newRow.INIT, `G K`); + assert.strictEqual(newRow.STREET, `4859 Elm Ave`); + assert.strictEqual(newRow.CITY, `Dallas`); + assert.strictEqual(newRow.STATE, `TX`); + assert.strictEqual(newRow.ZIPCOD, 75217); + assert.strictEqual(newRow.CDTLMT, 5000); + assert.strictEqual(newRow.CHGCOD, 3); + assert.strictEqual(newRow.BALDUE, 37); + assert.strictEqual(newRow.CDTDUE, 0); + } + }, + { + name: `assumeType tests (for object list)`, test: async () => { + const rowA = {"NAME":"NETNS","TYPE":"*FILE","ATTRIBUTE":"PF","TEXT":"DATA BASE FILE FOR NETNS INCLUDES","IS_SOURCE":"1","SOURCE_LENGTH":"92","IASP_NUMBER":"0"}; + const rowB = {"NAME":"NETNS","TYPE":"*FILE","ATTRIBUTE":"PF","TEXT":"DATA BASE FILE FOR NETNS INCLUDES","IS_SOURCE":"0","SOURCE_LENGTH":"92","IASP_NUMBER":"1"}; + + let newRowA: any = {}; + let newRowB: any = {}; + + for (const key in rowA) { + newRowA[key] = Tools.assumeType(rowA[key as keyof typeof rowA]); + } + + for (const key in rowB) { + newRowB[key] = Tools.assumeType(rowB[key as keyof typeof rowB]); + } + + assert.strictEqual(newRowA.NAME, `NETNS`); + assert.strictEqual(newRowA.TYPE, `*FILE`); + assert.strictEqual(newRowA.ATTRIBUTE, `PF`); + assert.strictEqual(newRowA.TEXT, `DATA BASE FILE FOR NETNS INCLUDES`); + assert.strictEqual(newRowA.IS_SOURCE, 1); + assert.strictEqual(newRowA.SOURCE_LENGTH, 92); + assert.strictEqual(newRowA.IASP_NUMBER, 0); + + assert.strictEqual(newRowB.NAME, `NETNS`); + assert.strictEqual(newRowB.TYPE, `*FILE`); + assert.strictEqual(newRowB.ATTRIBUTE, `PF`); + assert.strictEqual(newRowB.TEXT, `DATA BASE FILE FOR NETNS INCLUDES`); + assert.strictEqual(newRowB.IS_SOURCE, 0); + assert.strictEqual(newRowB.SOURCE_LENGTH, 92); + assert.strictEqual(newRowB.IASP_NUMBER, 1); + } + }, + { + name: `assumeType tests (simple values)`, test: async () => { + assert.strictEqual(Tools.assumeType(``), ``); + assert.strictEqual(Tools.assumeType(` `), ``); + assert.strictEqual(Tools.assumeType(`1`), 1); + assert.strictEqual(Tools.assumeType(`0`), 0); + } + }, { name: `unqualifyPath (In a named library)`, test: async () => { const qualifiedPath = `/QSYS.LIB/MYLIB.LIB/DEVSRC.FILE/THINGY.MBR`; @@ -35,9 +99,9 @@ export const ToolsSuite: TestSuite = { { name: `sanitizeLibraryNames ($ and #)`, test: async () => { const rawLibraryNames = [`QTEMP`, `#LIBRARY`, `My$lib`, `qsysinc`]; - const sanitizedLibraryNames = Tools.sanitizeLibraryNames(rawLibraryNames); + const sanitizedLibraryNames = Tools.sanitizeObjNamesForPase(rawLibraryNames); - assert.deepStrictEqual(sanitizedLibraryNames, [`QTEMP`, `"#LIBRARY"`, `My\\$lib`, `qsysinc`]); + assert.deepStrictEqual(sanitizedLibraryNames, [`QTEMP`, `"#LIBRARY"`, `My$lib`, `qsysinc`]); }, }, { diff --git a/src/views/helpView.ts b/src/views/helpView.ts index f241a4220..22d7b1ef2 100644 --- a/src/views/helpView.ts +++ b/src/views/helpView.ts @@ -293,6 +293,8 @@ async function getRemoteSection() { `|CCSID Origin|${ccsids.qccsid}|`, `|Runtime CCSID|${ccsids.runtimeCcsid || '?'}|`, `|Default CCSID|${ccsids.userDefaultCCSID || '?'}|`, + `|SSHD CCSID|${ccsids.sshdCcsid || '?'}|`, + `|cqsh|${connection.canUseCqsh}|`, `|SQL|${connection.enableSQL ? 'Enabled' : 'Disabled'}`, `|Source dates|${config.enableSourceDates ? 'Enabled' : 'Disabled'}`, '', @@ -307,11 +309,6 @@ async function getRemoteSection() { `\`\`\`json`, JSON.stringify(connection?.variantChars || {}, null, 2), `\`\`\``), - ``, - createSection(`Errors`, - `\`\`\`json`, - JSON.stringify(connection?.lastErrors || [], null, 2), - `\`\`\``) ].join("\n"); }); } diff --git a/src/views/objectBrowser.ts b/src/views/objectBrowser.ts index 6192e3e1d..38a2ab323 100644 --- a/src/views/objectBrowser.ts +++ b/src/views/objectBrowser.ts @@ -283,18 +283,13 @@ class ObjectBrowserSourcePhysicalFileItem extends ObjectBrowserItem implements O console.log(e); // Work around since we can't get the member list if the users CCSID is not setup. - const config = getConfig(); if (connection.enableSQL) { if (e && e.message && e.message.includes(`CCSID`)) { - vscode.window.showErrorMessage(`Error getting member list. Disabling SQL and refreshing. It is recommended you reload. ${e.message}`, `Reload`).then(async (value) => { + vscode.window.showErrorMessage(`Error getting member list. It is recommended you disconnect and correctly set your user profile CCSID. ${e.message}`, `Reload`).then(async (value) => { if (value === `Reload`) { await vscode.commands.executeCommand(`workbench.action.reloadWindow`); } }); - - connection.enableSQL = false; - await ConnectionConfiguration.update(config); - return this.getChildren(); } } else { throw e; @@ -727,7 +722,16 @@ export function initializeObjectBrowser(context: vscode.ExtensionContext) { newBasename = await vscode.window.showInputBox({ value: newBasename, prompt: vscode.l10n.t(`Rename {0}`, oldMember.basename), - validateInput: value => connection.upperCaseName(value) === oldMember.basename ? vscode.l10n.t(`New member name must be different from it's current name`) : undefined + validateInput: value => { + if (connection.upperCaseName(value) === oldMember.basename) { + return vscode.l10n.t(`New member name must be different from it's current name`) + } + + if (!connection.validQsysName(value)) { + return vscode.l10n.t(`Not a valid member name!`); + } + return undefined; + } }); if (newBasename) { @@ -843,7 +847,7 @@ export function initializeObjectBrowser(context: vscode.ExtensionContext) { const toBeDownloaded = members .filter((member, index, list) => list.findIndex(m => m.library === member.library && m.file === member.file && m.name === member.name) === index) .sort((m1, m2) => m1.name.localeCompare(m2.name)) - .map(member => ({ path: Tools.qualifyPath(member.library, member.file, member.name, member.asp), name: `${member.name}.${member.extension || "MBR"}`, copy: true })); + .map(member => ({ member, path: Tools.qualifyPath(member.library, member.file, member.name, member.asp), name: `${member.name}.${member.extension || "MBR"}`, copy: true })); if (!saveIntoDirectory) { toBeDownloaded[0].name = basename(downloadLocationURI.path); @@ -890,8 +894,12 @@ Do you want to replace it?`, item.name), skipAllLabel, overwriteLabel, overwrite await connection.withTempDirectory(async directory => { task.report({ message: vscode.l10n.t(`copying to streamfiles`), increment: 33 }) const copyToStreamFiles = toBeDownloaded - .filter(member => member.copy) - .map(member => `@CPYTOSTMF FROMMBR('${member.path}') TOSTMF('${directory}/${member.name.toLocaleLowerCase()}') STMFOPT(*REPLACE) STMFCCSID(1208) DBFCCSID(${config.sourceFileCCSID}) ENDLINFMT(*LF);`) + .filter(item => item.copy) + .map(item => + [ + `@QSYS/CPYF FROMFILE(${item.member.library}/${item.member.file}) TOFILE(QTEMP/QTEMPSRC) FROMMBR(${item.member.name}) TOMBR(TEMPMEMBER) MBROPT(*REPLACE) CRTFILE(*YES);`, + `@QSYS/CPYTOSTMF FROMMBR('${Tools.qualifyPath("QTEMP", "QTEMPSRC", "TEMPMEMBER", undefined)}') TOSTMF('${directory}/${item.name.toLocaleLowerCase()}') STMFOPT(*REPLACE) STMFCCSID(1208) DBFCCSID(${config.sourceFileCCSID});` + ].join("\n")) .join("\n"); await contentApi.runSQL(copyToStreamFiles); @@ -901,7 +909,7 @@ Do you want to replace it?`, item.name), skipAllLabel, overwriteLabel, overwrite .then(open => open ? vscode.commands.executeCommand('revealFileInOS', saveIntoDirectory ? vscode.Uri.joinPath(downloadLocationURI, toBeDownloaded[0].name.toLocaleLowerCase()) : downloadLocationURI) : undefined); }); } catch (e: any) { - vscode.window.showErrorMessage(vscode.l10n.t(`Error downloading member(s)! {0}`, e)); + vscode.window.showErrorMessage(vscode.l10n.t(`Error downloading member(s)! {0}`, String(e))); } }); } @@ -1009,7 +1017,7 @@ Do you want to replace it?`, item.name), skipAllLabel, overwriteLabel, overwrite const newLibrary = await vscode.window.showInputBox({ prompt: vscode.l10n.t(`Name of new library`), - validateInput: (library => library.length > 10 ? vscode.l10n.t(`Library name too long.`) : undefined) + validateInput: (library => !connection.validQsysName(library) ? vscode.l10n.t(`Library name not valid.`) : undefined) }); if (newLibrary) { @@ -1057,13 +1065,13 @@ Do you want to replace it?`, item.name), skipAllLabel, overwriteLabel, overwrite vscode.commands.registerCommand(`code-for-ibmi.createSourceFile`, async (node: ObjectBrowserFilterItem | ObjectBrowserObjectItem) => { if (node.library) { + const connection = getConnection(); const fileName = await vscode.window.showInputBox({ prompt: vscode.l10n.t(`Name of new source file`), - validateInput: (fileName => fileName.length > 10 ? vscode.l10n.t(`Source filename must be 10 chars or less.`) : undefined) + validateInput: (fileName => !connection.validQsysName(fileName) ? vscode.l10n.t(`Source filename is not valid.`) : undefined) }); if (fileName) { - const connection = getConnection(); const library = node.library; const uriPath = `${library}/${connection.upperCaseName(fileName)}` diff --git a/types/package-lock.json b/types/package-lock.json index 887e45759..018ed0563 100644 --- a/types/package-lock.json +++ b/types/package-lock.json @@ -1,12 +1,12 @@ { "name": "@halcyontech/vscode-ibmi-types", - "version": "2.13.10-dev.0", + "version": "2.13.19-dev.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@halcyontech/vscode-ibmi-types", - "version": "2.13.10-dev.0", + "version": "2.13.19-dev.0", "license": "ISC" } } diff --git a/types/package.json b/types/package.json index b5671c5d6..dc868e1a1 100644 --- a/types/package.json +++ b/types/package.json @@ -1,6 +1,6 @@ { "name": "@halcyontech/vscode-ibmi-types", - "version": "2.13.10-dev.0", + "version": "2.13.19-dev.0", "description": "Types for vscode-ibmi", "typings": "./typings.d.ts", "scripts": { diff --git a/webpack.config.js b/webpack.config.js index dca502afa..5e23f83aa 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,9 +3,10 @@ 'use strict'; const webpack = require(`webpack`); - +const fs = require(`fs`); const path = require(`path`); +const packageJson = require(`./package.json`); const npm_runner = process.env[`npm_lifecycle_script`]; const isProduction = (npm_runner && npm_runner.includes(`production`)); @@ -18,6 +19,33 @@ if (isProduction) { exclude = path.resolve(__dirname, `src`, `testing`) } +/// ==================== +// Move required binaries to dist folder +/// ==================== + +const dist = path.resolve(__dirname, `dist`); + +fs.mkdirSync(dist, {recursive: true}); + +const files = [{relative: `src/components/cqsh/cqsh`, name: `cqsh_1`}]; + +for (const file of files) { + const src = path.resolve(__dirname, file.relative); + const dest = path.resolve(dist, file.name); + + console.log(`Copying ${src} to ${dest}`); + if (fs.existsSync(src)) { + // Overwrites by default + fs.copyFileSync(src, dest); + } +} + +console.log(``); + +/// ==================== +// Webpack configuration +/// ==================== + /**@type {webpack.Configuration}*/ const config = { target: `node`, // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ @@ -40,6 +68,7 @@ const config = { }, plugins: [ new webpack.DefinePlugin({ + 'process.env.VSCODEIBMI_VERSION': JSON.stringify(packageJson.version), 'process.env.DEV': JSON.stringify(!isProduction), }),