@saptools/cf-debugger
Opens a verified Node.jsNode.jsA cross-platform JavaScript runtime that servers, command line tools and scripts written in JavaScript are run with. For the things here, the prerequisite a repository names when its build is a set of npm scripts. inspector tunnel over cf ssh to an app on SAP BTP Cloud Foundry from one command, with its own local port for each concurrent session.
Attaching an IDE debugger to a Node.jsNode.jsA cross-platform JavaScript runtime that servers, command line tools and scripts written in JavaScript are run with. For the things here, the prerequisite a repository names when its build is a set of npm scripts. app on Cloud FoundrySAP BTP, Cloud Foundry RuntimeThe Cloud Foundry runtime of SAP BTP: memory for applications in a Cloud Foundry org, run from SAP's Java, Node.js and Python buildpacks or community buildpacks and managed with the standard Cloud Foundry tools.sap official, btp means finding the right process in the container, starting its inspector and forwarding a port by hand. cf-debugger start does those steps in one command: it picks the Node PID, sends SIGUSR1, spawns cf ssh -N -L and reports ready only once GET /json/list through the tunnel returns an attachable target. Each session gets a local port in the 20000–20999 range and an isolated CF_HOME, so several apps can be debugged at once, and doctor reports what an interrupted session left behind. It opens the tunnel and stops there: breakpoints are set in whatever attaches to the local port, and an app with SSH disabled is restarted only when --allow-ssh-enable-restart is passed.
Facts
Where it sits
Who is behind it
In the atlas: maintains 18, contributes to 18@dongitranmaintainer
What it declares
Every artifact
- Type
- CLI
- Origin
- Independently maintained
- Runs on
- Cloud Foundry
As a CLI
- Distributed through
- npm
Connections
Seeing what running code did between input and result
Shell debugger for remote Node.js over the Chrome DevTools Protocol
A cross-platform JavaScript runtime that servers, command line tools and scripts written in JavaScript are run with. For the things here, the prerequisite a repository names when its build is a set of npm scripts.
Cloud Foundry runtime of SAP BTP for polyglot applications
Download
Install
npm add @saptools/cf-debuggerDerived from the package name. No recipe recorded; the README has the configuration.
Architecture
Download
README
quoted from dongitran/saptools under MIT, read@saptools/cf-debugger
Open a verified Node.js inspector tunnel to an SAP BTP Cloud Foundry app from one
command. Each concurrent session has its own local port, isolated CF_HOME, and
lock-guarded state entry.
Install
npm install -g @saptools/cf-debugger
Node.js 20+ and the official cf CLI are required. CF CLI v8 is recommended.
Non-web process targeting uses cf ssh --process and requires CF CLI v7+.
On macOS, lsof is a soft dependency used to prove which PID owns a local
listening port. Install it if cf-debugger reports TUNNEL_OWNER_UNVERIFIED.
Without it, an explicit stop refuses to signal a state-recorded PID whose port
ownership cannot be proved; teardown still completes when the spawned child is
independently confirmed dead.
Updates
Every command first checks npm for a newer @saptools/cf-debugger (at most once an hour, one small request
with a 2-second timeout) and, when one exists, installs that exact version with the package manager
that owns the running binary and re-runs the command you typed on the new version. Both steps are
announced on stderr; nothing is printed when the install is already current:
cf-debugger: updating 0.3.0 -> 0.4.0 ...
cf-debugger: updated to 0.4.0; re-running the command
If the install cannot complete, one stderr line gives the manual command and the command runs on the
installed version; that version is not retried for a day. cf-debugger self-update forces the check and
install now; cf-debugger self-update --check only reports.
| Control | Effect |
|---|---|
SAPTOOLS_AUTO_UPDATE=on|notify|off | on (default) installs and re-runs; notify prints the manual command once per version; off never checks. Applies to every @saptools CLI. |
CF_DEBUGGER_AUTO_UPDATE | same values, this CLI only; wins over the global variable |
SAPTOOLS_UPDATE_INTERVAL_MINUTES | minutes between checks (default 60; 0 checks on every run) |
SAPTOOLS_NPM_REGISTRY | registry to check and install from (default: npm's configured registry, then npmjs) |
SAPTOOLS_UPDATE_DEBUG=1 | explain on stderr why nothing happened |
The updater switches itself off in CI (CI set), under NODE_ENV=test or NO_UPDATE_NOTIFIER, when
the binary runs from a source checkout, an npm link or an npx cache, and inside the re-run itself.
It never writes to stdout, never asks for input, never uses sudo, and never moves onto a prerelease.
Its state lives in ~/.saptools/updates/.
Quick start
export SAP_EMAIL="you@company.com"
export SAP_PASSWORD="your-sap-password"
cf-debugger start \
--region eu10 \
--org my-org \
--space dev \
--app my-app \
--verbose
Successful output includes the complete attachment target:
Debugger ready for my-app (eu10/my-org/dev).
Process: web
Instance: 0
Local port: 20142
Remote port: 9229
Session id: 550e8400-e29b-41d4-a716-446655440000
Tunnel PID: 83421
Node PID: 4312
Press Ctrl+C to stop.
Attach the IDE to 127.0.0.1:20142. Node derives the advertised
webSocketDebuggerUrl from the incoming request's Host header, so a URL
returned through the forward already names the local forwarded port and is
directly usable. cf-debugger validates that the URL is well formed but does
not print or follow it.
CLI
start
cf-debugger start --region eu10 --org my-org --space dev --app my-app
cf-debugger start eu10/my-org/dev/my-app --remote-port 9230
cf-debugger start my-app --process worker --instance 2 --node-pid 4312
cf-debugger start my-app --startup-timeout 300 --timeout 180 --verbose
When region, org, or space is omitted, the current cf target supplies it.
| Flag | Meaning |
|---|---|
--region <key> | SAP region key; defaults to the current CF target |
--api-endpoint <url> | Absolute HTTPS endpoint override, including private/nonstandard hosts |
--org <name>, --space <name>, --app <name> | CF target and app |
--process <name> | CF process, default web |
-i, --instance <index> | Zero-based process instance, default 0 |
--node-pid <pid> | Exact remote Node PID |
--port <number> | Preferred local port; otherwise choose from 20000–20999 |
--remote-port <number> | Port where cf-debugger looks for an existing remote inspector, default 9229 |
--timeout <seconds> | Local-tunnel and inspector-readiness budget, default 180 |
--startup-timeout <seconds> | Overall startup deadline, default 300, maximum 1800 |
--allow-ssh-enable-restart | Permit app-level SSH enablement and one app restart |
--no-ssh-enable-restart | Explicitly forbid that mutation, overriding flag/environment |
--verbose | Print status, retry, and redacted tunnel transport diagnostics |
Important
App restart is opt-in in 0.2.0. By default, a disabled-SSH app fails with
SSH_NOT_ENABLEDand no deployment mutation. Use--allow-ssh-enable-restart, or setCF_DEBUGGER_ALLOW_RESTART=1, only when restarting the named app is acceptable.CF_DEBUGGER_ALLOW_RESTART=0keeps the guard enabled for a production shell and also vetoes a programmaticallowSshEnableRestart: true. The environment value1can opt in the CLI, but never grants permission to a programmatic caller by itself.
--api-endpoint accepts arbitrary hostnames but requires an absolute https:
URL with no userinfo, query, fragment, or non-root path. Invalid values are
rejected before cf api or cf auth. Version 0.2.2 intentionally rejects
plaintext http:// landscapes because credentials would be sent to that
endpoint.
Even with permission, a restart occurs only when all of these are true:
- the one-shot SSH error specifically says SSH support is disabled;
cf ssh-enabled <app>proves app-level SSH is disabled;- this invocation runs
cf enable-ssh <app>; - a second probe proves app-level SSH is now enabled.
An unknown probe result, an already-enabled app, a permission error, or a
space-level SSH policy never causes a restart. Before an allowed restart,
stderr names the app and org/space. With an explicit --node-pid, automatic
restart is always rejected because the container replacement invalidates that
PID.
Read-only CF commands have a 60-second per-attempt cap and retry transient
transport errors with bounded exponential delays while the single overall
startup budget still has time. Deployment-mutating commands have a separate
180-second cap and are never retried: a timed-out enable/restart reports
CF_MUTATION_TIMEOUT because the server-side mutation may still be completing.
cf auth has no outer retry loop, and rejected credentials fail after one
attempt; repeated manual failures may count toward a tenant's identity-provider
lockout policy. The one-shot SSH signal and readiness wait are also clamped to
the same overall deadline. No retry configuration multiplies startup work past
--startup-timeout. After a timeout, fail-closed tunnel and state cleanup still
runs; that safety teardown can briefly extend the observed command wall time
rather than abandoning an unverified child.
stop
cf-debugger stop --session-id 550e8400-e29b-41d4-a716-446655440000
cf-debugger stop eu10/my-org/dev/my-app
cf-debugger stop my-app --api-endpoint https://api.cf.eu10.hana.ondemand.com
cf-debugger stop my-app --node-pid 4312
cf-debugger stop --all
cf-debugger stop --session-id 550e8400-e29b-41d4-a716-446655440000 --force
stop --all attempts every local session, reports stopped, forced, stale,
pending, and failed outcomes separately, and exits 70 if any tunnel cleanup
failed (1 for other failures). It cannot
be combined with a positional selector, --session-id, or target selector
options; ambiguous scope is rejected instead of widening to every session.
stop --force means “forget safely,” not “kill harder.” It never signals a PID
whose tunnel ownership is unproven. It forgets the state record first, then
best-effort removes only the exact derived v2 CF_HOME. A deletion failure is
non-fatal and warns that the retained path may contain a live refresh token;
doctor then reports it as an orphan eligible for doctor --cleanup. The
warning also names the abandoned PID and port for manual investigation. If a
damaged record names a non-owned home path, that path is left untouched while
the record is still forgotten.
list and status
cf-debugger list
cf-debugger status --session-id 550e8400-e29b-41d4-a716-446655440000
cf-debugger status eu10/my-org/dev/my-app --api-endpoint https://api.example
list prunes only records proven stale. PID liveness is paired with an optional
OS process-start token to detect PID reuse; older records without the additive
token retain the compatible PID-only check. A ready record is healthy only when
the complete listener-owner set contains its recorded tunnel PID. Ownership
that cannot be inspected remains explicit rather than being guessed.
status prints null only when no exact session matches. Ambiguity is a coded
SESSION_AMBIGUOUS failure; refine with session ID, API endpoint, or Node PID.
doctor
cf-debugger doctor
cf-debugger doctor --cleanup
The default is read-only JSON reporting. It includes:
- every local state record with its health verdict and reason;
- v2 session homes with no state record;
- listeners in
20000–20999that no state record claims; - leftover state temp, lock, recovery, stop-intent, and corrupt-backup files;
- legacy v1 state/homes, home count, parseable claimed sessions, conservative PID liveness, and their credential-retention risk.
--cleanup removes only canonical orphan v2 homes and sufficiently old,
package-owned temp/lock/recovery/stop-intent artifacts. It revalidates orphan
homes against lock-guarded state immediately before deletion. It never signals
an unclaimed listener, removes corrupt evidence, or removes legacy v1 artifacts.
Corrupt backups include a manual-removal command in the report while remaining
ineligible for automatic cleanup. A symlinked v2 homes root is reported and
never traversed.
Region resolution
Curated region keys are available from listKnownRegionKeys(). Keys matching
aa00 or aa00-000 also work before the curated table is updated:
cf-debugger synthesizes https://api.cf.<key>.hana.ondemand.com, or the
.platform.sapcloud.cn domain for cn*, and warns with the synthesized URL.
This supports new regions but makes a syntactically valid typo such as eu99
reach DNS; verify the warning or use --api-endpoint. Malformed keys fail early
with UNKNOWN_REGION.
How it works
- Resolve and validate the endpoint, deadline, target, and credentials.
- Prune only provably stale state and register a unique session/port.
- Run
cf api, environment-onlycf auth,cf target, and an app existence check. - Probe the selected instance through one-shot
cf ssh. - If an inspector already owns the remote port and is Node, reuse that PID.
- Otherwise:
- choose the sole Node PID when exactly one exists;
- with several Node PIDs, use the app
$PORTlistener only as a tiebreaker; - otherwise fail
NODE_PROCESS_AMBIGUOUS.
- Send
SIGUSR1when needed and prove the selected PID owns the configured remote inspector port. - Spawn detached
cf ssh -N -L <local>:localhost:<remote>. - Prove the spawned PID is among all owners of the local listener.
- Request
GET /json/listthrough the tunnel and require HTTP 200 plus a non-empty target array whose first entry has a well-formedwebSocketDebuggerUrl. - Persist
readystate and return aDebuggerHandle.
Node's Linux cluster default can leave the app $PORT socket owned by the
cluster primary rather than a request-handling worker. When several Node
processes exist, inspect the reported candidates and pass --node-pid
explicitly if a worker is the intended target.
The remote probe reads /proc/<pid>/exe, not argv. Its marker lines are filtered
by a strict saptools-inspector-* prefix, so CF SSH banners cannot impersonate
results. GNU find -lname is capability-probed and is normally fast; the
portable fallback walks descriptors with readlink and can take roughly
10–12 seconds in a descriptor-heavy non-GNU container.
--remote-port does not choose the port opened by SIGUSR1; Node always uses
its default inspector port for that signal. A non-default value is therefore
for an app already started with matching --inspect=<port>.
Status sequence
The public onStatus sequence is:
starting → logging-in → targeting → signaling
→ [ssh-enabling → ssh-restarting → signaling]
→ tunneling → ready → stopping → stopped
Verbose retries repeat the current phase with a message and remaining budget.
After starting is emitted, startup failure emits terminal error and never
follows it with stopped. Option/credential/region validation can reject before
the status stream begins, in which case no callback is emitted.
Unexpected tunnel failure after readiness likewise emits terminal error;
cleanup still runs without claiming a normal stop sequence.
State and security
~/.saptools/cf-debugger-state-v2.json
~/.saptools/cf-debugger-state-v2.lock
~/.saptools/cf-debugger-homes-v2/<sessionId>/
State is written as a 0600 temp file plus atomic rename while a token-owned
open(..., "wx") lock is held. The parent and session-home directories are
0700. Keep ~/.saptools on a local filesystem: exclusive-create locking is
not reliably atomic on every NFS/SMB implementation. A generous age fallback
recovers foreign-host lock files, but it cannot make a network filesystem's
locking semantics safe.
Each isolated CF_HOME contains CF CLI credentials, including a live refresh
token. A clean stop removes it. If TUNNEL_TERMINATION_FAILED retains state and
the home, recover the process first, then run stop --force. If a record has
already disappeared but its canonical home remains, use doctor --cleanup.
Do not leave recovered homes indefinitely.
A damaged entry no longer erases healthy sessions. Invalid entries are dropped
individually, and the original state file is moved to a private
.corrupt-<timestamp>-<uuid> backup before any repair. Completely invalid JSON,
version, or root shape is likewise preserved before a fresh empty v2 file is
written.
During mixed-version use, an older cf-debugger sharing this v2 file may strip
optional fields it does not understand, including process-identity and startup
budget data. Newer code treats an absent/legacy identity as “cannot tell” and
retains the record with PID-only compatibility rather than pruning it; doctor
surfaces that degraded verdict. Updating every installed consumer is the only
way to restore the stronger identity check persistently.
Legacy ~/.saptools/cf-debugger-homes/ directories may still contain live
refresh and access tokens. doctor reports but never deletes them. After
confirming no v1 tunnel is running, remove them explicitly:
rm -rf "$HOME/.saptools/cf-debugger-homes" \
"$HOME/.saptools/cf-debugger-state.json" \
"$HOME/.saptools/cf-debugger-state.lock"
The v2 CLI never adopts or mutates v1 sessions. Prefer CLI/API operations over editing the v2 JSON by hand.
Credentials are passed to cf auth as CF_USERNAME and CF_PASSWORD in the
child environment, never argv. CF command and tunnel diagnostics are bounded
and redact sensitive values before they reach errors or verbose output.
CF_COLOR=false is forced so ANSI escapes cannot corrupt output parsing.
Environment variables
| Variable | Meaning |
|---|---|
SAP_EMAIL, SAP_PASSWORD | CF authentication credentials |
CF_DEBUGGER_ALLOW_RESTART=0|1 | CLI default; 0 is also a hard veto for the programmatic API |
CF_DEBUGGER_CF_BIN | Replace the cf executable; useful for wrappers and deterministic test stubs |
A caller-provided child environment cannot override the per-session CF_HOME.
Errors and exit status
Documented domain failures use Error [CODE]: .... An unexpected operating
system or filesystem failure can still surface as a plain Error without a
domain code; it is never converted into success.
| Code | Meaning |
|---|---|
UNKNOWN_REGION, UNSAFE_INPUT, MISSING_CREDENTIALS | Invalid endpoint/key, selector, numeric input, or credentials |
CF_LOGIN_FAILED, CF_AUTH_FAILED, CF_TARGET_FAILED, CF_CLI_FAILED, CF_CLI_TIMEOUT | CF command failure; rejected auth is not retried |
CF_MUTATION_TIMEOUT | A deployment mutation timed out, was not retried, and may still be completing remotely |
STARTUP_TIMEOUT, ABORTED | Overall deadline expired, or caller/stop intent cancelled startup |
APP_NOT_FOUND | The targeted app does not exist in the selected org/space |
SSH_NOT_ENABLED, SSH_PERMISSION_DENIED, SSH_STATE_UNKNOWN | Restart refused/not useful, permission denied, or SSH state ambiguous |
NODE_PID_RESTART_UNSAFE | Explicit PID cannot survive an app restart |
NODE_PROCESS_NOT_FOUND, NODE_PROCESS_AMBIGUOUS, NODE_PID_INVALID | Remote Node selection failed closed |
USR1_SIGNAL_FAILED, INSPECTOR_NOT_READY, INSPECTOR_OWNER_MISMATCH, INSPECTOR_OUTPUT_TOO_LARGE | Remote signal/ownership protocol failed |
TUNNEL_NOT_READY | The spawned local forward did not bind |
PORT_UNAVAILABLE, TUNNEL_PROCESS_MISSING | Local port allocation failed, or the spawned tunnel exposed no PID |
TUNNEL_OWNER_UNVERIFIED, TUNNEL_OWNER_MISMATCH | Local listener ownership could not be proved |
INSPECTOR_UNREACHABLE | Local forward bound, but /json/list did not prove an attachable remote inspector |
TUNNEL_EXITED, TUNNEL_TERMINATION_FAILED, TUNNEL_OWNERSHIP_UNVERIFIED | Tunnel died, cleanup could not terminate it, or stop could not safely signal it |
SESSION_ALREADY_RUNNING, SESSION_AMBIGUOUS, SESSION_NOT_FOUND | Session selection conflict |
SESSION_STATE_LOST, SESSION_STATE_CONFLICT, STATE_LOCK_TIMEOUT | Atomic state/lifecycle invariant failed |
CF_HOME_CLEANUP_FAILED | Session state was removed but lifecycle cleanup could not remove its exact credential-bearing CF home |
PACKAGE_METADATA_INVALID | Runtime package metadata has no usable version |
STOP_FAILED | One entry in a batch stop failed with a non-coded internal error |
| Exit | Meaning |
|---|---|
0 | Clean completion |
1 | Coded operational/startup error, including STARTUP_TIMEOUT |
70 | Tunnel or lifecycle cleanup failed; ownership state or a credential-bearing home may remain |
| SSH child's nonzero code | An unexpected numeric SSH exit is preserved, for example 255 |
128 + signal | Tunnel child died from a signal, for example 137 for SIGKILL |
130, 143 | User interrupted with SIGINT or SIGTERM |
Programmatic API
import {
startDebugger,
stopDebugger,
stopAllDebuggers,
listSessions,
runDoctor,
} from "@saptools/cf-debugger";
const handle = await startDebugger({
region: "eu10",
org: "my-org",
space: "dev",
app: "my-app",
startupTimeoutMs: 300_000,
allowSshEnableRestart: false,
});
// Attach to handle.session.localPort.
await handle.dispose();
const summary = await stopAllDebuggers();
// Outcomes and counts distinguish stopped/forced/stale/pending/failed.
Persisted schema additions in 0.2.0 are optional. Absence means compatible
fallback behavior because an older mixed-version writer may strip fields it
does not know. State remains version "2".
FAQ
Does this modify the remote app?
Not by default. Inspector activation sends SIGUSR1 to the verified Node
process, but app-level SSH enablement and cf restart require explicit
--allow-ssh-enable-restart permission. Even with permission, restart occurs
only after this run changes app SSH from proven disabled to proven enabled; an
already-enabled, unknown, permission-denied, or space-policy result never
restarts the app.
Is session state always removed when the CLI exits?
Only after cleanup is verified. TUNNEL_TERMINATION_FAILED deliberately retains
state and the isolated credential-bearing CF_HOME; recover it with doctor
and stop --force instead of assuming the next start can proceed.
Development
pnpm --filter @saptools/cf-debugger lint
pnpm --filter @saptools/cf-debugger typecheck
pnpm --filter @saptools/cf-debugger test:unit
pnpm --filter @saptools/cf-debugger build
The fake-backed tests set CF_DEBUGGER_CF_BIN. Live E2E tests require a safe
non-production CF target and credentials; do not run them against production.
History
Other 1
Notes
- Self-updating. Every command now checks npm for a newer
@saptools/cf-debugger(at most once an hour) and, when one exists, installs that exact version and re-runs the command on it, announcing both steps on stderr.SAPTOOLS_AUTO_UPDATE=on|notify|offcontrols it (see the README's Updates section); it is off by itself in CI, in tests, from a source checkout and inside the re-run. Newcf-debugger self-update [--check]forces the check and install now.--versionnow readspackage.jsonat runtime. Both come from the private, build-time-bundled@saptools/corepackage; the published tarball gains no runtime dependency.
Fixes, Other
Notes
This security and correctness patch hardens endpoint validation, deployment mutation retries, and forced-stop recovery while keeping state on additive schema v2. Security behavior change:
--api-endpointand the corresponding programmatic start option now reject plaintext or malformed endpoints that older releases accepted, because authentication credentials would otherwise be sent to that endpoint.P0 — mutation, recovery, and credential safety
- P0-1:
cf enable-sshandcf restartare explicitly classified as non-retryable mutations. A timeout issues no second mutation and reportsCF_MUTATION_TIMEOUT, warning that the platform operation may still be in flight; read-only transient failures retain bounded retries. - P0-2:
stop --forcenow removes the state record before best-effort deletion of its exact ownedCF_HOME. An undeletable home produces a token-risk warning and becomes visible todoctorinstead of trapping the session in state forever. - P0-3 (breaking): API endpoint overrides must be absolute HTTPS URLs with
no userinfo, query, fragment, or non-root path. Invalid CLI and library
values fail as
UNSAFE_INPUTbefore anycfinvocation or authentication.
P1 — ownership and cross-version resilience
- P1-4: timed-out one-shot SSH commands can terminate a still-open child spawned by this process even when the OS cannot provide a birth token; forced escalation remains identity-gated.
- P1-5: macOS process identities now force
TZ=UTCandLC_ALL=Cand use versioned tokens. Legacy, malformed, or future token formats degrade to an unverified/retain verdict instead of pruning a live session. - P1-6: expensive listener ownership probes run outside the global state lock on the normal path. A changed or stale candidate is revalidated under the persistence lock before any prune, closing the TOCTOU gap.
- P1-7:
doctor --cleanuprefuses to traverse a symlinked or non-directory v2 homes root and reports non-directory entries; a symlinked parent~/.saptoolsremains supported. - P1-8: startup cancellation keeps the lock-free sidecar fast path and
again treats state
stopRequestedAtor a missing record as compatibility stop signals, at the existing 500 ms cadence. - P1-9:
doctornow identifies PID-only fallback when a mixed-version writer stripped the optional process-identity token.
P2 — platform and outcome correctness
- P2-1: loopback inspector HTTP checks opt out of the process-wide HTTP
agent, so environment proxy support cannot route
/json/listaway from127.0.0.1. - P2-2: inspector attempt timeouts grow from 2.5 to 5 to 10 seconds while preserving retry and overall-budget headroom, allowing slow healthy inspectors to prove readiness.
- P2-3: tunnel readiness budgets local bind, both ownership checks, and the inspector phase inside one deadline. A slow ownership command can no longer consume the inspector's reserved HTTP window.
- P2-4: socket error listeners are installed before timeout/connect and all derived TCP timeouts are clamped positive, preventing a negative-timeout process crash.
- P2-5: lifecycle cleanup no longer treats missing
lsofas proof that a confirmed-dead owned child survived; it reports the lost diagnostic without silently assuming termination. - P2-6: ready-session stop intents are cleared after use and the tunnel close handler cross-checks state, preventing stale sidecars from turning a later crash into exit 0.
- P2-7:
stop --alldistinguishes forced outcomes and returns cleanup exit code70when any captured outcome containsTUNNEL_TERMINATION_FAILED. - P2-8:
CF_DEBUGGER_ALLOW_RESTART=0is now a hard veto for library calls as well as the CLI; environment value1never grants a programmatic caller permission by itself. - P2-9: duplicate session IDs keep the first valid record and drop only later copies, preserving ownership evidence for a possibly live tunnel.
- P2-10: stale main-lock and abandoned recovery-lock reclamation now compare-delete against the originally observed owner token (or a legacy file fingerprint), so a replacement lock is not blindly unlinked.
- P2-11: corrupt-state backups remain ineligible for automatic deletion but now include an evidence warning and shell-quoted manual removal command.
- P2-12: stop-intent inspection failures no longer turn an otherwise confirmed lifecycle shutdown into a false tunnel failure.
- P2-13:
doctor --cleanupreports eligible cleanup asskipped, rather thannot-requested, when corrupt/incomplete state makes deletion unsafe. - P2-14: an unknown child signal maps to a stable nonzero fallback instead
of producing
NaNand a secondary range error. - P2-15: CLI and README wording now clarify that
--remote-portselects where to find an inspector already opened with matching--inspect=<port>;SIGUSR1cannot choose a custom port.
P3 — maintainability and diagnostics
- P3-1: oversized lifecycle, startup, command-execution, process-stop, and CLI handlers were split into focused helpers without changing their fail-closed outcomes.
- P3-2: one-shot SSH execution, shared bounded-output/redaction helpers, and persistent-tunnel diagnostics now live in separate modules, leaving every source file below the package limit.
- P3-3: startup-age and recorded-process verdicts have one shared implementation for health pruning and explicit stop.
- P3-4: obsolete single-PID ownership and identity helpers plus the unused inspector timeout constant were removed.
- P3-5: readiness races dispose their losing child-close listeners instead of leaking two listeners per start.
- P3-6: legacy-v1 doctor output now reports home counts and defensively parsed claimed sessions with conservative PID liveness, without adopting or mutating v1 state.
- P3-7: missing-app failures consistently name
--app or selector. - P3-8:
--versionreads the installed package metadata at runtime, so it cannot drift when packaging updates the version after compilation. - P3-9: documentation now calls out the descriptor-dependent cost of the
non-GNU remote
/procfallback. - P3-10: ambiguous multi-Node selection now reports the discovered PID
candidates so cluster users can choose a request-handling worker with
--node-pid. - P3-11: automatic local-port scans start at a deterministic hash of the exact registration identity and wrap the full range, reducing concurrent first-candidate collisions while preserving preferred-port priority.
- P0-1:
Other 1
Notes
- Re-verify the spawned one-shot SSH process identity immediately before both graceful termination and forced escalation. If Unix process ownership can no longer be proved, cf-debugger now refuses to signal the PID, releases its local stream handles, and preserves the existing timeout/abort result.
Notes
This minor release intentionally changes observable safety behavior. Startup can now reject a tunnel that 0.1.x reported as ready, app restart is opt-in, tunnel signal death and failed cleanup use nonzero exit statuses, and new recovery commands/flags are available. State remains additive schema v2.
- Readiness now proves both local listener ownership and an attachable Node
inspector response from
/json/list. A bound forward with no working remote inspector fails asINSPECTOR_UNREACHABLE. - Automatic
cf enable-sshpluscf restartis now opt-in through--allow-ssh-enable-restartorCF_DEBUGGER_ALLOW_RESTART=1. Permission, already-enabled, space-policy, and unknown-probe failures never restart the app; an allowed restart is warned with its exact target. - Session recovery now detects PID reuse, bounds abandoned startup records, and
provides
stop --forceto forget unverifiable state without signalling an unverified process. The publicstopAllDebuggers()return value changes from a count to aStopAllResultcontaining every per-session outcome and summary count; CLIstop --allrejects any simultaneous selector instead of silently widening the requested scope. - Tunnel signal death maps to
128 + signal; incomplete cleanup exits70. Cleanup ignores unrelated listeners that later occupy the old local port. - Region resolution includes all sibling-package regions and synthesizes valid
new SAP keys with a warning. Malformed keys use
UNKNOWN_REGION, and--api-endpointis available for exact/nonstandard landscapes. - Startup has one configurable overall deadline (
--startup-timeout, default five minutes). CF retries share that budget, rejected credentials are tried once, and verbose retry messages show the remaining time. - Bounded, redacted tunnel stdout/stderr tails now reach readiness and unexpected-exit diagnostics; verbose mode streams labelled redacted lines.
- One corrupt session no longer erases healthy records. Invalid originals are
preserved as private corrupt backups, and the new read-only
doctorcommand reports orphan homes/ports/temp files and legacy credential artifacts;doctor --cleanupperforms only guarded v2 cleanup. - Batch stop, Windows port parsing, multi-PID ownership, exact selectors,
guarded home deletion, stream-specific truncation, macOS
lsofdiagnostics, configurable remote inspector ports,--version, color-free CF parsing, and immutable per-sessionCF_HOMEbehavior are corrected. - Remote listener ownership avoids one fork per file descriptor, startup stop polling is reduced to a proportionate cadence, port scans no longer hold the global lock, and no-op state writes/redundant reads are removed.
- Dead CF table helpers are removed, bundles share split chunks, startup
responsibilities are separated, foreign-host locks age out conservatively,
failed status streams remain terminal at
error, and selector/input errors consistently use coded failures.
- Readiness now proves both local listener ownership and an attachable Node
inspector response from
Others providing
Compare all 6 on the Debugging page →- @saptools/cf-explorer@saptools/cf-explorerSearches the files of an app deployed to SAP BTP Cloud Foundry over cf ssh, finding the app root, grepping the deployed code and turning matches into file and line candidates.independent, btpFile search in apps deployed to SAP BTP Cloud Foundry over cf ssh
- @saptools/cf-function-trace@saptools/cf-function-traceRecords how one JavaScript function's state changes, step by step, in a Node.js process running locally or on SAP BTP Cloud Foundry, and stores the run as a redacted timeline that can be queried later.independent, btpStep-by-step state recorder for one JavaScript function in Node.js
- @saptools/cf-hana@saptools/cf-hanaA command line and TypeScript API that runs SQL against the SAP HANA Cloud database bound to a Cloud Foundry app, addressed by a region/org/space/app selector.independent, btpSQL runner against the SAP HANA Cloud database bound to a CF app
- @saptools/cf-inspector@saptools/cf-inspectorSets breakpoints, captures variable snapshots and evaluates expressions on a remote Node.js process over the Chrome DevTools Protocol from the shell, with no IDE attached.independent, btpShell debugger for remote Node.js over the Chrome DevTools Protocol
- @saptools/cf-live-trace@saptools/cf-live-traceInjects a bounded HTTP trace hook into a running Node.js app on SAP BTP Cloud Foundry and streams each request and response to the terminal as JSON.independent, btpHTTP trace hook streaming a running CF Node.js app's traffic
Details
Sources
Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.
Compatibility and provenance
History in the atlas
- First seen.
For your agent
- Ref
artifact:saptools-cf-debugger- JSON
- /artifacts/saptools-cf-debugger.json
- MCP
- Connecting an agent