UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

187 lines (161 loc) 6.43 kB
module.exports = Object.assign(debug, { options: ['--port', '--host'], shortcuts: ['-p', '-h'], flags: [/*'--vscode', '--k8s'*/], help: ` # SYNOPSIS *cds debug* [<app>] Debug applications running locally or remotely on Cloud Foundry. Local applications will be started in debug mode, while remote applications are put into debug mode. If <app> is given, it's assumed to be running on the currently logged-in Cloud Foundry space (check with 'cf target'). SSH access to the app is required (check with 'cf ssh-enabled'). If no <app> is given, the app in the current working is started (with 'cds watch --debug' for Node.js and 'mvn spring-boot:run' for Java). # OPTIONS *-h* | *--host* the debug host (default: '127.0.0.1') *-p* | *--port* the debug port (default: '9229' for Node.js, '8000' for Java) # EXAMPLES *cds debug* *cds debug* bookshop-srv --port 8001 `}) async function debug() { const { fork, spawn, execSync, exec } = require('node:child_process') const os = require('node:os').platform() const cds = require('../lib/cds') const DEBUG = /\b(y|all|debug)\b/.test(process.env.DEBUG) ? console.debug : undefined const [appName] = cds.cli.argv const { port, host = '127.0.0.1', vscode } = cds.cli.options const maxAttempts = 15 let attempts = 0 if (!appName) { // local app if (cds.env['project-nature'] === 'java') { const portLocal = port || 8000 const cmd = `mvn` const args = ['spring-boot:run', `-Dspring-boot.run.jvmArguments="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${portLocal}"`] console.log(`Starting '${cmd} ${args.join(' ')}'\n`) spawn(cmd, args, { stdio: 'inherit' }) } else { const script = process.argv[1] // `.../cds` or `.../cds-tsx` console.log(`Starting '${cds.utils.path.basename(script)} watch --debug'`) fork(script, ['watch', '--debug']).on('error', e => { throw 'Failed to start debug process:' + e.message }) } } else if (cds.cli.options.k8s) { // Kyma/Kubernetes // to be implemented } else { // Cloud Foundry const [pid, kind] = processInfo(appName) console.log(`Found process of type ${kind}, ID: ${pid}`) if (kind === 'node') { enableNodeDebugging(pid, appName) const portLocal = openTunnel(9229) openDevTools(portLocal) } else if (kind === 'java') { const portRemote = findJavaDebugPort(pid, appName) const portLocal = openTunnel(portRemote) printHints(portLocal) } } function enableNodeDebugging(pid, appName) { return _execSync(`cf ssh ${appName} -c "kill -usr1 ${pid}"`, { stdio: 'inherit' }) } function findJavaDebugPort(pid, appName) { assertSapMachine(appName) const cmdLine = _execSync(`cf ssh ${appName} -c "~/app/META-INF/.sap_java_buildpack/sap_machine_jre/bin/jcmd ${pid} VM.command_line"`) const address = cmdLine.match(/agentlib:jdwp=.*?address=.*?(\d+)/i) if (address) { console.log(`Found debug port ${address[1]}`) return address[1] } throw `Failed to find debug port. JVM command line: ${cmdLine}` } function assertSapMachine(appName) { try { _execSync(`cf ssh ${appName} -c "ls ~/app/META-INF/.sap_java_buildpack/sap_machine_jre/bin/jcmd"`) } catch (err) { const message = err.message || err if (message.match(/cannot access.*jcmd/i)) { throw `Debugging is only supported for apps running with SapMachine.` + `\n\nTo set it, add the following environment variable to your _mta.yaml_ or _manifest.yml_:\n\n` + ` JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jre.SapMachine']"` + `\n\nSee https://help.sap.com/docs/btp/sap-business-technology-platform/sapmachine for more.\n` } } } function openTunnel(portRemote) { const portLocal = port || portRemote console.log(`\nOpening SSH tunnel on ${portRemote}:${host}:${portLocal}`) spawn('cf', ['ssh', '-N', '-L', `${portRemote}:${host}:${portLocal}`, appName], { stdio: 'inherit' }) return portLocal } function processInfo(appName) { const pidOf = exe => _execSyncSafe(`cf ssh ${appName} -c "pidof ${exe}"`) const exeTypes = ['node', 'java'] for (const exe of exeTypes) { const pid = pidOf(exe) if (pid) return [pid, exe] } throw `No ${exeTypes.join(' or ')} process found for app '${appName}'` } async function openDevTools(port) { attempts++ const ide = vscode ? 'vscode' : 'chrome' try { switch (ide) { case 'chrome': { const res = await fetch(`http://localhost:${port}/json/list`) const data = await res.json() if (!data?.length) throw 'No debugger found on port ' + port const wsUrl = data[0].webSocketDebuggerUrl let command const url = `devtools://devtools/bundled/inspector.html?ws=${wsUrl.split('ws://')[1]}` console.log(`Opening Chrome DevTools at ${url}`) if (os === 'darwin') { command = `open -a "Google Chrome" '${url}'` } else if (os === 'win32') { command = `start chrome "${url}"` } else { command = `google-chrome '${url}' || chromium-browser '${url}' || chromium '${url}'` } exec(command, (error) => { if (error) throw 'Failed to open DevTools: ' + error.message }) break } case 'vscode': { // to be implemented } } printHints(port) } catch (err) { if (attempts < maxAttempts) { setTimeout(() => openDevTools(port), 1000).unref() // connection fails sometimes, retry } else { throw `Failed to connect to debugger after ${maxAttempts} attempts: ${err}` } } } function printHints(portLocal) { console.log() console.log(`> Now attach a debugger to port ${portLocal}.`) console.log('> Keep this terminal open while debugging.') console.log('> See https://cap.cloud.sap/docs/tools/cds-cli#cds-debug for more.') } function _execSyncSafe(...args) { try { return _execSync(...args) } catch (_) {/* ignore */ } } function _execSync(cmd, options={ encoding: 'utf-8', shell: true }) { try { return execSync(cmd, options)?.trim() } catch (err) { if (DEBUG) throw err throw err.message } } }