UNPKG

@sap/cds-dk

Version:

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

274 lines (228 loc) 10.5 kB
module.exports = Object.assign(debug, { options: ['--port', '--host', '--no-devtools'], shortcuts: ['-p', '-h', undefined, '-f'], flags: ['--force', '--k8s', '--cf'], help: ` # SYNOPSIS *cds debug* [<app>] Debug applications running locally or remotely on Cloud Foundry or Kubernetes. If <app> is given, it's assumed to be running on a remote system. The command defaults to Kubernetes if Helm deployment descriptors are present, otherwise Cloud Foundry. For Cloud Foundry, the currently targeted space is used (check with 'cf target'). For Kubernetes, the current context and namespace are used (check with 'kubectl config get-contexts'). SSH access to the app is required (check with 'cf ssh-enabled' on Cloud Foundry). If no <app> is given, the app in the current working directory is started (with 'cds watch --debug' for Node.js and 'mvn spring-boot:run' for Java). For Node.js, Chrome DevTools are opened automatically unless *--no-devtools* is set. # OPTIONS *-h* | *--host* The debug host (default: '127.0.0.1'). *-p* | *--port* The debug port (default: '9229' for Node.js, '8000' for Java). *--cf* Force Cloud Foundry mode. *--k8s* Force Kubernetes mode. Use env vars KUBE_NAMESPACE or NAMESPACE to select a namespace. *--no-devtools* Don't open developer tools automatically (Node.js only). *-f* | *--force* If necessary, automatically enable SSH for the app and restart it. # EXAMPLES *cds debug* *cds debug* bookshop-srv --port 8001 *cds debug* bookshop-srv --host 0.0.0.0 *cds debug* --k8s bookshop-srv `}) async function debug() { const { fork, spawn, execSync, exec } = require('node:child_process') const os = require('node:os').platform() const cds = require('../lib/cds') const { BOLD, RESET } = cds.utils.colors const { readProject } = require('../lib/init/projectReader') const DEBUG = /\b(y|all|debug)\b/.test(process.env.DEBUG) ? console.debug : undefined const [appName] = cds.cli.argv const { port = 9229, host = '127.0.0.1', vscode, force } = cds.cli.options const maxAttempts = 15 let attempts = 0 const { hasKyma, hasMta } = readProject() const onKyma = cds.cli.options.k8s || hasKyma && !hasMta && !cds.cli.options.cf 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` const hostport = `${host}:${port}` console.log(`Starting '${cds.utils.path.basename(script)} watch --debug ${(host != '127.0.0.1' || port != 9229) ? hostport : ''}'`) fork(script, ['watch', '--debug', hostport]).on('error', e => { throw 'Failed to start debug process:' + e.message }) } } else if (onKyma) { // Kyma/Kubernetes (Node.js only) if (!appName) throw `pass a deployment name, for example 'cds debug --k8s bookshop-srv'` const namespace = process.env.KUBE_NAMESPACE || process.env.NAMESPACE || '' // optional const ns = namespace ? `-n ${namespace}` : '' const portLocal = port || 9229 const k = (cmd, opts) => _execSync(`kubectl ${ns} ${cmd}`, opts) // Ensure deployment exists k(`get deploy ${appName}`, { stdio: 'inherit' }) if (force) { console.log(`\nEnabling Node inspector on deployment/${appName} at 0.0.0.0:${portLocal} ...`) _execSyncSafe(`kubectl ${ns} set env deployment/${appName} NODE_OPTIONS="--inspect=0.0.0.0:${portLocal}"`, { stdio: 'inherit' }) _execSyncSafe(`kubectl ${ns} rollout restart deployment/${appName}`, { stdio: 'inherit' }) k(`rollout status deployment/${appName} --timeout=180s`, { stdio: 'inherit' }) } else { // Bail out early if inspector isn't already enabled const nodeOpts = _execSyncSafe(`kubectl ${ns} get deploy ${appName} -o jsonpath="{.spec.template.spec.containers[0].env[?(@.name=='NODE_OPTIONS')].value}"`) if (!nodeOpts || !/--inspect/.test(nodeOpts)) { throw ( `Node inspector not enabled on deployment/${appName}.\n` + `Run ${BOLD}cds debug ${appName} --force${RESET} to set NODE_OPTIONS="--inspect=0.0.0.0:${portLocal}" and restart.` ) } } const podsJson = _execSync(`kubectl ${ns} get pods -o json`) const items = JSON.parse(podsJson).items || [] const candidates = items.filter(i => (i.metadata?.name || '').startsWith(`${appName}-`)) if (!candidates.length) throw `no Pod for deployment/${appName} found (namespace: ${namespace || '<current>'}).` candidates.sort((a, b) => new Date(a.metadata.creationTimestamp) - new Date(b.metadata.creationTimestamp)) const pod = `pod/${candidates[candidates.length - 1].metadata.name}` console.log(`\nusing ${pod}`) console.log(`\nopening port-forward: localhost:${portLocal} -> ${pod}:${portLocal}`) const args = [] if (namespace) args.push('-n', namespace) args.push('port-forward', pod, `${portLocal}:${portLocal}`) spawn('kubectl', args, { stdio: 'inherit' }) if (!cds.cli.options['no-devtools']) openDevTools(portLocal) printHints(portLocal) } else { // Cloud Foundry const [pid, kind] = processInfo(appName, force) console.log(`Found process of type ${kind}, ID: ${pid}`) if (kind === 'node') { enableNodeDebugging(pid, appName) const portLocal = openTunnel(9229) if (!cds.cli.options['no-devtools']) 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 ${host}:${portRemote}:${host}:${portLocal}`) spawn('cf', ['ssh', '-N', '-L', `${host}:${portRemote}:${host}:${portLocal}`, appName], { stdio: 'inherit' }) return portLocal } function processInfo(appName, force) { if (force !== false) { const enabled = _execSyncSafe(`cf ssh-enabled ${appName}`) if (!enabled) throw `Make sure the application is visible in your targeted Cloud Foundry space.` if (enabled.match(/disabled/i)) { if (force) { console.log(`Trying to enable SSH for app '${appName}'...`) _execSyncSafe(`cf enable-ssh ${appName}`) _execSyncSafe(`cf restart ${appName}`) return processInfo(appName, false) } throw ( `SSH support is disabled for app '${appName}'.` + `\nRun "cds debug ${appName} --force" to automatically enable SSH for your application.` ) } } 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 } } }