@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
187 lines (161 loc) • 6.43 kB
JavaScript
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
}
}
}