diff --git a/package.json b/package.json index 230d88f81..1b2b10d3f 100644 --- a/package.json +++ b/package.json @@ -375,9 +375,14 @@ "default": "8005", "description": "Port to connect to IBM i Debug Service." }, + "debugSepPort": { + "type": "string", + "default": "8008", + "description": "Port to connect to IBM i Debug Service for SEP." + }, "debugIsSecure": { "type": "boolean", - "default": false, + "default": true, "description": "Used to determine if the client should connect securely or not." }, "debugUpdateProductionFiles": { @@ -956,16 +961,17 @@ "enablement": "code-for-ibmi:connected && code-for-ibmi:debugManaged != true" }, { - "command": "code-for-ibmi.debug.activeEditor", - "title": "Start Debugging Active Source", + "command": "code-for-ibmi.debug.batch", + "title": "Debug as Batch", "category": "IBM i", "icon": "$(debug-alt)", "enablement": "code-for-ibmi:connected" }, { - "command": "code-for-ibmi.debug.program", - "title": "Debug Program", + "command": "code-for-ibmi.debug.sep", + "title": "Set Service Entry Point", "category": "IBM i", + "icon": "$(debug-alt)", "enablement": "code-for-ibmi:connected" }, { @@ -1749,6 +1755,11 @@ { "id": "code-for-ibmi.openMember", "label": "Open" + }, + { + "id": "code-for-ibmi.debug.group", + "label": "Start Debugging", + "icon": "$(debug-start)" } ], "menus": { @@ -1785,6 +1796,14 @@ "when": "viewItem == member" } ], + "code-for-ibmi.debug.group": [ + { + "command": "code-for-ibmi.debug.batch" + }, + { + "command": "code-for-ibmi.debug.sep" + } + ], "commandPalette": [ { "command": "code-for-ibmi.userLibraryList.enable", @@ -2047,7 +2066,11 @@ "when": "never" }, { - "command": "code-for-ibmi.debug.program", + "command": "code-for-ibmi.debug.batch", + "when": "never" + }, + { + "command": "code-for-ibmi.debug.sep", "when": "never" }, { @@ -2236,7 +2259,7 @@ ], "editor/title": [ { - "command": "code-for-ibmi.debug.activeEditor", + "submenu": "code-for-ibmi.debug.group", "when": "code-for-ibmi:connected && !inDebugMode && editorLangId =~ /^rpgle$|^rpg$|^cobol$|^cl$/i", "group": "navigation@1" }, @@ -2438,8 +2461,8 @@ "group": "1_workspace@1" }, { - "command": "code-for-ibmi.debug.program", - "when": "view == objectBrowser && !inDebugMode && viewItem =~ /^object.pgm.*/", + "submenu": "code-for-ibmi.debug.group", + "when": "view == objectBrowser && !inDebugMode && (viewItem =~ /^object.pgm.*/ || viewItem =~ /^object.srvpgm.*/)", "group": "2_debug@1" }, { diff --git a/src/api/Configuration.ts b/src/api/Configuration.ts index 3191bb45f..405f1e74e 100644 --- a/src/api/Configuration.ts +++ b/src/api/Configuration.ts @@ -55,6 +55,7 @@ export namespace ConnectionConfiguration { showDescInLibList: boolean; debugCertDirectory: string; debugPort: string; + debugSepPort: string; debugIsSecure: boolean; debugUpdateProductionFiles: boolean; debugEnableDebugTracing: boolean; @@ -137,6 +138,7 @@ export namespace ConnectionConfiguration { showDescInLibList: (parameters.showDescInLibList === true), debugCertDirectory: (parameters.debugCertDirectory || DEFAULT_CERT_DIRECTORY), debugPort: (parameters.debugPort || "8005"), + debugSepPort: (parameters.debugSepPort || "8008"), debugIsSecure: (parameters.debugIsSecure === true), debugUpdateProductionFiles: (parameters.debugUpdateProductionFiles === true), debugEnableDebugTracing: (parameters.debugEnableDebugTracing === true), diff --git a/src/api/debug/certificates.ts b/src/api/debug/certificates.ts index c78e5d450..198ae2391 100644 --- a/src/api/debug/certificates.ts +++ b/src/api/debug/certificates.ts @@ -65,6 +65,10 @@ function getLegacyCertificatePath() { return path.posix.join(LEGACY_CERT_DIRECTORY, SERVER_CERTIFICATE); } +function getPasswordForHost(connection: IBMi) { + return connection.currentHost; +} + export function getRemoteServerCertificatePath(connection: IBMi) { return path.posix.join(getRemoteCertificateDirectory(connection), SERVER_CERTIFICATE); } @@ -85,8 +89,8 @@ export async function remoteServerCertificateExists(connection: IBMi, legacy = f * Generate all certifcates on the server */ export async function setup(connection: IBMi) { - const host = connection.currentHost; - const extFileContent = await getExtFileContent(host, connection); + const pw = getPasswordForHost(connection); + const extFileContent = await getExtFileContent(pw, connection); if (!connection.usingBash()) { if (connection.remoteFeatures[`bash`]) { @@ -98,9 +102,9 @@ export async function setup(connection: IBMi) { const commands = [ `openssl genrsa -out debug_service.key 2048`, - `openssl req -new -key debug_service.key -out debug_service.csr -subj '/CN=${host}'`, + `openssl req -new -key debug_service.key -out debug_service.csr -subj '/CN=${pw}'`, `openssl x509 -req -in debug_service.csr -signkey debug_service.key -out debug_service.crt -days 1095 -sha256 -req -extfile <(printf "${extFileContent}")`, - `openssl pkcs12 -export -out debug_service.pfx -inkey debug_service.key -in debug_service.crt -password pass:${host}`, + `openssl pkcs12 -export -out debug_service.pfx -inkey debug_service.key -in debug_service.crt -password pass:${pw}`, `rm debug_service.key debug_service.csr debug_service.crt`, `chmod 444 debug_service.pfx` ]; @@ -127,9 +131,10 @@ export async function setup(connection: IBMi) { export async function downloadClientCert(connection: IBMi) { const localPath = getLocalCertPath(connection); + const keyPass = getPasswordForHost(connection); const result = await connection.sendCommand({ - command: `openssl s_client -connect localhost:${connection.config?.debugPort} -showcerts < /dev/null 2> /dev/null | openssl x509 -outform PEM`, + command: `openssl pkcs12 -in ${getRemoteServerCertificatePath(connection)} -passin pass:${keyPass} -info -nokeys -clcerts 2>/dev/null | openssl x509 -outform PEM`, directory: getRemoteCertificateDirectory(connection) }); diff --git a/src/api/debug/index.ts b/src/api/debug/index.ts index 250129bff..a90774cd9 100644 --- a/src/api/debug/index.ts +++ b/src/api/debug/index.ts @@ -7,9 +7,11 @@ import * as vscode from 'vscode'; import { copyFileSync } from "fs"; import { instance } from "../../instantiate"; import { ILELibrarySettings } from "../CompileTools"; +import { ObjectItem } from "../../typings"; import { getEnvConfig } from "../local/env"; import * as certificates from "./certificates"; import * as server from "./server"; +import { debug } from "console"; const debugExtensionId = `IBM.ibmidebug`; @@ -25,13 +27,21 @@ export function isManaged() { return process.env[`DEBUG_MANAGED`] === `true`; } -export async function initialize(context: ExtensionContext) { - const debugExtensionAvailable = () => { - const debugclient = vscode.extensions.getExtension(debugExtensionId); - return debugclient !== undefined; +const activateDebugExtension = async () => { + const debugclient = vscode.extensions.getExtension(debugExtensionId); + if (debugclient && !debugclient.isActive) { + await debugclient.activate(); } +} - const startDebugging = async (objectLibrary: string, objectName: string, workspaceFolder?: vscode.WorkspaceFolder) => { +const debugExtensionAvailable = () => { + const debugclient = vscode.extensions.getExtension(debugExtensionId); + return debugclient && debugclient.isActive; +} + +export async function initialize(context: ExtensionContext) { + + const startDebugging = async (type: DebugType, objectType: DebugObjectType, objectLibrary: string, objectName: string, workspaceFolder?: vscode.WorkspaceFolder) => { if (debugExtensionAvailable()) { const connection = instance.getConnection(); const config = instance.getConfig(); @@ -75,13 +85,19 @@ export async function initialize(context: ExtensionContext) { } if (password) { - const debugOpts: DebugOptions = { + let debugOpts: DebugOptions = { password, library: objectLibrary, object: objectName, libraries }; + if (type === `sep`) { + debugOpts.sep = { + type: objectType + } + } + startDebug(instance, debugOpts); } } else { @@ -109,7 +125,24 @@ export async function initialize(context: ExtensionContext) { } } - const getObjectFromUri = async (uri: Uri) => { + let cachedResolvedTypes: { [path: string]: DebugObjectType } = {}; + const getObjectType = async(library: string, objectName: string) => { + const path = library + `/` + objectName; + if (cachedResolvedTypes[path]) { + return cachedResolvedTypes[path]; + } else { + const content = instance.getContent()!; + + const [row] = await content.runSQL(`select OBJTYPE from table(qsys2.object_statistics('${library}', '*PGM *SRVPGM', '${objectName}')) X`) as { OBJTYPE: DebugObjectType }[]; + + if (row) { + cachedResolvedTypes[path] = row.OBJTYPE; + return row.OBJTYPE; + }; + } + } + + const getObjectFromUri = (uri: Uri) => { const connection = instance.getConnection(); const configuration = instance.getConfig(); @@ -203,24 +236,35 @@ export async function initialize(context: ExtensionContext) { } }), - vscode.commands.registerCommand(`code-for-ibmi.debug.activeEditor`, async () => { - const activeEditor = vscode.window.activeTextEditor; - if (activeEditor) { - // Get the workspace folder if one is available. - const workspaceFolder = [`member`, `streamfile`].includes(activeEditor.document.uri.scheme) ? undefined : vscode.workspace.getWorkspaceFolder(activeEditor.document.uri); - - const qualifiedObject = await getObjectFromUri(activeEditor.document.uri); + vscode.commands.registerCommand(`code-for-ibmi.debug.batch`, (node?: ObjectItem|Uri) => { + vscode.commands.executeCommand(`code-for-ibmi.debug`, `batch`, node); + }), - if (qualifiedObject.library && qualifiedObject.object) { - startDebugging(qualifiedObject.library, qualifiedObject.object, workspaceFolder); - } - } + vscode.commands.registerCommand(`code-for-ibmi.debug.sep`, (node?: ObjectItem|Uri) => { + vscode.commands.executeCommand(`code-for-ibmi.debug`, `sep`, node); }), - vscode.commands.registerCommand(`code-for-ibmi.debug.program`, async (node) => { - const [library, object] = node.path.split(`/`); - if (library && object) { - startDebugging(library, object); + vscode.commands.registerCommand(`code-for-ibmi.debug`, async (debugType?: DebugType, node?: ObjectItem|Uri) => { + if (debugType && node) { + if (node instanceof Uri) { + const workspaceFolder = [`member`, `streamfile`].includes(node.scheme) ? undefined : vscode.workspace.getWorkspaceFolder(node); + + const qualifiedObject = getObjectFromUri(node); + + if (qualifiedObject.library && qualifiedObject.object) { + const objectType = await getObjectType(qualifiedObject.library, qualifiedObject.object); + if (objectType) { + startDebugging(debugType, objectType, qualifiedObject.library, qualifiedObject.object, workspaceFolder); + } else { + vscode.window.showErrorMessage(`Failed to determine object type. Ensure the object exists and is a program (*PGM) or service program (*SRVPGM).`); + } + } + } else + if ('object' in node) { + const { library, name, type } = node.object + + startDebugging(debugType, type as DebugObjectType, library, name); + } } }), @@ -283,22 +327,13 @@ export async function initialize(context: ExtensionContext) { if (connection.config!.debugIsSecure) { try { - const existingDebugService = await server.getRunningJob(connection.config?.debugPort || "8005", instance.getContent()!); const remoteCertExists = await certificates.remoteServerCertificateExists(connection); // If the client certificate exists on the server, download it if (remoteCertExists) { - if (existingDebugService) { - await certificates.downloadClientCert(connection); - localCertsOk = true; - vscode.window.showInformationMessage(`Debug client certificate downloaded from the server.`); - } else { - vscode.window.showInformationMessage(`Cannot fetch client certificate because the Debug Service is not running.`, `Startup Service`).then(result => { - if (result) { - vscode.commands.executeCommand(`code-for-ibmi.debug.start`); - } - }); - } + await certificates.downloadClientCert(connection); + localCertsOk = true; + vscode.window.showInformationMessage(`Debug client certificate downloaded from the server.`); } else { const doImport = await vscode.window.showInformationMessage(`Debug setup`, { modal: true, @@ -364,7 +399,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); + await server.end(instance); startupService = true; } catch (e: any) { vscode.window.showErrorMessage(`Failed to end existing debug service (${e.message})`); @@ -377,7 +412,7 @@ export async function initialize(context: ExtensionContext) { if (startupService) { progress.report({ increment: 34, message: `Starting service up.` }); try { - await server.startup(connection); + await server.startup(instance); } catch (e: any) { vscode.window.showErrorMessage(`Failed to start debug service (${e.message})`); } @@ -402,7 +437,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.stop(instance); }); } } @@ -411,6 +446,8 @@ export async function initialize(context: ExtensionContext) { // Run during startup: instance.onEvent("connected", async () => { + activateDebugExtension(); + server.resetDebugServiceDetails() const connection = instance.getConnection(); const content = instance.getContent(); if (connection && content && debugPTFInstalled()) { @@ -469,14 +506,24 @@ interface DebugOptions { password: string; library: string; object: string; - libraries: ILELibrarySettings + libraries: ILELibrarySettings; + sep?: { + type: DebugObjectType; + moduleName?: string; + procedureName?: string; + } }; +type DebugType = "batch" | "sep"; +type DebugObjectType = "*PGM" | "*SRVPGM"; + export async function startDebug(instance: Instance, options: DebugOptions) { const connection = instance.getConnection(); const config = instance.getConfig(); const storage = instance.getStorage(); + const serviceDetails = await server.getDebugServiceDetails(instance.getContent()!); + const port = config?.debugPort; const updateProductionFiles = config?.debugUpdateProductionFiles; const enableDebugTracing = config?.debugEnableDebugTracing; @@ -496,47 +543,69 @@ export async function startDebug(instance: Instance, options: DebugOptions) { } } - const pathKey = options.library.trim() + `/` + options.object.trim(); - - const previousCommands = storage!.getDebugCommands(); + if (options.sep) { + if (serviceDetails.version === `1.0.0`) { + vscode.window.showErrorMessage(`The debug service on this system, version ${serviceDetails.version}, does not support service entry points.`); + return; + } - let currentCommand: string | undefined = previousCommands[pathKey] || `CALL PGM(` + pathKey + `)`; + // libraryName/programName programType/moduleName/procedureName + const formattedDebugString = `${options.library.toUpperCase()}/${options.object.toUpperCase()} ${options.sep.type}/${options.sep.moduleName || `*ALL`}/${options.sep.procedureName || `*ALL`}`; + vscode.commands.executeCommand( + `ibmidebug.create-service-entry-point-with-prompt`, + connection?.currentHost!, + connection?.currentUser!.toUpperCase(), + options.password, + formattedDebugString, + Number(config?.debugPort), + Number(config?.debugSepPort) + ); - currentCommand = await vscode.window.showInputBox({ - ignoreFocusOut: true, - title: `Debug command`, - prompt: `Command used to start debugging the ${pathKey} program object. The command is wrapped around SBMJOB.`, - value: currentCommand - }); + } else { - if (currentCommand) { - previousCommands[pathKey] = currentCommand; - storage?.setDebugCommands(previousCommands); - - const debugConfig = { - "type": `IBMiDebug`, - "request": `launch`, - "name": `Remote debug: Launch a batch debug session`, - "user": connection!.currentUser.toUpperCase(), - "password": options.password, - "host": connection!.currentHost, - "port": port, - "secure": secure, // Enforce secure mode - "ignoreCertificateErrors": true, - "library": connection!.upperCaseName(options.library), - "program": connection!.upperCaseName(options.object), - "startBatchJobCommand": `SBMJOB CMD(${currentCommand}) INLLIBL(${options.libraries.libraryList.join(` `)}) CURLIB(${options.libraries.currentLibrary}) JOBQ(QSYSNOMAX) MSGQ(*USRPRF)`, - "updateProductionFiles": updateProductionFiles, - "trace": enableDebugTracing, - }; - - const debugResult = await vscode.debug.startDebugging(undefined, debugConfig, undefined); - - if (debugResult) { - connectionConfirmed = true; - } else { - if (!connectionConfirmed) { - temporaryPassword = undefined; + const pathKey = options.library.trim() + `/` + options.object.trim(); + + const previousCommands = storage!.getDebugCommands(); + + let currentCommand: string | undefined = previousCommands[pathKey] || `CALL PGM(` + pathKey + `)`; + + currentCommand = await vscode.window.showInputBox({ + ignoreFocusOut: true, + title: `Debug command`, + prompt: `Command used to start debugging the ${pathKey} program object. The command is wrapped around SBMJOB.`, + value: currentCommand + }); + + if (currentCommand) { + previousCommands[pathKey] = currentCommand; + storage?.setDebugCommands(previousCommands); + + const debugConfig = { + "type": `IBMiDebug`, + "request": `launch`, + "name": `Remote debug: Launch a batch debug session`, + "user": connection!.currentUser.toUpperCase(), + "password": options.password, + "host": connection!.currentHost, + "port": port, + "secure": secure, // Enforce secure mode + "ignoreCertificateErrors": !secure, + "subType": "batch", + "library": options.library.toUpperCase(), + "program": options.object.toUpperCase(), + "startBatchJobCommand": `SBMJOB CMD(${currentCommand}) INLLIBL(${options.libraries.libraryList.join(` `)}) CURLIB(${options.libraries.currentLibrary}) JOBQ(QSYSNOMAX) MSGQ(*USRPRF)`, + "updateProductionFiles": updateProductionFiles, + "trace": enableDebugTracing, + }; + + const debugResult = await vscode.debug.startDebugging(undefined, debugConfig, undefined); + + if (debugResult) { + connectionConfirmed = true; + } else { + if (!connectionConfirmed) { + temporaryPassword = undefined; + } } } } diff --git a/src/api/debug/server.ts b/src/api/debug/server.ts index 2db07e902..c088a6550 100644 --- a/src/api/debug/server.ts +++ b/src/api/debug/server.ts @@ -4,15 +4,68 @@ import { window } from "vscode"; import IBMi from "../IBMi"; import IBMiContent from "../IBMiContent"; import * as certificates from "./certificates"; +import Instance from "../Instance"; -const serverDirectory = `/QIBM/ProdData/IBMiDebugService/bin/`; -const MY_JAVA_HOME = `MY_JAVA_HOME="/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit"`; +const directory = `/QIBM/ProdData/IBMiDebugService/`; +const binDirectory = path.posix.join(directory, `bin`); +const detailFile = `package.json`; + +const JavaPaths: {[version: string]: string} = { + "8": `/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit`, + "11": `/QOpenSys/QIBM/ProdData/JavaVM/jdk11/64bit` +} + +interface DebugServiceDetails { + version: string; + java: string; +} + +function getMyJavaHome(javaVersion: string) { + if (JavaPaths[javaVersion]) { + return `MY_JAVA_HOME="${JavaPaths[javaVersion]}"`; + } +} + +let debugServiceDetails: DebugServiceDetails | undefined; +export function resetDebugServiceDetails() { + debugServiceDetails = undefined; +} + +export async function getDebugServiceDetails(content: IBMiContent): Promise { + if (debugServiceDetails) { + return debugServiceDetails; + } + + debugServiceDetails = { + version: `1.0.0`, + java: `8` + }; + + const detailExists = await content.testStreamFile(path.posix.join(directory, detailFile), "r"); + if (detailExists) { + const fileContents = await content.downloadStreamfile(path.posix.join(directory, detailFile)); + try { + debugServiceDetails = JSON.parse(fileContents); + } catch (e) { + // Something very very bad has happened + console.log(e); + } + } + + return debugServiceDetails!; +} + +export async function startup(instance: Instance){ + const connection = instance.getConnection()!; + const content = instance.getContent()!; + const config = instance.getConfig()!; -export async function startup(connection: IBMi) { const host = connection.currentHost; + const details = await getDebugServiceDetails(content); + const javaHome = getMyJavaHome(details.java); 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: `${javaHome} MY_DBGSRV_SECURED_PORT="${config.debugPort}" MY_DBGSRV_SEP_DAEMON_PORT=${config.debugSepPort} DEBUG_SERVICE_KEYSTORE_PASSWORD="${host}" ${path.posix.join(binDirectory, `encryptKeystorePassword.sh`)} | /usr/bin/tail -n 1` }); if ((encryptResult.code || 0) >= 1) { @@ -28,7 +81,7 @@ export async function startup(connection: IBMi) { const keystorePath = certificates.getRemoteServerCertificatePath(connection); 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`)}"` + command: `${javaHome} DEBUG_SERVICE_KEYSTORE_PASSWORD="${password}" DEBUG_SERVICE_KEYSTORE_FILE="${keystorePath}" /QOpenSys/usr/bin/nohup "${path.posix.join(binDirectory, `startDebugService.sh`)}"` }).then(startResult => { if ((startResult.code || 0) >= 1) { window.showErrorMessage(startResult.stdout || startResult.stderr); @@ -38,9 +91,16 @@ export async function startup(connection: IBMi) { return; } -export async function stop(connection: IBMi) { +export async function stop(instance: Instance) { + const connection = instance.getConnection()!; + const content = instance.getContent()!; + const config = instance.getConfig()!; + + const details = await getDebugServiceDetails(content); + const javaHome = getMyJavaHome(details.java); + const endResult = await connection.sendCommand({ - command: `${path.posix.join(serverDirectory, `stopDebugService.sh`)}` + command: `${path.posix.join(binDirectory, `stopDebugService.sh`)}` }); if (endResult.code === 0) { @@ -56,9 +116,15 @@ export async function getRunningJob(localPort: string, content: IBMiContent): Pr return (rows.length > 0 ? String(rows[0].JOB_NAME) : undefined); } -export async function end(connection: IBMi): Promise { +export async function end(instance: Instance): Promise { + const connection = instance.getConnection()!; + const content = instance.getContent()!; + + const details = await getDebugServiceDetails(content); + const javaHome = getMyJavaHome(details.java); + const endResult = await connection.sendCommand({ - command: `${MY_JAVA_HOME} ${path.posix.join(serverDirectory, `stopDebugService.sh`)}` + command: `${javaHome} ${path.posix.join(binDirectory, `stopDebugService.sh`)}` }); if (endResult.code && endResult.code >= 0) { diff --git a/src/webviews/settings/index.ts b/src/webviews/settings/index.ts index d192ec732..84adda223 100644 --- a/src/webviews/settings/index.ts +++ b/src/webviews/settings/index.ts @@ -176,6 +176,7 @@ export class SettingsUI { if (connection && connection.remoteFeatures[`startDebugService.sh`]) { debuggerTab .addInput(`debugPort`, `Debug port`, `Default secure port is 8005. Tells the client which port the debug service is running on.`, { default: config.debugPort, minlength: 1, maxlength: 5, regexTest: `^\\d+$` }) + .addInput(`debugSepPort`, `SEP debug port`, `Default secure port is 8008. Tells the client which port the debug service for SEP is running on.`, { default: config.debugSepPort, minlength: 1, maxlength: 5, regexTest: `^\\d+$` }) .addCheckbox(`debugUpdateProductionFiles`, `Update production files`, `Determines whether the job being debugged can update objects in production (*PROD) libraries.`, config.debugUpdateProductionFiles) .addCheckbox(`debugEnableDebugTracing`, `Debug trace`, `Tells the debug service to send more data to the client. Only useful for debugging issues in the service. Not recommended for general debugging.`, config.debugEnableDebugTracing);