diff --git a/.vscodeignore b/.vscodeignore index 8dcb0c0be..ebaff67bc 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -10,8 +10,10 @@ vsc-extension-quickstart.md **/tsconfig.json src/** !src/schemas/** +tools/** types/** docs/** .github/** node_modules -media \ No newline at end of file +media +*.config.* \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7b57765f8..d8642ca73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,11 @@ "": { "name": "code-for-ibmi", "version": "2.18.1-dev.0", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@ibm/ibmi-eventf-parser": "^1.0.2", + "@ibm/mapepire-js": "^0.6.0", "@vscode-elements/elements": "^1.9.1", "adm-zip": "^0.5.14", "crc-32": "https://cdn.sheetjs.com/crc-32-latest/crc-32-latest.tgz", @@ -22,6 +24,7 @@ "vscode-diff": "^2.0.2" }, "devDependencies": { + "@octokit/rest": "^21.1.1", "@types/adm-zip": "^0.5.5", "@types/glob": "^7.1.3", "@types/node": "^18.0.0", @@ -620,6 +623,18 @@ "version": "1.0.2", "license": "Apache-2.0" }, + "node_modules/@ibm/mapepire-js": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@ibm/mapepire-js/-/mapepire-js-0.6.0.tgz", + "integrity": "sha512-MU5IqIqs3B9Jr3hGXOsfSf/4GvTgNaWb4uB/bFvVBtq9sleR29yj54Ao0gPPBiNwPrDAASTa928TguZDNOxi/w==", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.16.0" + }, + "bin": { + "so": "dist/index.js" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "dev": true, @@ -677,6 +692,206 @@ "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz", "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==" }, + "node_modules/@octokit/auth-token": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", + "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.6.tgz", + "integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.2.2", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", + "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz", + "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.10.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", + "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz", + "integrity": "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.10.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/request": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^2.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz", + "integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^6.1.4", + "@octokit/plugin-paginate-rest": "^11.4.2", + "@octokit/plugin-request-log": "^5.3.1", + "@octokit/plugin-rest-endpoint-methods": "^13.3.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.34.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.3.tgz", @@ -1479,6 +1694,13 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/before-after-hook": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/big.js": { "version": "5.2.2", "dev": true, @@ -2976,6 +3198,23 @@ "node": ">=12.0.0" } }, + "node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "dev": true, @@ -3369,7 +3608,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -4537,6 +4778,13 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC" + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "dev": true, @@ -4580,9 +4828,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.19", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", - "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -4986,6 +5234,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "4.0.0", "license": "ISC" diff --git a/package.json b/package.json index c6fa197e8..795933160 100644 --- a/package.json +++ b/package.json @@ -373,6 +373,11 @@ "type": "boolean", "default": false, "description": "Always open source members and IFS files in read-only mode." + }, + "secureSQL": { + "type": "boolean", + "default": false, + "description": "Use TLS when connecting Mapepire to Db2 for i" } } } @@ -3008,13 +3013,14 @@ "pretest": "npm run lint", "test": "vitest run", "package": "vsce package --allow-package-all-secrets", - "build": "rm -rf dist && tsc", "vscode:prepublish": "webpack --mode production", "webpack": "webpack --mode development", "webpack-dev": "webpack --mode development --watch", - "typings": "npx -p typescript tsc ./src/extension.ts --declaration --allowJs --emitDeclarationOnly --outDir types --esModuleInterop -t es2022 --moduleResolution node --resolveJsonModule" + "typings": "npx -p typescript tsc ./src/extension.ts --declaration --allowJs --emitDeclarationOnly --outDir types --esModuleInterop -t es2022 --moduleResolution node --resolveJsonModule", + "postinstall": "npx tsx tools/downloadMapepire" }, "devDependencies": { + "@octokit/rest": "^21.1.1", "@types/adm-zip": "^0.5.5", "@types/glob": "^7.1.3", "@types/node": "^18.0.0", @@ -3036,6 +3042,7 @@ }, "dependencies": { "@ibm/ibmi-eventf-parser": "^1.0.2", + "@ibm/mapepire-js": "^0.6.0", "@vscode-elements/elements": "^1.9.1", "adm-zip": "^0.5.14", "crc-32": "https://cdn.sheetjs.com/crc-32-latest/crc-32-latest.tgz", @@ -3052,4 +3059,4 @@ "halcyontechltd.vscode-ibmi-walkthroughs", "vscode.git" ] -} +} \ No newline at end of file diff --git a/schemas/settings.json b/schemas/settings.json index f349b6f51..9e215cc77 100644 --- a/schemas/settings.json +++ b/schemas/settings.json @@ -110,6 +110,11 @@ "type": "string", "default": "8008", "description": "Port to connect to IBM i Debug Service for SEP." + }, + "secureSQL": { + "type": "boolean", + "default": "false", + "description": "Use TLS when connecting Mapepire to Db2 for i" } } }, diff --git a/src/api/CompileTools.ts b/src/api/CompileTools.ts index 1e4b3c322..216e4c3a1 100644 --- a/src/api/CompileTools.ts +++ b/src/api/CompileTools.ts @@ -1,5 +1,6 @@ import IBMi from './IBMi'; +import { SimpleQueue } from './queue'; import { Tools } from './Tools'; import { CommandResult, RemoteCommand, StandardIO } from './types'; import { Variables } from './variables'; @@ -12,6 +13,11 @@ export interface ILELibrarySettings { export namespace CompileTools { export const NEWLINE = `\r\n`; export const DID_NOT_RUN = -123; + const HIDE_MESSAGE_IDS = [`CPF3485`, `SQL0462`]; + + const ileQueue = new SimpleQueue(); + + let jobLogOrdinal = 0; interface RunCommandEvents { writeEvent?: (content: string) => void; @@ -66,7 +72,7 @@ export namespace CompileTools { } } : {}; - let commandResult; + let commandResult: CommandResult; switch (options.environment) { case `pase`: commandResult = await connection.sendCommand({ @@ -90,17 +96,75 @@ export namespace CompileTools { case `ile`: default: - // escape $ and # in commands - commandResult = await connection.sendQsh({ - command: [ - ...options.noLibList ? [] : buildLiblistCommands(connection, ileSetup), - ...commands.map(command => - `${`system "${IBMi.escapeForShell(command)}"`}`, - ) - ].join(` && `), - directory: cwd, - ...callbacks + // TODO: fetch job log + // TODO: exit code? + commandResult = { + code: 0, // TODO: exit code based on job log? + stderr: ``, + stdout: ``, // TODO: job log? + command: commands.join(`, `), + }; + + if (options.skipDetail) { + options.noLibList = true; + } + + await ileQueue.next(async () => { + try { + await connection.runSQL([ + ...(cwd ? [`@CHGCURDIR DIR('${cwd}')`] : []), + ...(options.noLibList ? [] : [`@CHGLIBL CURLIB(${ileSetup.currentLibrary}) LIBL(${ileSetup.libraryList.join(` `)})`]), + ...commands.map(c => `@${c}`) + ]); + } catch (e: any) { + commandResult.stdout = e.message; + commandResult.code = 1; + } + + // Do we really care about the job log and spool output when this is used? + + // Then fetch the job log + + if (options.skipDetail) { + // We still need to skip all the messages for the next time the commands are run + const lastMessage = await connection.runSQL(`select max(ORDINAL_POSITION) as O from table(qsys2.joblog_info('*'))`); + if (lastMessage && lastMessage.length === 1) { + jobLogOrdinal = Number(lastMessage[0].O); + } + + } else { + try { + // We only care about messages since the last run :) + const lastJobLog = await connection.runSQL(`select ORDINAL_POSITION, message_id, message_text from table(qsys2.joblog_info('*')) where ordinal_position > ?`, { fakeBindings: [jobLogOrdinal] }); + if (lastJobLog && lastJobLog.length > 0) { + commandResult.stderr = lastJobLog + .filter(r => !HIDE_MESSAGE_IDS.includes(r.MESSAGE_ID as string)) + .map(r => `${r.MESSAGE_ID}: ${r.MESSAGE_TEXT}`).join(`\n`); + callbacks.onStderr?.(Buffer.from(commandResult.stderr)); + jobLogOrdinal = Number(lastJobLog[lastJobLog.length - 1].ORDINAL_POSITION); + } else { + jobLogOrdinal = 0; // Reset if no job log + } + } catch (e) { + commandResult.code = 3; + } + + // Then fetch the spool file + try { + const lastSpool = await connection.runSQL(LAST_SPOOL_STATEMENT); + + if (lastSpool && lastSpool.length > 0) { + commandResult.stdout = lastSpool.map(r => (r.SPOOLED_DATA as string).trimEnd()).join(`\n`); + callbacks.onStdout?.(Buffer.from(commandResult.stdout)); + } + } catch (e) { + commandResult.code = 2; + console.log(`Failed to get spool output: `, e); + } + } + }); + break; } @@ -134,3 +198,30 @@ export namespace CompileTools { ]; } } + +const LAST_SPOOL_STATEMENT = [ + `WITH my_spooled_files (`, + ` job,`, + ` FILE,`, + ` file_number,`, + ` user_data,`, + ` create_timestamp`, + ` )`, + ` AS (SELECT job_name,`, + ` spooled_file_name,`, + ` file_number,`, + ` user_data,`, + ` create_timestamp`, + ` FROM qsys2.output_queue_entries_basic`, + ` WHERE user_name = USER`, + ` ORDER BY create_timestamp DESC`, + ` LIMIT 1)`, + ` SELECT `, + ` spooled_data`, + ` FROM my_spooled_files,`, + ` TABLE (`, + ` systools.spooled_file_data(`, + ` job_name => job, spooled_file_name => FILE,`, + ` spooled_file_number => file_number)`, + ` )`, +].join(` `); \ No newline at end of file diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index bf64f790d..8e3b76451 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -1,5 +1,5 @@ import { parse } from 'csv-parse/sync'; -import { existsSync } from "fs"; +import { existsSync, stat } from "fs"; import * as node_ssh from "node-ssh"; import os from "os"; import path, { parse as parsePath } from 'path'; @@ -8,8 +8,6 @@ import { CompileTools } from "./CompileTools"; import IBMiContent from "./IBMiContent"; import { Tools } from './Tools'; import { IBMiComponent } from "./components/component"; -import { CopyToImport } from "./components/copyToImport"; -import { CustomQSh } from './components/cqsh'; import { ComponentManager, ComponentSearchProps } from "./components/manager"; import * as configVars from './configVars'; import { DebugConfiguration } from "./configuration/DebugConfiguration"; @@ -18,6 +16,8 @@ import { ConnectionConfig, RemoteConfigFile } from './configuration/config/types import { ConfigFile } from './configuration/serverFile'; import { CachedServerSettings, CodeForIStorage } from './configuration/storage/CodeForIStorage'; import { AspInfo, CommandData, CommandResult, ConnectionData, EditorPath, IBMiMember, RemoteCommand, WrapResult } from './types'; +import { Mapepire } from './components/mapepire'; +import { sshSqlJob } from './components/mapepire/sqlJob'; export interface MemberParts extends IBMiMember { basename: string @@ -41,14 +41,6 @@ const remoteApps = [ // All names MUST also be defined as key in 'remoteFeatures path: `/QOpenSys/pkgs/bin/`, names: [`git`, `grep`, `tn5250`, `pfgrep`, `md5sum`, `bash`, `chsh`, `stat`, `sort`, `tar`, `ls`, `find`] }, - { - path: `/QSYS.LIB/`, - // In the future, we may use a generic specific. - // Right now we only need one program - // specific: `*.PGM`, - specific: `QZDFMDB2.PGM`, - names: [`QZDFMDB2.PGM`] - }, { path: `/QIBM/ProdData/IBMiDebugService/bin/`, specific: `startDebugService.sh`, @@ -90,9 +82,6 @@ export default class IBMi { private systemVersion: number = 0; private qccsid: number = IBMi.CCSID_NOCONVERSION; private userJobCcsid: number = IBMi.CCSID_SYSVAL; - /** User default CCSID is job default CCSID */ - private userDefaultCCSID: number = 0; - private sshdCcsid: number | undefined; private componentManager = new ComponentManager(this); @@ -117,6 +106,8 @@ export default class IBMi { private tempRemoteFiles: { [name: string]: string } = {}; defaultUserLibraries: string[] = []; + private sqlJob: sshSqlJob|undefined; + /** * Used to store ASP numbers and their names * Their names usually maps up to a directory in @@ -176,15 +167,9 @@ export default class IBMi { * and if not, call `loadFromServer()` on it. */ getConfigFile(id: keyof ConnectionConfigFiles) { - if (!this.configFiles[id]) { - this.configFiles[id] = new ConfigFile(this, id as string, {} as T); - } - - const configFile = this.configFiles[id] as ConfigFile; - - return configFile; + return this.configFiles[id] as ConfigFile; } - + async loadRemoteConfigs() { for (const configFile in this.configFiles) { const currentConfig = this.configFiles[configFile as keyof ConnectionConfigFiles]; @@ -199,9 +184,6 @@ export default class IBMi { } } - get canUseCqsh() { - return this.getComponent(CustomQSh.ID) !== undefined; - } /** * Primarily used for running SQL statements. @@ -210,19 +192,6 @@ export default class IBMi { return this.userJobCcsid === IBMi.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(); }; @@ -261,7 +230,6 @@ export default class IBMi { sort: undefined, 'GETNEWLIBL.PGM': undefined, 'GETMBRINFO.SQL': undefined, - 'QZDFMDB2.PGM': undefined, 'startDebugService.sh': undefined, attr: undefined, iconv: undefined, @@ -501,7 +469,7 @@ export default class IBMi { callbacks.progress({ message: `Checking installed components on host IBM i: Java` }); - const javaCheck = async (root: string) => await this.content.testStreamFile(`${root}/bin/java`, 'x') ? root : undefined; + const javaCheck = async (root: string) => await this.getContent().testStreamFile(`${root}/bin/java`, 'x') ? root : undefined; [ this.remoteFeatures.jdk80, this.remoteFeatures.jdk11, @@ -541,6 +509,36 @@ export default class IBMi { callbacks.progress({ message: `Checking Code for IBM i components.` }); + // We always start up Mapepire first + await this.componentManager.startupComponent(Mapepire.ID, quickConnect() ? cachedServerSettings?.installedComponents : []); + + // Check Mapepire state after startup + const mapepireStates = this.componentManager.getComponentStates(); + const mapepireState = mapepireStates.find(s => s.id.name === Mapepire.ID); + this.appendOutput(`Mapepire state after startup: ${mapepireState?.state || 'not found'}\n`); + + const mapepire = this.getComponent(Mapepire.ID); + if (mapepire) { + const useJavaVersion = (this.remoteFeatures.jdk17 || this.remoteFeatures.jdk11 || this.remoteFeatures.jdk80); + if (useJavaVersion) { + const javaPath = path.posix.join(useJavaVersion, `bin`, `java`); + try { + this.sqlJob = await mapepire.newJob(this, javaPath); + } catch (e: any) { + callbacks.message(`error`, `Failed to start Mapepire SQL job: ${e.message || e}`); + this.appendOutput(`Mapepire error: ${e.message || e}\n`); + } + } else { + callbacks.message(`warning`, `No Java installation found. SQL operations will not be available. Please install Java 8, 11, or 17.`); + this.appendOutput(`Warning: No Java found for Mapepire\n`); + } + } else { + callbacks.message(`warning`, `Mapepire component failed to start. SQL operations will not be available.`); + this.appendOutput(`Warning: Mapepire component not available\n`); + } + + // Then check the remaining components + await this.componentManager.startup(quickConnect() ? cachedServerSettings?.installedComponents : []); const componentStates = this.componentManager.getComponentStates(); @@ -548,6 +546,7 @@ export default class IBMi { for (const state of componentStates) { this.appendOutput(`\t${state.id.name} (${state.id.version}): ${state.state}\n`); } + this.appendOutput(`\n`); // Load the remote connection configuration and apply it to the connection @@ -572,45 +571,34 @@ export default class IBMi { message: `Checking library list configuration.` }); + // TODO: RIP OUT LIBLIST WITH LIBRARY_LIST_INFO + //Since the compiles are stateless, then we have to set the library list each time we use the `SYSTEM` command //We setup the defaultUserLibraries here so we can remove them later on so the user can setup their own library list let currentLibrary = `QGPL`; this.defaultUserLibraries = []; - const liblResult = await this.sendQsh({ - command: `liblist` - }); - if (liblResult.code === 0) { - const libraryListString = liblResult.stdout; - if (libraryListString !== ``) { - const libraryList = libraryListString.split(`\n`); - - let lib, type; - for (const line of libraryList) { - lib = line.substring(0, 10).trim(); - type = line.substring(12); - - switch (type) { - case `USR`: - this.defaultUserLibraries.push(lib); - break; - - case `CUR`: - currentLibrary = lib; - break; - } - } + const liblRows = await this.runSQL(`SELECT TYPE, SYSTEM_SCHEMA_NAME, IASP_NUMBER FROM QSYS2.LIBRARY_LIST_INFO`); - //If this is the first time the config is made, then these arrays will be empty - if (this.config.currentLibrary.length === 0) { - this.config.currentLibrary = currentLibrary; - } - if (this.config.libraryList.length === 0) { - this.config.libraryList = this.defaultUserLibraries; - } + for (const row of liblRows) { + switch (row.TYPE) { + case `USER`: + this.defaultUserLibraries.push(row.SYSTEM_SCHEMA_NAME as string); + break; + case `CURRENT`: + currentLibrary = (row.SYSTEM_SCHEMA_NAME as string); + break; } } + //If this is the first time the config is made, then these arrays will be empty + if (this.config.currentLibrary.length === 0) { + this.config.currentLibrary = currentLibrary; + } + if (this.config.libraryList.length === 0) { + this.config.libraryList = this.defaultUserLibraries; + } + callbacks.progress({ message: `Checking temporary directory and temporary library configuration.` }); @@ -674,21 +662,21 @@ export default class IBMi { message: `Checking for bad data areas.` }); - const [QCPTOIMPF, QCPFRMIMPF] = await Promise.all([ - this.runCommand({ - command: `CHKOBJ OBJ(QSYS/QCPTOIMPF) OBJTYPE(*DTAARA)`, - noLibList: true - }), - this.runCommand({ - command: `CHKOBJ OBJ(QSYS/QCPFRMIMPF) OBJTYPE(*DTAARA)`, - noLibList: true - }) - ]); + + const QCPTOIMPF = await this.runCommand({ + command: `CHKOBJ OBJ(QSYS/QCPTOIMPF) OBJTYPE(*DTAARA)`, + skipDetail: true + }); if (QCPTOIMPF?.code === 0) { callbacks.uiErrorHandler(this, `QCPTOIMPF_exists`); } + const QCPFRMIMPF = await this.runCommand({ + command: `CHKOBJ OBJ(QSYS/QCPFRMIMPF) OBJTYPE(*DTAARA)`, + skipDetail: true + }) + if (QCPFRMIMPF?.code === 0) { callbacks.uiErrorHandler(this, `QCPFRMIMPF_exists`); } @@ -775,6 +763,8 @@ export default class IBMi { let validLibs: string[] = []; let badLibs: string[] = []; + // TODO: swap liblist with object_statistics? + const result = await this.sendQsh({ command: [ `liblist -d ` + IBMi.escapeForShell(this.defaultUserLibraries.join(` `)), @@ -863,11 +853,12 @@ export default class IBMi { this.currentAsp = await this.getUserProfileAsp(); + // TODO: since we are using Mapepire, we only need the QCCSID and the job CCSID now + // Fetch conversion values? - if (quickConnect() && cachedServerSettings?.jobCcsid !== null && cachedServerSettings?.userDefaultCCSID && cachedServerSettings?.qccsid) { + if (quickConnect() && cachedServerSettings?.jobCcsid !== null && cachedServerSettings?.qccsid) { this.qccsid = cachedServerSettings.qccsid; this.userJobCcsid = cachedServerSettings.jobCcsid; - this.userDefaultCCSID = cachedServerSettings.userDefaultCCSID; } else { callbacks.progress({ message: `Fetching conversion values.` @@ -894,69 +885,19 @@ export default class IBMi { 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) => { callbacks.uiErrorHandler(this, `ccsid_warning`, message); } - if (this.canUseCqsh) { - // If cqsh is available, but the user profile CCSID is bad, then cqsh won't work - if (this.getCcsid() === IBMi.CCSID_NOCONVERSION) { - userCcsidNeedsFixing = true; - } - } - - else { - // If cqsh is not available, then we need to check the SSHD CCSID - this.sshdCcsid = await this.content.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 ${IBMi.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) { @@ -1013,7 +954,6 @@ export default class IBMi { badDataAreasChecked: true, libraryListValidated: true, pathChecked: true, - userDefaultCCSID: this.userDefaultCCSID, debugConfigLoaded, maximumArgsLength: this.maximumArgsLength }); @@ -1073,7 +1013,7 @@ export default class IBMi { else if (messages.findId(`CPD0032`)) { //Can't use CRTLIB const tempLibExists = await this.runCommand({ command: `CHKOBJ OBJ(QSYS/${this.config.tempLibrary}) OBJTYPE(*LIB)`, - noLibList: true + skipDetail: true }); if (tempLibExists.code === 0) { @@ -1151,15 +1091,6 @@ export default class IBMi { let qshExecutable = `/QOpenSys/usr/bin/qsh`; - if (this.canUseCqsh) { - qshExecutable = this.getComponent(CustomQSh.ID)!.installPath; - } - - if (this.requiresTranslation) { - options.stdin = this.sysNameInAmerican(options.stdin); - options.directory = options.directory ? this.sysNameInAmerican(options.directory) : undefined; - } - return this.sendCommand({ ...options, command: `${IBMi.locale} ${qshExecutable}` @@ -1212,6 +1143,11 @@ export default class IBMi { } private disconnect(failedToConnect = false) { + if (this.sqlJob) { + this.sqlJob.close(); + this.sqlJob = undefined; + } + if (this.client) { this.client = undefined; @@ -1234,7 +1170,7 @@ export default class IBMi { } public sqlRunnerAvailable() { - return this.remoteFeatures[`QZDFMDB2.PGM`] !== undefined; + return this.sqlJob !== undefined; } /** @@ -1416,105 +1352,71 @@ export default class IBMi { * @param statements * @returns a Result set */ - 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) { - // 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 = `${IBMi.locale} system "call QSYS/QZDFMDB2 PARM('-d' '-i' '-t')"` - let useCsv = options.forceSafe; - - // Use custom QSH if available - if (this.canUseCqsh) { - const customQsh = this.getComponent(CustomQSh.ID)!; - command = `${IBMi.locale} ${customQsh.installPath} -c "system \\"call QSYS/QZDFMDB2 PARM('-d' '-i' '-t')\\""`; - } + async runSQL(statements: string|string[], options: { fakeBindings?: (string | number)[], forceSafe?: boolean } = {}): Promise { + if (this.sqlJob) { + let list = Array.isArray(statements) ? statements : statements.split(`;`).filter(x => x.trim().length > 0); - 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; - } + let lastResultSet: any; - // 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; + for (let i = 0; i < list.length; i++) { + let statement = list[i]; + let isLast = i === (list.length-1); + + if (statement.startsWith(`@`)) { + await this.sqlJob.execute(statement.substring(1), {isClCommand: true}); + } else { + if (isLast) { + // There is a bug with Mapepire handling of binding parameters. + // We work around it by using these fake parameters and passing + // in UTF8 encoding strings/numbers. + const fakeBindings = options.fakeBindings; + if (statement.includes(`?`) && fakeBindings && fakeBindings.length > 0) { + const parts = statement.split(`?`); + + statement = ``; + for (let partsIndex = 0; partsIndex < parts.length; partsIndex++) { + statement += parts[partsIndex]; + if (fakeBindings[partsIndex] !== undefined) { + switch (typeof fakeBindings[partsIndex]) { + case `number`: + statement += fakeBindings[partsIndex]; + break; + + case `string`: + statement += Tools.bufferToUx(fakeBindings[partsIndex] as string); + break; + } + } } } } - } - // Return as CSV when needed - if (useCsv && (asUpper?.startsWith(`SELECT`) || asUpper?.startsWith(`WITH`))) { - const copyToImport = this.getComponent(CopyToImport.ID); - if (copyToImport) { - returningAsCsv = copyToImport.wrap(this, lastStmt); - list.push(...returningAsCsv.newStatements); + let query; + let error: Tools.SqlError|undefined; + try { + query = this.sqlJob.query(statement); + const rs = await query.execute(99999); + lastResultSet = rs.data; + } catch (e: any) { + error = new Tools.SqlError(e.message); + error.cause = statement + + const parts: string[] = e.message.split(`,`); + if (parts.length > 3) { + error.sqlstate = parts[parts.length-2].trim(); + } + } finally { + query?.close(); } - } - - if (!returningAsCsv) { - list.push(lastStmt); - } - - input = list.join(`;\n`); - } - - const output = await this.sendCommand({ - command, - stdin: input - }) - - if (output.stdout) { - const fromStdout = Tools.db2Parse(output.stdout, input); - if (returningAsCsv) { - // Will throw an error if stdout contains an error - - const csvContent = await this.content.downloadStreamfileRaw(returningAsCsv.outStmf); - if (csvContent) { - this.sendCommand({ command: `rm -rf "${returningAsCsv.outStmf}"` }); - - return parse(csvContent, { - columns: true, - skip_empty_lines: true, - onRecord(record) { - for (const key of Object.keys(record)) { - record[key] = record[key] === ` ` ? `` : Tools.assumeType(record[key], key); - } - return record; - } - }) as Tools.DB2Row[]; + if (error) { + throw error; } - - throw new Error(`There was an error fetching the SQL result set.`) - } else { - return fromStdout; } } + + + return lastResultSet; } throw new Error(`There is no way to run SQL on this system.`); @@ -1534,17 +1436,13 @@ export default class IBMi { } getCcsid() { - const fallbackToDefault = ((this.userJobCcsid < 1 || this.userJobCcsid === IBMi.CCSID_NOCONVERSION) && this.userDefaultCCSID > 0); - const ccsid = fallbackToDefault ? this.userDefaultCCSID : this.userJobCcsid; - return ccsid; + return this.userJobCcsid; } getCcsids() { return { qccsid: this.qccsid, runtimeCcsid: this.userJobCcsid, - userDefaultCCSID: this.userDefaultCCSID, - sshdCcsid: this.sshdCcsid }; } diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 06a6196c1..23468b56c 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -264,7 +264,7 @@ export default class IBMiContent { try { await this.ibmi.runSQL([ `@QSYS/CPYF FROMFILE(${library}/${sourceFile}) FROMMBR(${member}) TOFILE(QTEMP/QTEMPSRC) TOMBR(TEMPMEMBER) MBROPT(*REPLACE) CRTFILE(*YES);`, - `@QSYS/CPYFRMSTMF FROMSTMF('${tempRmt}') TOMBR('${Tools.qualifyPath("QTEMP", "QTEMPSRC", "TEMPMEMBER", undefined)}') MBROPT(*REPLACE) STMFCCSID(1208) DBFCCSID(${this.config.sourceFileCCSID})`, + `@QSYS/CPYFRMSTMF FROMSTMF('${tempRmt}') TOMBR('${Tools.qualifyPath("QTEMP", "QTEMPSRC", "TEMPMEMBER", undefined)}') MBROPT(*REPLACE)`, `@QSYS/CPYF FROMFILE(QTEMP/QTEMPSRC) FROMMBR(TEMPMEMBER) TOFILE(${library}/${sourceFile}) TOMBR(${member}) MBROPT(*REPLACE);` ].join("\n")); } catch (error: any) { @@ -274,7 +274,7 @@ export default class IBMiContent { } else { copyResult = await this.ibmi.runCommand({ - command: `QSYS/CPYFRMSTMF FROMSTMF('${tempRmt}') TOMBR('${path}') MBROPT(*REPLACE) STMFCCSID(1208) DBFCCSID(${this.config.sourceFileCCSID})`, + command: `QSYS/CPYFRMSTMF FROMSTMF('${tempRmt}') TOMBR('${path}') MBROPT(*REPLACE)`, noLibList: true }); } @@ -300,7 +300,7 @@ export default class IBMiContent { * @returns result set */ runStatements(...statements: string[]): Promise { - return this.ibmi.runSQL(statements.map(s => s.trimEnd().endsWith(`;`) ? s : `${s};`).join(`\n`)); + return this.ibmi.runSQL(statements); } /** @@ -535,8 +535,8 @@ export default class IBMiContent { createOBJLIST = [ `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,`, + ` rtrim(cast(t.SYSTEM_TABLE_SCHEMA as char(10))) as LIBRARY,`, + ` rtrim(cast(t.SYSTEM_TABLE_NAME as char(10))) as NAME,`, ` '*FILE' as TYPE,`, ` 'PF' as ATTRIBUTE,`, ` t.TABLE_TEXT as TEXT,`, @@ -572,8 +572,8 @@ export default class IBMiContent { createOBJLIST = [ `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,`, + ` rtrim(cast(t.SYSTEM_TABLE_SCHEMA as char(10))) as LIBRARY,`, + ` rtrim(cast(t.SYSTEM_TABLE_NAME as char(10))) as NAME,`, ` '*FILE' as TYPE,`, ` 'PF' as ATTRIBUTE,`, ` t.TABLE_TEXT as TEXT,`, @@ -723,12 +723,12 @@ export default class IBMiContent { const statement = `with MEMBERS as ( select - rtrim(cast(a.SYSTEM_TABLE_SCHEMA as char(10) for bit data)) as LIBRARY, + rtrim(cast(a.SYSTEM_TABLE_SCHEMA as char(10))) as LIBRARY, b.AVGROWSIZE as RECORD_LENGTH, a.IASP_NUMBER as ASP, - rtrim(cast(a.SYSTEM_TABLE_NAME as char(10) for bit data)) AS SOURCE_FILE, - rtrim(cast(b.SYSTEM_TABLE_MEMBER as char(10) for bit data)) as NAME, - coalesce(rtrim(cast(b.SOURCE_TYPE as varchar(10) for bit data)), '') as TYPE, + rtrim(cast(a.SYSTEM_TABLE_NAME as char(10))) AS SOURCE_FILE, + rtrim(cast(b.SYSTEM_TABLE_MEMBER as char(10))) as NAME, + coalesce(rtrim(cast(b.SOURCE_TYPE as varchar(10))), '') as TYPE, coalesce(rtrim(varchar(b.PARTITION_TEXT)), '') as TEXT, b.NUMBER_ROWS as LINES, extract(epoch from (b.CREATE_TIMESTAMP))*1000 as CREATED, diff --git a/src/api/Tools.ts b/src/api/Tools.ts index 222735cdd..4f01d2f34 100644 --- a/src/api/Tools.ts +++ b/src/api/Tools.ts @@ -271,33 +271,6 @@ export namespace Tools { } } - /** - * Fixes an SQL statement to make it compatible with db2 CLI program QZDFMDB2. - * - Changes `@clCommand` statements into Call `QSYS2.QCMDEX('clCommand')` procedure calls - * - Makes sure each comment (`--`) starts on a new line - * @param statement the statement to fix - * @returns statement compatible with QZDFMDB2 - */ - export function fixSQL(statement: string, removeComments = false): string { - let statements = statement.split("\n").map(line => { - if (line.startsWith('@')) { - //- Escape all ' - //- Remove any trailing ; - //- Put the command in a Call QSYS2.QCMDEXC statement - line = `Call QSYS2.QCMDEXC('${line.substring(1, line.endsWith(";") ? line.length - 1 : undefined).replaceAll("'", "''")}');`; - } - - //Make each comment start on a new line - return line.replaceAll("--", "\n--"); - }).join(`\n`); - - if (removeComments) { - statements = statements.split(`\n`).filter(l => !l.trim().startsWith(`--`)).join(`\n`); - } - - return statements; - } - export function fileToPath(file: EditorPath): string { if (typeof file === "string") { return Tools.fixWindowsPath(file); diff --git a/src/api/components/copyToImport.ts b/src/api/components/copyToImport.ts deleted file mode 100644 index 0c17c7e32..000000000 --- a/src/api/components/copyToImport.ts +++ /dev/null @@ -1,84 +0,0 @@ -import IBMi from "../IBMi"; -import { Tools } from "../Tools"; -import { WrapResult } from "../types"; -import { ComponentState, IBMiComponent } from "./component"; - -export class CopyToImport implements IBMiComponent { - static ID = 'CopyToImport'; - - static isSimple(statement: string): boolean { - statement = statement.trim(); - if (statement.endsWith(';')) { - statement = statement.substring(0, statement.length - 1); - } - - const parts = statement.split(` `); - return parts.length === 4 && parts[0].toUpperCase() === `SELECT` && parts[1] === `*` && parts[2].toUpperCase() === `FROM` && parts[3].includes(`.`); - } - - getIdentification() { - return { name: CopyToImport.ID, version: 1 }; - } - - getRemoteState(): ComponentState { - return `Installed`; - } - - update(): ComponentState | Promise { - return this.getRemoteState(); - } - - wrap(connection: IBMi, statement: string): WrapResult { - const outStmf = connection.getTempRemote(Tools.makeid())!; - - statement = statement.trim(); - if (statement.endsWith(';')) { - statement = statement.substring(0, statement.length - 1); - } - - statement = statement.replace(new RegExp(`for bit data`, `gi`), ``); - - let newStatements: string[] = []; - let requiresQtempTable = true; - - const parts = statement.split(` `); - - let library: string, table: string; - - // If it's a simple statement, then we should use fallback to CPYTOIMPF as it's faster in some cases. - if (parts.length === 4 && parts[0].toUpperCase() === `SELECT` && parts[1] === `*` && parts[2].toUpperCase() === `FROM` && parts[3].includes(`.`)) { - const [lib, file] = parts[3].toUpperCase().split(`.`); - if (file.length <= 10) { - requiresQtempTable = false; - library = lib; - table = file; - } - } - - if (requiresQtempTable) { - library = `QTEMP`; - table = Tools.makeid(5).toUpperCase(); - newStatements.push(`CREATE TABLE ${library}.${table} AS (${statement}) WITH DATA`); - } - - newStatements.push(`Call QSYS2.QCMDEXC('` + connection.getContent().toCl(`CPYTOIMPF`, { - FROMFILE: `${library!}/${table!} *FIRST`, - TOSTMF: outStmf, - MBROPT: `*REPLACE`, - STMFCCSID: 1208, - RCDDLM: `*CRLF`, - DTAFMT: `*DLM`, - RMVBLANK: `*TRAILING`, - ADDCOLNAM: `*SQL`, - FLDDLM: `','`, - DECPNT: `*PERIOD`, - STRDLM: `*DBLQUOTE`, - STRESCCHR: `*STRDLM` - }).replaceAll(`'`, `''`) + `')`); - - return { - newStatements, - outStmf - }; - } -} \ No newline at end of file diff --git a/src/api/components/cqsh/cqsh b/src/api/components/cqsh/cqsh deleted file mode 100644 index 363c5291e..000000000 Binary files a/src/api/components/cqsh/cqsh and /dev/null differ diff --git a/src/api/components/cqsh/cqsh.c b/src/api/components/cqsh/cqsh.c deleted file mode 100644 index b38bcaa74..000000000 --- a/src/api/components/cqsh/cqsh.c +++ /dev/null @@ -1,14 +0,0 @@ -#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/api/components/cqsh/index.ts b/src/api/components/cqsh/index.ts deleted file mode 100644 index 748846cf4..000000000 --- a/src/api/components/cqsh/index.ts +++ /dev/null @@ -1,95 +0,0 @@ - -import { stat } from "fs/promises"; -import path from "path"; -import IBMi from "../../IBMi"; -import { ComponentState, IBMiComponent } from "../component"; - -export class CustomQSh implements IBMiComponent { - static ID = "cqsh"; - private localAssetPath: string|undefined; - - setLocalAssetPath(newPath: string) { - this.localAssetPath = newPath; - } - - installPath = ""; - - getIdentification() { - return { name: CustomQSh.ID, version: 1 }; - } - - getFileName() { - const id = this.getIdentification(); - return `${id.name}_${id.version}`; - } - - async setInstallDirectory(installDirectory: string): Promise { - this.installPath = path.posix.join(installDirectory, this.getFileName()); - } - - async getRemoteState(connection: IBMi, installDirectory: string): Promise { - this.installPath = path.posix.join(installDirectory, this.getFileName()); - const result = await connection.getContent().testStreamFile(this.installPath, "x"); - - if (!result) { - return `NotInstalled`; - } - - const testResult = await this.testCommand(connection); - - if (!testResult) { - return `Error`; - } - - return `Installed`; - } - - async update(connection: IBMi): Promise { - if (!this.localAssetPath) { - return `Error`; - } - - const assetExistsLocally = await exists(this.localAssetPath); - - if (!assetExistsLocally) { - return `Error`; - } - - await connection.getContent().uploadFiles([{ local: this.localAssetPath, remote: this.installPath }]); - - await connection.sendCommand({ - command: `chmod +x ${this.installPath}`, - }); - - const testResult = await this.testCommand(connection); - - if (!testResult) { - return `Error`; - } - - return `Installed`; - } - - async testCommand(connection: IBMi) { - const text = `Hello world`; - const result = await connection.sendCommand({ - stdin: `echo "${text}"`, - command: this.installPath, - }); - - 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/api/components/getMemberInfo.ts b/src/api/components/getMemberInfo.ts index 23dcb1377..00c946a70 100644 --- a/src/api/components/getMemberInfo.ts +++ b/src/api/components/getMemberInfo.ts @@ -57,6 +57,11 @@ export class GetMemberInfo implements IBMiComponent { if (!tsString) { return undefined; } + + let possibleDate = new Date(tsString); + if (!isNaN(possibleDate.getTime())) { + return possibleDate; + } const dateParts = tsString.split('-'); const timeParts = dateParts[3].split('.'); @@ -165,7 +170,7 @@ function getSource(library: string, name: string, version: number) { `specific ${name}`, `modifies sql data`, `begin`, - ` declare buffer char( 135 ) for bit data not null default '';`, + ` declare buffer char( 135 ) not null default '';`, ` declare BUFLEN integer constant 135 ;`, ` declare FORMAT char( 8 ) constant 'MBRD0100' ;`, ` declare OVR char( 1 ) constant '0' ;`, diff --git a/src/api/components/manager.ts b/src/api/components/manager.ts index a8a631baa..003f7e285 100644 --- a/src/api/components/manager.ts +++ b/src/api/components/manager.ts @@ -122,20 +122,36 @@ export class ComponentManager { public async startup(lastInstalled: ComponentInstallState[] = []) { const components = this.getAllAvailableComponents(); for (const component of components) { - await component.reset?.(); - const newComponent = new IBMiComponentRuntime(this.connection, component); + await this.startupComponent(component.getIdentification().name, lastInstalled); + } + } - const installedBefore = lastInstalled.find(i => i.id.name === component.getIdentification().name); - const sameVersion = installedBefore && (installedBefore.id.version === component.getIdentification().version); + public async startupComponent(key: string, lastInstalled: ComponentInstallState[] = []) { + const component = this.getAllAvailableComponents().find(c => c.getIdentification().name === key); - if ((!installedBefore || !sameVersion || installedBefore.state === `NotChecked`)) { - await newComponent.startupCheck(); - } else if (installedBefore) { - await newComponent.overrideState(installedBefore.state); - } + if (!component) { + throw new Error(`Component ${key} not found.`); + } - this.registered.push(newComponent); + if (this.registered.find(c => c.component.getIdentification().name === component.getIdentification().name)) { + return; } + + await component.reset?.(); + const newComponent = new IBMiComponentRuntime(this.connection, component); + + const installedBefore = lastInstalled.find(i => i.id.name === component.getIdentification().name); + const sameVersion = installedBefore && (installedBefore.id.version === component.getIdentification().version); + const isUserManaged = component.getIdentification().userManaged; + + // Always check non-user-managed components to ensure they're actually installed + if ((!installedBefore || !sameVersion || installedBefore.state === `NotChecked` || !isUserManaged)) { + await newComponent.startupCheck(); + } else if (installedBefore) { + await newComponent.overrideState(installedBefore.state); + } + + this.registered.push(newComponent); } /** diff --git a/src/api/components/mapepire/index.ts b/src/api/components/mapepire/index.ts new file mode 100644 index 000000000..5f085fd8d --- /dev/null +++ b/src/api/components/mapepire/index.ts @@ -0,0 +1,108 @@ + +import { stat } from "fs/promises"; +import path from "path"; +import IBMi from "../../IBMi"; +import { ComponentState, IBMiComponent } from "../component"; +import { sshSqlJob } from "./sqlJob"; +import { SERVER_VERSION_FILE, VERSION_NUMBER } from "./version"; + +const DEFAULT_JAVA_EIGHT = `/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit/bin/java`; + +export class Mapepire implements IBMiComponent { + static ID = "mapepire"; + private localAssetPath: string | undefined; + + setLocalAssetPath(newPath: string) { + this.localAssetPath = newPath; + } + + installPath = ""; + + getIdentification() { + return { name: Mapepire.ID, version: VERSION_NUMBER }; + } + + getFileName() { + return SERVER_VERSION_FILE; + } + + async setInstallDirectory(installDirectory: string): Promise { + this.installPath = path.posix.join(installDirectory, this.getFileName()); + } + + async getRemoteState(connection: IBMi, installDirectory: string): Promise { + this.installPath = path.posix.join(installDirectory, this.getFileName()); + const result = await connection.getContent().testStreamFile(this.installPath, "x"); + + if (!result) { + return `NotInstalled`; + } + + return `Installed`; + } + + async update(connection: IBMi): Promise { + if (!this.localAssetPath) { + return `Error`; + } + + const assetExistsLocally = await exists(this.localAssetPath); + + if (!assetExistsLocally) { + return `Error`; + } + + await connection.sendCommand({ command: `rm ${this.installPath.substring(0, this.installPath.lastIndexOf('-'))}*` }); + await connection.getContent().uploadFiles([{ local: this.localAssetPath, remote: this.installPath }]); + + await connection.sendCommand({ + command: `chmod +x ${this.installPath}`, + }); + + return `Installed`; + } + + getInitCommand(javaVersion = DEFAULT_JAVA_EIGHT): string | undefined { + if (this.installPath) { + return `${javaVersion} -Dos400.stdio.convert=N -jar ${this.installPath} --single` + } + } + + public static async useExec(connection: IBMi) { + let useExec = false; + + const bashPathAvailable = connection.remoteFeatures[`bash`]; + if (bashPathAvailable) { + const commandShellResult = await connection.sendCommand({ + command: `echo $SHELL` + }); + if (!commandShellResult.stderr) { + let userDefaultShell = commandShellResult.stdout.trim(); + if (userDefaultShell === bashPathAvailable) { + useExec = true; + } + } + } + + return useExec; + } + + public async newJob(connection: IBMi, javaPath?: string) { + const sqlJob = new sshSqlJob(); + sqlJob.options.secure = connection.getConfig().secureSQL; + const stream = await sqlJob.getSshChannel(this, connection, javaPath); + await sqlJob.connectSsh(stream); + // sqlJob.setTraceConfig(`IN_MEM`, `ON`); + // sqlJob.enableLocalTrace(); + return sqlJob; + } +} + +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/api/components/mapepire/sqlJob.ts b/src/api/components/mapepire/sqlJob.ts new file mode 100644 index 000000000..7ed65ae0a --- /dev/null +++ b/src/api/components/mapepire/sqlJob.ts @@ -0,0 +1,170 @@ +import type { ConnectionResult, JDBCOptions, QueryResult, ServerRequest, ServerResponse } from "@ibm/mapepire-js"; +import { SQLJob } from "@ibm/mapepire-js"; +import { ClientChannel } from "ssh2"; +import { Mapepire } from "."; +import IBMi from "../../IBMi"; +import { JobStatus } from "./types"; + +export class sshSqlJob extends SQLJob { + static application = ""; + private channel: ClientChannel | undefined; + + private currentSchemaStore: string | undefined; + + resetCurrentSchemaCache() { + this.currentSchemaStore = undefined; + } + + // Explicitly declare inherited methods from SQLJob for TypeScript + declare query: (sql: string, opts?: any) => any; + declare execute: (sql: string, opts?: any) => Promise>; + + async getSshChannel(mapepire: Mapepire, connection: IBMi, javaPath?: string): Promise { + const useExec = await Mapepire.useExec(connection); + + return new Promise((resolve, reject) => { + // Setting QIBM_JAVA_STDIO_CONVERT and QIBM_PASE_DESCRIPTOR_STDIO to make sure all PASE and Java converters are off + const startingCommand = `QIBM_JAVA_STDIO_CONVERT=N QIBM_PASE_DESCRIPTOR_STDIO=B QIBM_USE_DESCRIPTOR_STDIO=Y QIBM_MULTI_THREADED=Y ${useExec ? `exec ` : ``}` + mapepire.getInitCommand(javaPath); + + // ServerComponent.writeOutput(startingCommand); + + const a = connection.client?.connection?.exec(startingCommand, {}, (err, stream) => { + if (err) { + reject(err); + // ServerComponent.writeOutput(err); + } + + let outString = ``; + + // TODO: on is undefined? + stream.stderr.on(`data`, (data: Buffer) => { + console.log(data); + }) + + stream.stdout.on(`data`, (data: Buffer) => { + outString += String(data); + if (outString.endsWith(`\n`)) { + for (const thisMsg of outString.split(`\n`)) { + if (thisMsg === ``) continue; + + outString = ``; + // if (this.isTracingChannelData) ServerComponent.writeOutput(thisMsg); + try { + let response: ServerResponse = JSON.parse(thisMsg); + this.responseEmitter.emit(response.id, response); + } catch (e: any) { + console.log(`Error: ` + e); + console.log(`Data: ` + thisMsg); + outString = ``; + } + } + } + }); + + resolve(stream); + }); + }) + } + + override async send(content: ServerRequest): Promise { + if (!this.channel) { + throw new Error("SQL client is not yet setup."); + } + if (this.isTracingChannelData) console.log(JSON.stringify(content)); + + this.channel.stdin.write(JSON.stringify(content) + `\n`); + return new Promise((resolve, reject) => { + this.responseEmitter.on(content.id, (x: ServerResponse) => { + this.responseEmitter.removeAllListeners(x.id); + resolve(x as T); + }); + }); + } + + getStatus(): JobStatus { + const currentListenerCount = this.responseEmitter.eventNames().length; + + return this.channel && currentListenerCount > 0 ? JobStatus.BUSY : this.status as JobStatus; + } + + /** + * The same as mapepire-js#connect, but with SSH + */ + async connectSsh(channel: ClientChannel): Promise { + // this.isTracingChannelData = true; + + this.channel = channel; + + this.channel.on(`error`, (err: any) => { + console.warn(err); + this.end(); + }) + + this.channel.on(`close`, (code: number) => { + console.warn(`Exited with code ${code}.`) + this.end(); + }) + + const props = (Object + .keys(this.options) as { [key: string]: any }) + .filter((prop: keyof JDBCOptions) => this.options[prop] !== `` && this.options[prop] !== null && this.options[prop] !== undefined) // 0 is valid + .map((prop: keyof JDBCOptions) => { + if (Array.isArray(this.options[prop])) { + return `${prop}=${(this.options[prop] as string[]).join(`,`)}`; + } else { + return `${prop}=${this.options[prop]}`; + } + }) + .join(`;`) + + const connectionObject = { + id: sshSqlJob.getNewUniqueId(), + type: `connect`, + //technique: (getInstance().getConnection().qccsid === 65535 || this.options["database name"]) ? `tcp` : `cli`, //TODO: investigate why QCCSID 65535 breaks CLI and if there is any workaround + technique: `tcp`, // TODO: DOVE does not work in cli mode + application: sshSqlJob.application, + props: props.length > 0 ? props : undefined + } + + const connectResult = await this.send(connectionObject); + + if (connectResult.success === true) { + this.status = JobStatus.READY; + } else { + this.end(); + this.status = JobStatus.NOT_STARTED; + throw new Error(connectResult.error || `Failed to connect to server.`); + } + + this.id = connectResult.job; + this.isTracingChannelData = false; + + return connectResult; + } + + async close() { + const exitObject: ServerRequest = { + id: sshSqlJob.getNewUniqueId(), + type: `exit` + }; + + this.send(exitObject); + + this.responseEmitter.eventNames().forEach(event => { + this.responseEmitter.emit(event, JSON.stringify({ + id: event, + success: false, + error: `Job ended before response returned.` + })); + }); + + this.end(); + } + + private end() { + this.channel?.close(); + this.channel = undefined; + this.status = JobStatus.ENDED; + this.responseEmitter.removeAllListeners(); + } +} \ No newline at end of file diff --git a/src/api/components/mapepire/types.ts b/src/api/components/mapepire/types.ts new file mode 100644 index 000000000..dcd7c367c --- /dev/null +++ b/src/api/components/mapepire/types.ts @@ -0,0 +1,9 @@ + +// Redefined from mapepire-js +export enum JobStatus { + NOT_STARTED = "notStarted", + CONNECTING = "connecting", + READY = "ready", + BUSY = "busy", + ENDED = "ended" +} diff --git a/src/api/components/mapepire/version.ts b/src/api/components/mapepire/version.ts new file mode 100644 index 000000000..32532f446 --- /dev/null +++ b/src/api/components/mapepire/version.ts @@ -0,0 +1,5 @@ + +export const VERSION = `2.3.3`; +export const VERSION_NUMBER = 233; +export const SERVER_VERSION_TAG = `v${VERSION}`; +export const SERVER_VERSION_FILE = `mapepire-server-${VERSION}.jar`; diff --git a/src/api/components/runtime.ts b/src/api/components/runtime.ts index 244d663d9..74b6f3a43 100644 --- a/src/api/components/runtime.ts +++ b/src/api/components/runtime.ts @@ -46,7 +46,7 @@ export class IBMiComponentRuntime { const newState = await this.component.getRemoteState(this.connection, installDirectory); await this.setState(newState); if (newState !== `Installed` && !this.component.getIdentification().userManaged) { - this.update(installDirectory); + await this.update(installDirectory); } } catch (error) { diff --git a/src/api/configuration/config/ConnectionManager.ts b/src/api/configuration/config/ConnectionManager.ts index 6958c2602..8f1bdf912 100644 --- a/src/api/configuration/config/ConnectionManager.ts +++ b/src/api/configuration/config/ConnectionManager.ts @@ -43,7 +43,8 @@ function initialize(parameters: Partial): ConnectionConfig { defaultDeploymentMethod: parameters.defaultDeploymentMethod || ``, protectedPaths: (parameters.protectedPaths || []), showHiddenFiles: (parameters.showHiddenFiles === true || parameters.showHiddenFiles === undefined), - lastDownloadLocation: (parameters.lastDownloadLocation || os.homedir()) + lastDownloadLocation: (parameters.lastDownloadLocation || os.homedir()), + secureSQL: (parameters.secureSQL === true), } } diff --git a/src/api/configuration/config/types.ts b/src/api/configuration/config/types.ts index de91dc157..44f5d7ee8 100644 --- a/src/api/configuration/config/types.ts +++ b/src/api/configuration/config/types.ts @@ -1,5 +1,5 @@ import { FilterType } from "../../Filter"; -import { DeploymentMethod, ConnectionData } from "../../types"; +import { ConnectionData, DeploymentMethod } from "../../types"; export type DefaultOpenMode = "browse" | "edit"; export type ReconnectMode = "always" | "never" | "ask"; @@ -33,6 +33,7 @@ export interface ConnectionConfig extends ConnectionProfile { protectedPaths: string[]; showHiddenFiles: boolean; lastDownloadLocation: string; + secureSQL: boolean; [name: string]: any; } diff --git a/src/api/configuration/storage/CodeForIStorage.ts b/src/api/configuration/storage/CodeForIStorage.ts index eb219dfbe..82cfff831 100644 --- a/src/api/configuration/storage/CodeForIStorage.ts +++ b/src/api/configuration/storage/CodeForIStorage.ts @@ -26,7 +26,6 @@ export type CachedServerSettings = { badDataAreasChecked: boolean | null libraryListValidated: boolean | null pathChecked?: boolean - userDefaultCCSID: number | null debugConfigLoaded: boolean maximumArgsLength: number } | undefined; diff --git a/src/api/queue.ts b/src/api/queue.ts new file mode 100644 index 000000000..d752766b3 --- /dev/null +++ b/src/api/queue.ts @@ -0,0 +1,59 @@ +export class SimpleQueue { + private static _instance: SimpleQueue | null = null; + + static get instance() { + if (!this._instance) { + this._instance = new SimpleQueue(); + } + return this._instance; + } + + private delay = 0; // milliseconds + private queue: (() => Promise)[] = []; + private queueRunning = false; + + setDelay(delay: number) { + this.delay = delay; + } + + next(run: () => Promise, cancelCheck?: () => boolean): Promise { + return new Promise((resolve, reject) => { + this.queue.push(async () => { + if (this.delay) { + // We intentially do the cancel check twice. + if (cancelCheck && cancelCheck()) { + return undefined; + } + + await new Promise(r => setTimeout(r, this.delay)); // delay before running + } + + try { + if (cancelCheck && cancelCheck()) { + return undefined; + } + + const result = await run(); + resolve(result); + } catch (err) { + reject(err); + } + }); + this.runNext(); + }); + } + + private async runNext() { + if (this.queueRunning) {return;} + const task = this.queue.shift(); + if (!task) {return;} + + this.queueRunning = true; + try { + await task(); + } finally { + this.queueRunning = false; + this.runNext(); // run next item in queue + } + } +} diff --git a/src/api/tests/connection.ts b/src/api/tests/connection.ts index dcdc4a0e0..1226579fd 100644 --- a/src/api/tests/connection.ts +++ b/src/api/tests/connection.ts @@ -1,7 +1,5 @@ import path from "path"; import IBMi from "../IBMi"; -import { CopyToImport } from "../components/copyToImport"; -import { CustomQSh } from "../components/cqsh"; import { GetMemberInfo } from "../components/getMemberInfo"; import { GetNewLibl } from "../components/getNewLibl"; import { extensionComponentRegistry } from "../components/manager"; @@ -9,6 +7,8 @@ import { CodeForIStorage } from "../configuration/storage/CodeForIStorage"; import { ConnectionData } from "../types"; import { CustomCLI } from "./components/customCli"; import { JSONConfig, JsonStorage } from "./testConfigSetup"; +import { Mapepire } from "../components/mapepire"; +import { SERVER_VERSION_FILE } from "../components/mapepire/version"; export const testStorage = new JsonStorage(); const testConfig = new JSONConfig(); @@ -50,16 +50,14 @@ export async function newConnection(reloadSettings?: boolean) { const conn = new IBMi(); - const customQsh = new CustomQSh(); - const cqshPath = path.join(__dirname, `..`, `components`, `cqsh`, `cqsh`); - customQsh.setLocalAssetPath(cqshPath); + const mapepire = new Mapepire(); + const mapepireAssetPath = path.join(__dirname, `..`, `..`, `..`, `dist`, SERVER_VERSION_FILE); + mapepire.setLocalAssetPath(mapepireAssetPath); const testingId = `testing`; - extensionComponentRegistry.registerComponent(testingId, customQsh); + extensionComponentRegistry.registerComponent(testingId, mapepire); extensionComponentRegistry.registerComponent(testingId, new GetNewLibl()); extensionComponentRegistry.registerComponent(testingId, new GetMemberInfo()); - extensionComponentRegistry.registerComponent(testingId, new CopyToImport()); - extensionComponentRegistry.registerComponent(testingId, new CustomCLI()); const creds: ConnectionData = { diff --git a/src/api/tests/setup.ts b/src/api/tests/setup.ts index 2eb812cb1..73c9d2f74 100644 --- a/src/api/tests/setup.ts +++ b/src/api/tests/setup.ts @@ -1,16 +1,30 @@ import type { TestProject } from "vitest/node"; import { disposeConnection, newConnection } from "./connection"; +import { existsSync } from "fs"; +import path from "path"; +import { JSONConfig, JsonStorage } from "./testConfigSetup"; export async function setup(project: TestProject) { // Pre-connects to create/refresh the configuration files. // When the config files exist, it makes future connections just slightly faster. // Mostly useful during the CI stage. - console.log(``); - console.log(`Connecting before tests run to create/refresh settings.`); - const conn = await newConnection(true); - await disposeConnection(conn); + const configsExist = exists(JSONConfig.NAME) && exists(JsonStorage.NAME); - console.log(`Testing connection complete. Settings written/refreshed.`); - console.log(``); + if (configsExist) { + console.log(`Connection settings already exists. Skipped connection setup.`); + } else { + console.log(``); + console.log(`Connecting before tests run to create/refresh settings.`); + const conn = await newConnection(true); + await disposeConnection(conn); + + console.log(`Testing connection complete. Settings written/refreshed.`); + console.log(``); + } +} + +function exists(fileName: string) { + const fullPath = path.join(__dirname, fileName); + return existsSync(fullPath); } \ No newline at end of file diff --git a/src/api/tests/suites/components.test.ts b/src/api/tests/suites/components.test.ts index 5c628fdd5..0864fc529 100644 --- a/src/api/tests/suites/components.test.ts +++ b/src/api/tests/suites/components.test.ts @@ -99,6 +99,7 @@ describe('Component Tests', () => { try { await manager.uninstallComponent(CustomCLI.ID); } catch (e) { + console.log(e); console.log(`Component not installed, skipping uninstall.`); } diff --git a/src/api/tests/suites/content.test.ts b/src/api/tests/suites/content.test.ts index 543a21e5a..8b16f3ccb 100644 --- a/src/api/tests/suites/content.test.ts +++ b/src/api/tests/suites/content.test.ts @@ -236,7 +236,7 @@ describe('Content Tests', { concurrent: true }, () => { await connection.runSQL('select from qiws.qcustcdt'); expect.fail('Should have thrown an error'); } catch (e: any) { - expect(e.message.endsWith(': , FROM INTO. (42601)')).toBeTruthy(); + expect(e.message.endsWith(': , FROM INTO., 42601, -104')).toBeTruthy(); expect(e.sqlstate).toBe('42601'); } }); @@ -411,7 +411,9 @@ describe('Content Tests', { concurrent: true }, () => { let members = await content?.getMemberList({ library: 'qsysinc', sourceFile: 'mih', members: '*inxen' }); - expect(members?.length).toBe(3); + for (const member of members) { + expect(member.name.endsWith(`INXEN`)).toBeTruthy(); + } members = await content?.getMemberList({ library: 'qsysinc', sourceFile: 'mih' }); @@ -526,18 +528,10 @@ describe('Content Tests', { concurrent: true }, () => { it('Test @clCommand + select statement', async () => { const content = connection.getContent(); - const [resultA] = await connection.runSQL(`@CRTSAVF FILE(QTEMP/UNITTEST) TEXT('Code for i test');\nSelect * From Table(QSYS2.OBJECT_STATISTICS('QTEMP', '*FILE')) Where OBJATTRIBUTE = 'SAVF';`); + const [resultA] = await connection.runSQL(`@CRTSAVF FILE(QTEMP/UNITTESTA) TEXT('Code for i test');\nSelect * From Table(QSYS2.OBJECT_STATISTICS('QTEMP', '*FILE')) Where OBJATTRIBUTE = 'SAVF';`); - expect(resultA.OBJNAME).toBe('UNITTEST'); + expect(resultA.OBJNAME).toBe('UNITTESTA'); expect(resultA.OBJTEXT).toBe('Code for i test'); - - const [resultB] = await content.runStatements( - `@CRTSAVF FILE(QTEMP/UNITTEST) TEXT('Code for i test')`, - `Select * From Table(QSYS2.OBJECT_STATISTICS('QTEMP', '*FILE')) Where OBJATTRIBUTE = 'SAVF'` - ); - - expect(resultB.OBJNAME).toBe('UNITTEST'); - expect(resultB.OBJTEXT).toBe('Code for i test'); }); it('should get attributes', async () => { @@ -641,7 +635,36 @@ describe('Content Tests', { concurrent: true }, () => { expect(members?.length).toBe(1); } finally { - await connection.runCommand({ command: `RUNSQL 'drop schema "${longName}"' commit(*none)`, noLibList: true }); + // The job goes into MSGW (message waiting) status with message CPA7025 "Receiver QSQJRN0001 in never fully saved. (I C)" + // We need to add a reply list entry and change the job to use the system reply list + + // Add reply list entry to automatically reply to CPA7025 with 'I' + await connection.runCommand({ + command: `ADDRPYLE SEQNBR(9999) MSGID(CPA7025) RPY('I')`, + }); + + // Change job to use system reply list + await connection.runCommand({ + command: `CHGJOB INQMSGRPY(*SYSRPYL)`, + }); + + try { + // Now drop the schema - it will automatically reply to CPA7025 + await connection.runCommand({ + command: `RUNSQL 'drop schema "${longName}"' commit(*none)`, + noLibList: true + }); + } finally { + // Restore job to default inquiry message reply + await connection.runCommand({ + command: `CHGJOB INQMSGRPY(*RQD)`, + }); + + // Clean up the reply list entry + await connection.runCommand({ + command: `RMVRPYLE SEQNBR(9999)`, + }); + } } } else { throw new Error(`Failed to create schema "${longName}"`); @@ -655,7 +678,6 @@ describe('Content Tests', { concurrent: true }, () => { const id = `${Tools.makeid().toUpperCase()}`; await connection.withTempDirectory(async directory => { const source = `${directory}/vscodetemp-${id}.clle`; - console.log(source); try { await content.runStatements( `CALL QSYS2.IFS_WRITE(PATH_NAME =>'${source}', @@ -666,9 +688,10 @@ describe('Content Tests', { concurrent: true }, () => { LINE => 'ENDPGM', OVERWRITE => 'APPEND', END_OF_LINE => 'CRLF')`, - `@CRTCLMOD MODULE(${tempLib}/${id}) SRCSTMF('${source}')`, - `select 1 from sysibm.sysdummy1` ); + + await connection.runCommand({environment: `ile`, command: `CRTCLMOD MODULE(${tempLib}/${id}) SRCSTMF('${source}')`}) + let exports: ModuleExport[] = await content.getModuleExports(tempLib, id); expect(exports.length).toBe(1); diff --git a/src/api/tests/suites/encoding.test.ts b/src/api/tests/suites/encoding.test.ts index 61dada617..ca359350c 100644 --- a/src/api/tests/suites/encoding.test.ts +++ b/src/api/tests/suites/encoding.test.ts @@ -16,39 +16,6 @@ const contents = { const SHELL_CHARS = [`$`, `#`]; -async function runCommandsWithCCSID(connection: IBMi, commands: string[], ccsid: number) { - const testPgmSrcFile = connection.upperCaseName(Tools.makeid(6)); - const config = connection.getConfig(); - - const tempLib = config.tempLibrary; - const testPgmName = connection.upperCaseName(`T${commands.length}${ccsid}${Tools.makeid(2)}`); - - await connection.runCommand({ command: `DLTOBJ OBJ(${tempLib}/${testPgmSrcFile}) OBJTYPE(*FILE)`, noLibList: true }); - await connection.runCommand({ command: `DLTOBJ OBJ(${tempLib}/${testPgmName}) OBJTYPE(*PGM)`, noLibList: true }); - - const sourceFileCreated = await connection!.runCommand({ command: `CRTSRCPF FILE(${tempLib}/${testPgmSrcFile}) RCDLEN(112) CCSID(${ccsid})`, noLibList: true }); - - try { - await connection.getContent().uploadMemberContent(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; - } - finally { - await connection.runCommand({ command: `DLTOBJ OBJ(${tempLib}/${testPgmSrcFile}) OBJTYPE(*FILE)`, noLibList: true }); - await connection.runCommand({ command: `DLTOBJ OBJ(${tempLib}/${testPgmName}) OBJTYPE(*PGM)`, noLibList: true }); - } -} - describe('Encoding tests', { concurrent: true }, () => { let connection: IBMi beforeAll(async () => { @@ -59,7 +26,7 @@ describe('Encoding tests', { concurrent: true }, () => { await disposeConnection(connection); }); - it('Prove that input strings are messed up by CCSID', async () => { + it('Prove that input strings are NOT messed up by CCSID', async () => { let howManyTimesItMessedUpTheResult = 0; for (const strCcsid in contents) { @@ -79,7 +46,7 @@ describe('Encoding tests', { concurrent: true }, () => { } } - expect(howManyTimesItMessedUpTheResult).toBeTruthy(); + expect(howManyTimesItMessedUpTheResult).toBe(0); }); it('Compare Unicode to EBCDIC successfully', async () => { @@ -194,7 +161,6 @@ describe('Encoding tests', { concurrent: true }, () => { const content = connection.getContent(); if (connection && content) { const tempLib = connection.getConfig().tempLibrary; - const ccsid = connection.getCcsid(); let library = `TESTLIB${connection.variantChars.local}`; let skipLibrary = false; @@ -214,16 +180,15 @@ describe('Encoding tests', { concurrent: true }, () => { skipLibrary = true; } - let commands: string[] = []; + let result = await connection.runCommand({command: `CRTSRCPF FILE(${library}/${sourceFile}) RCDLEN(112)`}); + expect(result.code).toBe(0); - 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) TEXT('Test ${member}')`); + result = await connection.runCommand({command: `ADDPFM FILE(${library}/${sourceFile}) MBR(${member}) SRCTYPE(TXT) TEXT('Test ${member}')`}); + expect(result.code).toBe(0); } - commands.push(`CRTDTAARA DTAARA(${library}/${dataArea}) TYPE(*CHAR) LEN(50) VALUE('hi')`); - - const result = await runCommandsWithCCSID(connection, commands, ccsid); + result = await connection.runCommand({command: `CRTDTAARA DTAARA(${library}/${dataArea}) TYPE(*CHAR) LEN(50) VALUE('hi')`}); expect(result.code).toBe(0); if (!skipLibrary) { @@ -286,7 +251,6 @@ describe('Encoding tests', { concurrent: true }, () => { 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 }); @@ -297,7 +261,7 @@ describe('Encoding tests', { concurrent: true }, () => { } try { - const createSourceFileCommand = await connection.runCommand({ command: `CRTSRCPF FILE(${library}/${sourceFile}) RCDLEN(112) CCSID(${ccsid})`, noLibList: true }); + const createSourceFileCommand = await connection.runCommand({ command: `CRTSRCPF FILE(${library}/${sourceFile}) RCDLEN(112)`, noLibList: true }); expect(createSourceFileCommand.code).toBe(0); const addPf = await connection.runCommand({ command: `ADDPFM FILE(${library}/${sourceFile}) MBR(${member}) SRCTYPE(TXT)`, noLibList: true }); @@ -319,7 +283,6 @@ describe('Encoding tests', { concurrent: true }, () => { it('Variant character in source names and commands', async () => { const config = connection.getConfig(); - const ccsidData = connection.getCcsids()!; const tempLib = config.tempLibrary; async function testSingleVariant(varChar: string) { @@ -329,23 +292,15 @@ describe('Encoding tests', { concurrent: true }, () => { 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); + const createResult = await connection.runCommand({command: `CRTSRCPF FILE(${tempLib}/${testFile}) RCDLEN(112)`}); expect(createResult.code).toBe(0); try { const addPf = await connection.runCommand({ command: `ADDPFM FILE(${tempLib}/${testFile}) MBR(${testMember}) SRCTYPE(TXT)`, noLibList: true }); expect(addPf.code).toBe(0); - const attributes = await connection.getContent().getAttributes({ library: tempLib, name: testFile, member: testMember }, `CCSID`); - expect(attributes).toBeTruthy(); - expect(attributes![`CCSID`]).toBe(String(ccsidData.userDefaultCCSID)); - const addPfB = await connection.runCommand({ command: `ADDPFM FILE(${tempLib}/${testFile}) MBR(${variantMember}) SRCTYPE(TXT)`, noLibList: true }); expect(addPfB.code).toBe(0); - const attributesB = await connection.getContent().getAttributes({ library: tempLib, name: testFile, member: variantMember }, `CCSID`); - expect(attributesB).toBeTruthy(); - expect(attributesB![`CCSID`]).toBe(String(ccsidData.userDefaultCCSID)); - const objects = await connection.getContent().getObjectList({ library: tempLib, types: [`*SRCPF`] }); expect(objects.length).toBeTruthy(); expect(objects.some(obj => obj.name === testFile)).toBeTruthy(); @@ -366,7 +321,6 @@ describe('Encoding tests', { concurrent: true }, () => { await connection.getContent().uploadMemberContent(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 }); - console.log(compileResult); expect(compileResult.code).toBe(0); await connection.runCommand({ command: `DLTOBJ OBJ(${tempLib}/${testMember}) OBJTYPE(*PGM)`, noLibList: true }); diff --git a/src/api/tests/testConfigSetup.ts b/src/api/tests/testConfigSetup.ts index d839a7019..2682f1630 100644 --- a/src/api/tests/testConfigSetup.ts +++ b/src/api/tests/testConfigSetup.ts @@ -4,7 +4,6 @@ import { Config } from "../configuration/config/VirtualConfig"; import { BaseStorage } from "../configuration/storage/BaseStorage"; class JSONMap extends Map { - constructor(private readonly filePath: string) { if (existsSync(filePath)) { const data = JSON.parse(readFileSync(filePath).toString("utf-8")); @@ -21,7 +20,8 @@ class JSONMap extends Map { } export class JSONConfig extends Config { - private readonly config: JSONMap = new JSONMap(path.join(__dirname, `.config.json`)); + public static readonly NAME = `.config.json`; + private readonly config: JSONMap = new JSONMap(path.join(__dirname, JSONConfig.NAME)); public save() { this.config.save(); @@ -37,10 +37,11 @@ export class JSONConfig extends Config { } export class JsonStorage extends BaseStorage { + public static readonly NAME = `.storage.json`; private readonly config: JSONMap; constructor() { - const jsonMap = new JSONMap(path.join(__dirname, `.storage.json`)); + const jsonMap = new JSONMap(path.join(__dirname, JsonStorage.NAME)); super(jsonMap); this.config = jsonMap; } diff --git a/src/api/types.ts b/src/api/types.ts index a79caa4a1..554ebd0ab 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -26,7 +26,8 @@ export interface RemoteCommand { environment?: ActionEnvironment; cwd?: string; env?: Record | Variables; - noLibList?: boolean + noLibList?: boolean, + skipDetail?: boolean } export interface CommandData extends StandardIO { diff --git a/src/commands/open.ts b/src/commands/open.ts index d2f04fd3a..201514540 100644 --- a/src/commands/open.ts +++ b/src/commands/open.ts @@ -175,8 +175,8 @@ export function registerOpenCommands(instance: Instance): Disposable[] { // Create a cache for Schema if autosuggest enabled if (schemaItems.length === 0 && connection?.enableSQL) { content!.runSQL(` - select cast( SYSTEM_SCHEMA_NAME as char( 10 ) for bit data ) as SYSTEM_SCHEMA_NAME - , ifnull( cast( SCHEMA_TEXT as char( 50 ) for bit data ), '' ) as SCHEMA_TEXT + select cast( SYSTEM_SCHEMA_NAME as char( 10 ) ) as SYSTEM_SCHEMA_NAME + , ifnull( cast( SCHEMA_TEXT as char( 50 ) ), '' ) as SCHEMA_TEXT from QSYS2.SYSSCHEMAS order by 1` ).then(resultSetLibrary => { @@ -245,7 +245,7 @@ export function registerOpenCommands(instance: Instance): Disposable[] { ] resultSet = await connection.runSQL(` - select ifnull( cast( SYSTEM_TABLE_NAME as char( 10 ) for bit data ), '' ) as SYSTEM_TABLE_NAME + select ifnull( cast( SYSTEM_TABLE_NAME as char( 10 ) ), '' ) as SYSTEM_TABLE_NAME , ifnull( TABLE_TEXT, '' ) as TABLE_TEXT from QSYS2.SYSTABLES where SYSTEM_TABLE_SCHEMA = '${connection!.sysNameInAmerican(selectionSplit[0])}' @@ -287,7 +287,7 @@ export function registerOpenCommands(instance: Instance): Disposable[] { filterText = filterText.endsWith(`.`) ? filterText.substring(0, filterText.length - 1) : filterText; resultSet = await connection.runSQL(` - select cast( SYSTEM_TABLE_MEMBER as char( 10 ) for bit data ) as SYSTEM_TABLE_MEMBER + select cast( SYSTEM_TABLE_MEMBER as char( 10 ) ) as SYSTEM_TABLE_MEMBER , ifnull( PARTITION_TEXT, '' ) as PARTITION_TEXT , ifnull( SOURCE_TYPE, '' ) as SOURCE_TYPE from QSYS2.SYSPARTITIONSTAT diff --git a/src/extension.ts b/src/extension.ts index f9545e6e5..53ad0e560 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,11 +6,11 @@ import { commands, ExtensionContext, languages, window, workspace } from "vscode import path from "path"; import IBMi from "./api/IBMi"; -import { CopyToImport } from "./api/components/copyToImport"; -import { CustomQSh } from "./api/components/cqsh"; import { GetMemberInfo } from "./api/components/getMemberInfo"; import { GetNewLibl } from "./api/components/getNewLibl"; import { extensionComponentRegistry } from "./api/components/manager"; +import { Mapepire } from "./api/components/mapepire"; +import { sshSqlJob } from "./api/components/mapepire/sqlJob"; import { parseErrors } from "./api/errors/parser"; import { CustomCLI } from "./api/tests/components/customCli"; import { onCodeForIBMiConfigurationChange } from "./config/Configuration"; @@ -43,6 +43,8 @@ export async function activate(context: ExtensionContext): Promise // This line of code will only be executed once when your extension is activated console.log(`Congratulations, your extension "code-for-ibmi" is now active!`); + sshSqlJob.application = `${context.extension.packageJSON.name} ${context.extension.packageJSON.version}`; + await loadAllofExtension(context); const updateLastConnectionAndServerCache = () => { @@ -115,13 +117,12 @@ export async function activate(context: ExtensionContext): Promise commands.executeCommand("code-for-ibmi.refreshProfileView"); }); - const customQsh = new CustomQSh(); - customQsh.setLocalAssetPath(path.join(context.extensionPath, `dist`, customQsh.getFileName())); + const mapepire = new Mapepire(); + mapepire.setLocalAssetPath(path.join(context.extensionPath, `dist`, mapepire.getFileName())); - extensionComponentRegistry.registerComponent(context, customQsh); + extensionComponentRegistry.registerComponent(context, mapepire); extensionComponentRegistry.registerComponent(context, new GetNewLibl); extensionComponentRegistry.registerComponent(context, new GetMemberInfo()); - extensionComponentRegistry.registerComponent(context, new CopyToImport()); registerURIHandler(context, sandboxURIHandler, diff --git a/src/testing/encoding.ts b/src/testing/encoding.ts index 9860993c5..c36aad1d2 100644 --- a/src/testing/encoding.ts +++ b/src/testing/encoding.ts @@ -187,7 +187,7 @@ export const EncodingSuite: TestSuite = { 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); + const createResult = await runCommandsWithCCSID(connection, [`CRTSRCPF FILE(${tempLib}/${testFile}) RCDLEN(112) CCSID(${ccsidData.runtimeCcsid})`], ccsidData.runtimeCcsid); assert.strictEqual(createResult.code, 0); const addPf = await connection.runCommand({ command: `ADDPFM FILE(${tempLib}/${testFile}) MBR(${testMember}) SRCTYPE(TXT)`, noLibList: true }); @@ -195,7 +195,7 @@ export const EncodingSuite: TestSuite = { const attributes = await connection.getContent().getAttributes({ library: tempLib, name: testFile, member: testMember }, `CCSID`); assert.ok(attributes); - assert.strictEqual(attributes[`CCSID`], String(ccsidData.userDefaultCCSID)); + assert.strictEqual(attributes[`CCSID`], String(ccsidData.runtimeCcsid)); /// Test for getAttributes on member with all variants @@ -204,7 +204,7 @@ export const EncodingSuite: TestSuite = { const attributesB = await connection.getContent().getAttributes({ library: tempLib, name: testFile, member: variantMember }, `CCSID`); assert.ok(attributesB); - assert.strictEqual(attributesB[`CCSID`], String(ccsidData.userDefaultCCSID)); + assert.strictEqual(attributesB[`CCSID`], String(ccsidData.runtimeCcsid)); /// ----- diff --git a/src/testing/tools.ts b/src/testing/tools.ts index 6449f09c8..fd5d00606 100644 --- a/src/testing/tools.ts +++ b/src/testing/tools.ts @@ -104,15 +104,6 @@ export const ToolsSuite: TestSuite = { assert.deepStrictEqual(sanitizedLibraryNames, [`QTEMP`, `"#LIBRARY"`, `My$lib`, `qsysinc`]); }, }, - { - name: `fixQZDFMDB2Statement`, test: async () => { - let statement = Tools.fixSQL('Select * From MYTABLE -- This is a comment') - assert.deepStrictEqual(statement, 'Select * From MYTABLE \n-- This is a comment'); - - statement = Tools.fixSQL("@COMMAND LIB(QTEMP/*ALL) TEXT('Hello!');\nSelect * From QTEMP.MYTABLE -- This is mytable"); - assert.deepStrictEqual(statement, "Call QSYS2.QCMDEXC('COMMAND LIB(QTEMP/*ALL) TEXT(''Hello!'')');\nSelect * From QTEMP.MYTABLE \n-- This is mytable"); - } - }, { name: `EN result set test`, test: async () => { const lines = [ diff --git a/src/ui/views/helpView.ts b/src/ui/views/helpView.ts index 4a010bcff..cc5257a38 100644 --- a/src/ui/views/helpView.ts +++ b/src/ui/views/helpView.ts @@ -315,9 +315,7 @@ async function getRemoteSection() { `|Tech Refresh|${osVersion?.TR || '?'}|`, `|CCSID Origin|${ccsids.qccsid}|`, `|Runtime CCSID|${ccsids.runtimeCcsid || '?'}|`, - `|Default CCSID|${ccsids.userDefaultCCSID || '?'}|`, - `|SSHD CCSID|${ccsids.sshdCcsid || '?'}|`, - `|cqsh|${connection.canUseCqsh}|`, + `|SQL runner|${connection.sqlRunnerAvailable}|`, `|SQL|${connection.enableSQL ? 'Enabled' : 'Disabled'}`, `|Source dates|${config.enableSourceDates ? 'Enabled' : 'Disabled'}`, '', diff --git a/src/webviews/settings/index.ts b/src/webviews/settings/index.ts index 2fbf20fe0..412934e58 100644 --- a/src/webviews/settings/index.ts +++ b/src/webviews/settings/index.ts @@ -75,7 +75,7 @@ export class SettingsUI { } } - const restartFields = [`readOnlyMode`, `showDescInLibList`, `tempDir`, `debugCertDirectory`]; + const restartFields = [`readOnlyMode`, `showDescInLibList`, `tempDir`, `debugCertDirectory`, 'secureSQL']; let restart = false; const featuresTab = new Section(); @@ -89,6 +89,7 @@ export class SettingsUI { featuresTab .addCheckbox(`readOnlyMode`, `Read only mode`, `When enabled, content on the server can not be changed. Requires restart when changed.`, config.readOnlyMode) .addHorizontalRule() + .addCheckbox(`secureSQL`, `Use secure SQL connection`, `When enabled, Mapepire will connect to the databse using TLS (i.e. the secure JDBC option will be enabled)`, config.secureSQL) .addCheckbox(`quickConnect`, `Quick Connect`, `When enabled, server settings from previous connection will be used, resulting in much quicker connection. If server settings are changed, right-click the connection in Connection Browser and select Connect and Reload Server Settings to refresh the cache.`, config.quickConnect) .addCheckbox(`showDescInLibList`, `Show description of libraries in User Library List view`, `When enabled, library text and attribute will be shown in User Library List. It is recommended to also enable SQL for this.`, config.showDescInLibList) .addCheckbox(`showHiddenFiles`, `Show hidden files and directories in IFS browser.`, `When disabled, hidden files and directories (i.e. names starting with '.') will not be shown in the IFS browser, except for special config files.`, config.showHiddenFiles) diff --git a/tools/downloadMapepire.ts b/tools/downloadMapepire.ts new file mode 100644 index 000000000..b4014ee2a --- /dev/null +++ b/tools/downloadMapepire.ts @@ -0,0 +1,71 @@ +import path from "path"; + +import { Octokit } from "@octokit/rest"; +import { existsSync, mkdirSync, statSync } from "fs"; +import { writeFile } from "fs/promises"; +import { SERVER_VERSION_FILE, SERVER_VERSION_TAG } from "../src/api/components/mapepire/version"; + +async function work() { + const distDirectory = path.join(`.`, `dist`); + if (!existsSync(distDirectory)) { + mkdirSync(distDirectory); + } + + const serverFile = path.join(distDirectory, SERVER_VERSION_FILE); + + if (exists(serverFile)) { + console.log(`Server file exists: ${SERVER_VERSION_FILE}`) + return; + } + + const octokit = new Octokit(); + + const owner = `Mapepire-IBMi`; + const repo = `mapepire-server`; + + try { + const result = await octokit.request(`GET /repos/{owner}/{repo}/releases/tags/${SERVER_VERSION_TAG}`, { + owner, + repo, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + + const newAsset = result.data.assets.find((asset: any) => asset.name.endsWith(`.jar`)); + + if (newAsset) { + console.log(`Asset found: ${newAsset.name}`); + + const url = newAsset.browser_download_url; + + await downloadFile(url, serverFile); + + console.log(`Asset downloaded: ${serverFile}`); + + } else { + console.log(`Release found but no asset found.`); + } + + + } catch (e) { + console.log(e); + } +} + +function downloadFile(url: string, outputPath: string) { + return fetch(url) + .then(x => x.arrayBuffer()) + .then(x => writeFile(outputPath, Buffer.from(x))); +} + +function exists(localPath: string): boolean { + try { + statSync(localPath) + return true; + } catch (e) { + return false; + } +} + +work(); \ No newline at end of file diff --git a/tools/locales.ts b/tools/locales.ts deleted file mode 100644 index 5d5e88cde..000000000 --- a/tools/locales.ts +++ /dev/null @@ -1,81 +0,0 @@ -import fs from 'fs'; -import { basename } from 'path'; - -async function translate() { - const ids = fs.readdirSync('src/locale/ids').map(id => `src/locale/ids/${id}`); - const main = ids.find(id => id.endsWith('en.json')); - const others = ids.filter(id => !id.endsWith('en.json')); - if (main) { - console.log('Generating main bundle'); - const toBundle = (path: string) => JSON.parse(fs.readFileSync(path).toString("utf8")) as Record - const mainBundle = toBundle(main); - const keys = Object.keys(mainBundle); - const l10nBundle = Object.values(mainBundle).sort((v1, v2) => v1.localeCompare(v2)).reduce((p, c) => { p[c] = c; return p }, {} as Record); - fs.writeFileSync('dist/l10n/bundle.l10n.json', JSON.stringify(l10nBundle, null, 2)); - - others.forEach(bundle => { - const id = basename(bundle, ".json"); - console.log(`Generating ${id} bundle`); - const content = toBundle(bundle); - const l10n = Object.entries(content) - .sort(([k1], [k2]) => mainBundle[k1].localeCompare(mainBundle[k2])) - .reduce((p, [key, value]) => { p[mainBundle[key]] = value; return p }, {} as Record); - fs.writeFileSync(`dist/l10n/bundle.l10n.${id}.json`, JSON.stringify(l10n, null, 2), { encoding: "utf8" }); - }); - console.log(`Done generating bundles\n`); - - console.log(`Gathering files`); - const files: string[] = []; - const listFiles = (path: string) => { - for (const file of fs.readdirSync(path)) { - const fullPath = `${path}/${file}`; - if (fs.statSync(fullPath).isDirectory()) { - listFiles(fullPath); - } - else if (!fullPath.startsWith('src/tools')) { - files.push(fullPath); - } - } - } - listFiles('src'); - - console.log(`Found ${files.length} files\n`); - - for (const file of files) { - let changed = false; - let importVscode = false; - let prefix = "vscode."; - const lines = fs.readFileSync(file).toString('utf8').split('\n') - .filter(line => !/import .* from .*locale/.test(line)) - .map((line, i) => { - if (!importVscode && /from ['"`]vscode['"`]/.test(line)) { - importVscode = true; - if(/\{(.*)\}/.test(line)){ - prefix = ''; - return line.replace(/\{ (.*) \}/, "{ l10n, $1 }") - } - } - const res = /[^\w\.]t\(([^)]+)\)/.exec(line); - if (res && res[1]) { - changed = true; - const parts = res[1].replaceAll(/['"`]/g, '').split(','); - const oldKey = parts.splice(0, 1)[0]; - const key = mainBundle[oldKey]; - if (!key) { - console.log(`KEY ${oldKey} NOT FOUND ${file} line ${i + 1}`) - } - return line.replaceAll(/([^\w\.])t\(([^)]+)\)/g, `$1${prefix}l10n.t(\`${key}\`${parts.length ? ',' + parts.join(', ') : ''})`); - } - else { - return line; - } - }); - - if (changed) { - fs.writeFileSync(file, `${importVscode ? '' : `import vscode from "vscode";\n`}${lines.join("\n")}`, { encoding: "utf8" }); - } - } - } -} - -translate(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 867b94387..f00296578 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,6 @@ // "noUnusedParameters": true, /* Report errors on unused parameters. */ }, "include": [ - "src", + "src" ] } \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index e9d830bd9..faf912dab 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -27,7 +27,7 @@ const dist = path.resolve(__dirname, `dist`); fs.mkdirSync(dist, {recursive: true}); -const files = [{relative: `src/api/components/cqsh/cqsh`, name: `cqsh_1`}]; +const files = []; for (const file of files) { const src = path.resolve(__dirname, file.relative);