From c742db50f85e09fb6199c1ed95b7e8950f0f65a0 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 25 Mar 2024 23:13:22 +0100 Subject: [PATCH] Added more info to debugger panel + actions to start/stop server and service Signed-off-by: Seb Julliand --- src/api/debug/index.ts | 15 +-- src/api/debug/server.ts | 89 +++++++++---- src/locale/ids/en.ts | 24 +++- src/webviews/debugger/index.ts | 233 +++++++++++++++++++++++++-------- 4 files changed, 273 insertions(+), 88 deletions(-) diff --git a/src/api/debug/index.ts b/src/api/debug/index.ts index 4c88c532d..41ced9246 100644 --- a/src/api/debug/index.ts +++ b/src/api/debug/index.ts @@ -363,12 +363,7 @@ export async function initialize(context: ExtensionContext) { if (confirmEndServer === `End service`) { progress.report({ increment: 33, message: `Ending currently running service.` }); - try { - await server.end(connection); - startupService = true; - } catch (e: any) { - vscode.window.showErrorMessage(`Failed to end existing debug service (${e.message})`); - } + startupService = await server.stopService(connection); } } else { startupService = true; @@ -376,11 +371,7 @@ export async function initialize(context: ExtensionContext) { if (startupService) { progress.report({ increment: 34, message: `Starting service up.` }); - try { - await server.startup(connection); - } catch (e: any) { - vscode.window.showErrorMessage(`Failed to start debug service (${e.message})`); - } + await server.startService(connection); } else { vscode.window.showInformationMessage(`Cancelled startup of debug service.`); } @@ -402,7 +393,7 @@ export async function initialize(context: ExtensionContext) { if (ptfInstalled) { vscode.window.withProgress({ location: vscode.ProgressLocation.Notification }, async (progress) => { progress.report({ message: `Ending Debug Service` }); - await server.stop(connection); + await server.stopService(connection); }); } } diff --git a/src/api/debug/server.ts b/src/api/debug/server.ts index 04bc242a9..ba347fe5c 100644 --- a/src/api/debug/server.ts +++ b/src/api/debug/server.ts @@ -1,7 +1,8 @@ import path from "path"; -import { window } from "vscode"; +import { commands, window } from "vscode"; import { instance } from "../../instantiate"; +import { t } from "../../locale"; import IBMi from "../IBMi"; import IBMiContent from "../IBMiContent"; import { Tools } from "../Tools"; @@ -11,15 +12,19 @@ const serverDirectory = `/QIBM/ProdData/IBMiDebugService/bin/`; const MY_JAVA_HOME = `MY_JAVA_HOME="/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit"`; export type DebugJob = { - job: string + name: string port: number } -export async function startup(connection: IBMi) { +export async function startService(connection: IBMi) { const host = connection.currentHost; const encryptResult = await connection.sendCommand({ - command: `${MY_JAVA_HOME} DEBUG_SERVICE_KEYSTORE_PASSWORD="${host}" ${path.posix.join(serverDirectory, `encryptKeystorePassword.sh`)} | /usr/bin/tail -n 1` + command: `${path.posix.join(serverDirectory, `encryptKeystorePassword.sh`)} | /usr/bin/tail -n 1`, + env: { + DEBUG_SERVICE_KEYSTORE_PASSWORD: host, + MY_JAVA_HOME: "/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit" + } }); if ((encryptResult.code || 0) >= 1) { @@ -34,26 +39,44 @@ export async function startup(connection: IBMi) { const keystorePath = certificates.getRemoteServerCertificatePath(connection); + let didNotStart = false; connection.sendCommand({ command: `${MY_JAVA_HOME} DEBUG_SERVICE_KEYSTORE_PASSWORD="${password}" DEBUG_SERVICE_KEYSTORE_FILE="${keystorePath}" /QOpenSys/usr/bin/nohup "${path.posix.join(serverDirectory, `startDebugService.sh`)}"` }).then(startResult => { - if ((startResult.code || 0) >= 1) { - window.showErrorMessage(startResult.stdout || startResult.stderr); + if (startResult.code) { + window.showErrorMessage(t("start.debug.service.failed", startResult.stdout || startResult.stderr)); + didNotStart = true; } }); - return; + let tries = 0; + while (!didNotStart && tries < 20) { + if (await getDebugServiceJob()) { + window.showInformationMessage(t("start.debug.service.succeeded")); + commands.executeCommand("code-for-ibmi.updateConnectedBar"); + return true; + } + else { + await Tools.sleep(500); + tries++; + } + } + + return false; } -export async function stop(connection: IBMi) { +export async function stopService(connection: IBMi) { const endResult = await connection.sendCommand({ - command: `${path.posix.join(serverDirectory, `stopDebugService.sh`)}` + command: `${MY_JAVA_HOME} ${path.posix.join(serverDirectory, `stopDebugService.sh`)}` }); - if (endResult.code === 0) { - window.showInformationMessage(`Ended Debug Service.`); + if (!endResult.code) { + window.showInformationMessage(t("stop.debug.service.succeeded")); + commands.executeCommand("code-for-ibmi.updateConnectedBar"); + return true; } else { - window.showErrorMessage(endResult.stdout || endResult.stderr); + window.showErrorMessage(t("stop.debug.service.failed", endResult.stdout || endResult.stderr)); + return false; } } @@ -76,17 +99,7 @@ export async function getDebugServerJob() { } function rowToDebugJob(row?: Tools.DB2Row): DebugJob | undefined { - return row?.JOB_NAME ? { job: String(row.JOB_NAME), port: Number(row.LOCAL_PORT) } : undefined; -} - -export async function end(connection: IBMi): Promise { - const endResult = await connection.sendCommand({ - command: `${MY_JAVA_HOME} ${path.posix.join(serverDirectory, `stopDebugService.sh`)}` - }); - - if (endResult.code && endResult.code >= 0) { - throw new Error(endResult.stdout || endResult.stderr); - } + return row?.JOB_NAME ? { name: String(row.JOB_NAME), port: Number(row.LOCAL_PORT) } : undefined; } /** @@ -113,4 +126,34 @@ export function endJobs(jobIds: string[], connection: IBMi) { export async function isDebugEngineRunning() { return Boolean(await getDebugServerJob()) && Boolean(await getDebugServiceJob()); +} + +export async function startServer() { + const result = await instance.getConnection()?.runCommand({ command: "STRDBGSVR", noLibList: true }); + if (result) { + if (result.code) { + window.showErrorMessage(t("strdbgsvr.failed"), result.stderr); + return false; + } + else { + commands.executeCommand("code-for-ibmi.updateConnectedBar"); + window.showInformationMessage(t("strdbgsvr.succeeded")); + } + } + return true; +} + +export async function stopServer() { + const result = await instance.getConnection()?.runCommand({ command: "ENDDBGSVR", noLibList: true }); + if (result) { + if (result.code) { + window.showErrorMessage(t("enddbgsvr.failed"), result.stderr); + return false; + } + else { + commands.executeCommand("code-for-ibmi.updateConnectedBar"); + window.showInformationMessage(t("enddbgsvr.succeeded")); + } + } + return true; } \ No newline at end of file diff --git a/src/locale/ids/en.ts b/src/locale/ids/en.ts index 4ac8430b0..963fb124b 100644 --- a/src/locale/ids/en.ts +++ b/src/locale/ids/en.ts @@ -47,7 +47,10 @@ export const en: Locale = { 'online': 'Online', 'status': 'Status', 'job': 'Job', - 'listening.on.port': 'Listening on port', + 'start':'Start', + 'restart':'Restart', + 'stop':'Stop', + 'listening.on.port': 'Listening on port', 'overview': 'Overview', 'JOB_NAME_SHORT':'Job name', 'JOB_USER':'Job user', @@ -362,5 +365,22 @@ export const en: Locale = { 'debug.server': 'Debug Server', 'debug.service': 'Debug Service', 'active.job':'Active job', - 'jvm.info':'JVM information' + 'jvm.info':'JVM information', + 'debug.service.certificate':'Debug Service Certificate', + 'service.certificate.exists':'Remote service certificate exists', + 'local.certificate':'Local certificate', + 'certificates.match':'Local certificate matches remote', + 'generate.certificate':'Generate service certificate', + 'download.certificate':'Download certificate', + 'not.found.in':'Not found in {0}', + 'strdbgsvr.succeeded':'Debug server started.', + 'strdbgsvr.failed':'Failed to start debug server: {0}', + 'enddbgsvr.succeeded':'Debug server stopped.', + 'enddbgsvr.failed':'Failed to stop debug server: {0}', + 'start.debug.service.task':'Starting debug service...', + 'start.debug.service.succeeded':'Debug service started.', + 'start.debug.service.failed':'Failed to start debug service: {0}', + 'stop.debug.service.task':'Stopping debug service...', + 'stop.debug.service.succeeded':'Debug service stopped.', + 'stop.debug.service.failed':'Failed to stop debug service: {0}' }; \ No newline at end of file diff --git a/src/webviews/debugger/index.ts b/src/webviews/debugger/index.ts index 1a3a81062..dfb26a0be 100644 --- a/src/webviews/debugger/index.ts +++ b/src/webviews/debugger/index.ts @@ -1,92 +1,164 @@ +import { readFileSync } from "fs"; import vscode from "vscode"; -import { CustomUI, Field, Section } from "../../api/CustomUI"; +import { CustomUI, Field, Page, Section } from "../../api/CustomUI"; import IBMiContent from "../../api/IBMiContent"; import { Tools } from "../../api/Tools"; -import { getDebugServerJob, getDebugServiceJob } from "../../api/debug/server"; +import { isManaged } from "../../api/debug"; +import { getLocalCertPath, getRemoteCertificateDirectory, localClientCertExists, readRemoteCertificate, remoteServerCertificateExists } from "../../api/debug/certificates"; +import { DebugJob, getDebugServerJob, getDebugServiceJob, startServer, startService, stopServer, stopService } from "../../api/debug/server"; import { instance } from "../../instantiate"; import { t } from "../../locale"; +type DebuggerPage = { + buttons?: string +} + + export async function openDebugStatusPanel() { const content = instance.getContent(); - if (content) { + const config = instance.getConfig() + const connection = instance.getConnection(); + if (content && config && connection) { const debbuggerInfo = await vscode.window.withProgress({ title: t("loading.debugger.info"), location: vscode.ProgressLocation.Notification }, async () => { const serverJob = await getDebugServerJob(); - const activeServerJob = serverJob ? await readActiveJob(content, serverJob.job) : undefined; + const activeServerJob = serverJob ? await readActiveJob(content, serverJob) : undefined; const serviceJob = await getDebugServiceJob(); - const activeServiceJob = serviceJob ? await readActiveJob(content, serviceJob.job) : undefined; - const activeServiceJava = serviceJob ? await readJVMInfo(content, serviceJob.job) : undefined; + const activeServiceJob = serviceJob ? await readActiveJob(content, serviceJob) : undefined; + const activeServiceJava = serviceJob ? await readJVMInfo(content, serviceJob) : undefined; + const remoteCertificateExists = await remoteServerCertificateExists(connection); + + let localCertificate = undefined; + let remoteCertificate = undefined; + let localError = undefined; + let remoteError = undefined; + if (config.debugIsSecure) { + try { + remoteCertificate = await readRemoteCertificate(connection); + } + catch (error: any) { + remoteError = String(error); + } + + try { + if (await localClientCertExists(connection)) { + localCertificate = readFileSync(getLocalCertPath(connection)).toString("utf-8"); + } + } + catch (error: any) { + localError = String(error); + } + } + return { - serverJob, - activeServerJob, - serviceJob, - activeServiceJob, - activeServiceJava + server: serverJob ? { job: serverJob, activeJob: activeServerJob } : undefined, + service: serviceJob ? { job: serviceJob, activeJob: activeServiceJob, java: activeServiceJava } : undefined, + certificate: { remoteExists: remoteCertificateExists, local: localCertificate, localError, remote: remoteCertificate, remoteError } }; }); + const debugManaged = isManaged(); + + const summary = new Section() + //Debug Server summary + .addParagraph(/* html */` +

${t("debug.server")} ${debbuggerInfo.server ? "✅" : "❌"}

+
    +
  • ${t("status")}: ${debbuggerInfo.server ? t("online") : t("offline")}
  • + ${debbuggerInfo.server ? /* html */ ` +
  • ${t("job")}: ${debbuggerInfo.server.job.name}
  • +
  • ${t("listening.on.port")}: ${debbuggerInfo.server.job.port}
  • ` + : "" + } +
`); + addStartStopButtons("server", summary, debbuggerInfo.server !== undefined); + + //Debug Service summary + summary.addHorizontalRule() + .addParagraph(/* html */` +

${t("debug.service")} ${debbuggerInfo.service ? "✅" : "❌"}

+
    +
  • ${t("status")}: ${debbuggerInfo.service ? t("online") : t("offline")}
  • + ${debbuggerInfo.service ? /* html */ ` +
  • ${t("job")}: ${debbuggerInfo.service.job.name}
  • +
  • ${t("listening.on.port")}: ${debbuggerInfo.service.job.port}
  • + ` + : "" + } +
`); + addStartStopButtons("service", summary, debbuggerInfo.service !== undefined); + + //Certificates summary + const certificatesMatch = certificateMatchStatus(debbuggerInfo.certificate); + summary.addHorizontalRule() + .addParagraph(/* html */` +

${t("debug.service.certificate")} ${config.debugIsSecure ? certificatesMatch : (debbuggerInfo.certificate.remoteExists ? "✅" : "❌")}

+
    +
  • ${t("service.certificate.exists")}: ${debbuggerInfo.certificate.remoteExists ? "✅" : t("not.found.in", getRemoteCertificateDirectory(connection))}
  • + ${config.debugIsSecure ? /* html */` +
  • ${t("local.certificate")}: ${debbuggerInfo.certificate.localError ? debbuggerInfo.certificate.localError : "✅"}
  • +
  • ${t("certificates.match")}: ${debbuggerInfo.certificate.remoteError ? "❓" : certificatesMatch}
  • + ` + : "" + } +
`) + .addButtons( + !debbuggerInfo.certificate.remoteExists ? { id: `service.generateCertificate`, label: t("generate.certificate") } : undefined, + debbuggerInfo.certificate.remoteExists && config.debugIsSecure && !certificatesMatch ? { id: `service.downloadCertificate`, label: t("download.certificate") } : undefined + ); + const tabs = [{ - label: t("overview"), fields: new Section() - .addParagraph(/* html */` -

${t("debug.server")} ${debbuggerInfo.serverJob ? "✅" : "❌"}

-
    -
  • ${t("status")}: ${debbuggerInfo.serverJob ? t("online") : t("offline")}
  • - ${debbuggerInfo.serverJob ? /* html */ ` -
  • ${t("job")}: ${debbuggerInfo.serverJob.job}
  • -
  • ${t("listening.on.port")}: ${debbuggerInfo.serverJob.port}
  • ` - : - "" - } -
`) - .addParagraph(/* html */` -

${t("debug.service")} ${debbuggerInfo.serviceJob ? "✅" : "❌"}

-
    -
  • ${t("status")}: ${debbuggerInfo.serviceJob ? t("online") : t("offline")}
  • - ${debbuggerInfo.serviceJob ? /* html */ ` -
  • ${t("job")}: ${debbuggerInfo.serviceJob.job}
  • -
  • ${t("listening.on.port")}: ${debbuggerInfo.serviceJob.port}
  • ` - : - "" - } -
`).fields + label: t("overview"), fields: summary.fields }]; - if (debbuggerInfo.activeServerJob) { + //Debug server details + if (debbuggerInfo.server?.activeJob) { tabs.push({ label: t("debug.server"), - fields: getActiveJobFields(t("active.job"), debbuggerInfo.activeServerJob) + fields: getActiveJobFields(t("active.job"), debbuggerInfo.server?.activeJob) }); } - if (debbuggerInfo.activeServiceJob) { + //Debug service details + if (debbuggerInfo.service?.activeJob) { tabs.push({ label: t("debug.service"), fields: [ - ...getActiveJobFields(t("active.job"), debbuggerInfo.activeServiceJob), - ...(debbuggerInfo.activeServiceJava ? getActiveJobFields(t("jvm.info"), debbuggerInfo.activeServiceJava) : []) + ...getActiveJobFields(t("active.job"), debbuggerInfo.service?.activeJob), + ...(debbuggerInfo.service.java ? getActiveJobFields(t("jvm.info"), debbuggerInfo.service.java) : []) ] }); } - new CustomUI().addComplexTabs(tabs).loadPage(t('debugger.status')); + new CustomUI() + .addComplexTabs(tabs) + .loadPage(t('debugger.status'), handleAction); } } -async function readActiveJob(content: IBMiContent, job: string) { +function addStartStopButtons(target: "server" | "service", section: Section, running: boolean) { + section.addButtons( + running ? undefined : { id: `${target}.start`, label: t("start") }, + running ? { id: `${target}.restart`, label: t("restart") } : undefined, + running ? { id: `${target}.stop`, label: t("stop") } : undefined + ); +} + +async function readActiveJob(content: IBMiContent, job: DebugJob) { try { return (await content.runSQL( - `select job_name_short, job_user, job_number, subsystem_library_name || '/' || subsystem as subsystem, authorization_name, job_status, memory_pool from table(qsys2.active_job_info(job_name_filter => '${job.substring(job.lastIndexOf('/') + 1)}')) where job_name = '${job}' fetch first row only` + `select job_name_short, job_user, job_number, subsystem_library_name || '/' || subsystem as subsystem, authorization_name, job_status, memory_pool from table(qsys2.active_job_info(job_name_filter => '${job.name.substring(job.name.lastIndexOf('/') + 1)}')) where job_name = '${job.name}' fetch first row only` )).at(0); } catch (error) { return String(error); } } -async function readJVMInfo(content: IBMiContent, job: string) { +async function readJVMInfo(content: IBMiContent, job: DebugJob) { try { return (await content.runSQL(` select START_TIME, JAVA_HOME, USER_DIRECTORY, CURRENT_HEAP_SIZE, MAX_HEAP_SIZE from QSYS2.JVM_INFO - where job_name = '${job}' + where job_name = '${job.name}' fetch first row only`)).at(0); } catch (error) { return String(error); @@ -94,12 +166,71 @@ async function readJVMInfo(content: IBMiContent, job: string) { } function getActiveJobFields(label: string, jobRow: Tools.DB2Row | string): Field[] { - return new Section().addParagraph(`

${label}

-
    - ${Object.entries(jobRow).filter(([key, value]) => value !== null).map(([key, value]) => `
  • ${t(key)}: ${value}
  • `).join("")} -
`).fields; - ; + return new Section().addParagraph(/*html*/ `

${label}

+
    ${typeof jobRow === "string" ? + jobRow + : + Object.entries(jobRow).filter(([key, value]) => value !== null).map(([key, value]) => /*html*/ `
  • ${t(key)}: ${value}
  • `).join("") + }
` + ).fields; } -//JOB_NAME_SHORT, JOB_USER, JOB_NUMBER, SUBSYSTEM, SUBSYSTEM_LIBRARY_NAME, AUTHORIZATION_NAME, JOB_STATUS, MEMORY_POOL -//START_TIME, JAVA_HOME, USER_DIRECTORY, CURRENT_HEAP_SIZE, MAX_HEAP_SIZE \ No newline at end of file +function certificateMatchStatus(certificate: { local?: string, remote?: string }) { + return certificate.local && (certificate.local === certificate.remote) ? "✅" : "❌"; +} + +function handleAction(page: Page) { + const actionParts = page.data?.buttons?.split('.'); + const target = actionParts?.at(0); + const action = actionParts?.at(1); + console.log(target, action); + if (action) { + let result; + if (target === "server") { + result = handleServerAction(action); + } + else if (target === "service") { + result = handleServiceAction(action); + } + + result?.then(reload => { + if (reload) { + page.panel.dispose(); + openDebugStatusPanel(); + } + }); + } +} + +async function handleServerAction(action: string): Promise { + switch (action) { + case "start": + return startServer(); + case "restart": + return await stopServer() && startServer(); + case "stop": + return stopServer(); + } + + return false; +} + +async function handleServiceAction(action: string): Promise { + const connection = instance.getConnection(); + if (connection) { + const start = () => vscode.window.withProgress({ title: t('start.debug.service.task'), location: vscode.ProgressLocation.Notification }, () => startService(connection)); + const stop = () => vscode.window.withProgress({ title: t('stop.debug.service.task'), location: vscode.ProgressLocation.Notification }, () => stopService(connection)); + switch (action) { + case "start": + return start(); + case "restart": + return await stop() && start(); + case "stop": + return stop(); + case "generateCertificate": + case "downloadCertificate": + } + } + + return false; +} \ No newline at end of file