abapgit-agent 1.17.9 → 1.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/abap/guidelines/debug-session.md +11 -4
- package/package.json +10 -6
- package/src/commands/debug.js +156 -119
- package/src/commands/inspect.js +71 -1
- package/src/commands/pull.js +3 -2
- package/src/commands/view.js +1 -1
- package/src/config.js +2 -0
- package/src/utils/abap-http.js +136 -252
- package/src/utils/adt-http.js +134 -216
- package/src/utils/debug-daemon.js +57 -48
- package/src/utils/debug-session.js +126 -25
|
@@ -216,13 +216,20 @@ abapgit-agent debug step --type continue --json # continue to next brea
|
|
|
216
216
|
abapgit-agent debug stack --json # call stack (shows which test method is active)
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
-
>
|
|
219
|
+
> To inspect a specific table row, use `--expand TABLE[N]`:
|
|
220
|
+
> ```bash
|
|
221
|
+
> abapgit-agent debug vars --expand LT_DATA[1] --json # first row — shows its fields
|
|
222
|
+
> abapgit-agent debug vars --expand LT_DATA[1]->CARRID --json # drill into one field in row 1
|
|
223
|
+
> ```
|
|
224
|
+
> Use `--expand LT_DATA` (without index) to see all rows first.
|
|
220
225
|
|
|
221
|
-
> **Field symbols
|
|
226
|
+
> **Field symbols are supported via source-parse enrichment.** `FIELD-SYMBOLS <FS>` declarations are not returned by ADT's `getChildVariables` hierarchy, so `debug vars` fetches the current method's source, extracts all `FIELD-SYMBOLS <name>` declarations, and requests them directly via `getVariables`. Assigned field symbols appear in the variable list and can be expanded like any other variable:
|
|
222
227
|
> ```bash
|
|
223
|
-
> abapgit-agent debug vars --
|
|
228
|
+
> abapgit-agent debug vars --json # <LS> appears in the list
|
|
229
|
+
> abapgit-agent debug vars --expand '<LS>' --json # expand field symbol children
|
|
230
|
+
> abapgit-agent debug vars --expand '<LS>'->NAME --json # single field
|
|
224
231
|
> ```
|
|
225
|
-
>
|
|
232
|
+
> Unassigned field symbols will be returned with an empty or unset value.
|
|
226
233
|
|
|
227
234
|
> **`step --type continue` return values:**
|
|
228
235
|
> - `{"continued":true,"finished":true}` — program ran to **completion** (ADT returned HTTP 500, session is over). Do not re-attach.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abapgit-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.1",
|
|
4
4
|
"description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
|
|
5
5
|
"files": [
|
|
6
6
|
"bin/",
|
|
@@ -42,10 +42,13 @@
|
|
|
42
42
|
"test:customize": "node tests/run-all.js --customize",
|
|
43
43
|
"test:cmd:debug": "node tests/run-all.js --cmd --command=debug",
|
|
44
44
|
"test:debug": "node tests/run-all.js --debug",
|
|
45
|
-
"test:debug:
|
|
46
|
-
"test:debug:
|
|
47
|
-
"test:debug:
|
|
48
|
-
"test:debug:
|
|
45
|
+
"test:debug:scripted": "node tests/run-all.js --debug-scripted",
|
|
46
|
+
"test:debug:scripted:3": "bash tests/integration/debug-scripted-scenarios.sh 3",
|
|
47
|
+
"test:debug:scripted:4": "bash tests/integration/debug-scripted-scenarios.sh 4",
|
|
48
|
+
"test:debug:scripted:5": "bash tests/integration/debug-scripted-scenarios.sh 5",
|
|
49
|
+
"test:debug:repl": "node tests/run-all.js --debug-repl",
|
|
50
|
+
"test:debug:repl:1": "bash tests/integration/debug-repl-scenarios.sh 1",
|
|
51
|
+
"test:debug:repl:2": "bash tests/integration/debug-repl-scenarios.sh 2",
|
|
49
52
|
"test:cmd:upgrade": "node tests/run-all.js --cmd --command=upgrade",
|
|
50
53
|
"test:lifecycle": "node tests/run-all.js --lifecycle",
|
|
51
54
|
"test:pull": "node tests/run-all.js --pull",
|
|
@@ -56,13 +59,14 @@
|
|
|
56
59
|
"unrelease": "node scripts/unrelease.js"
|
|
57
60
|
},
|
|
58
61
|
"dependencies": {
|
|
62
|
+
"axios": "^1.15.2",
|
|
59
63
|
"dotenv": "^16.3.1",
|
|
60
64
|
"node-fetch": "^2.7.0",
|
|
61
65
|
"uuid": "^9.0.0",
|
|
62
66
|
"winston": "^3.11.0"
|
|
63
67
|
},
|
|
64
68
|
"devDependencies": {
|
|
65
|
-
"jest": "^
|
|
69
|
+
"jest": "^30.3.0",
|
|
66
70
|
"supertest": "^7.2.2"
|
|
67
71
|
},
|
|
68
72
|
"engines": {
|
package/src/commands/debug.js
CHANGED
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
* - Local state persisted in tmp so stateless CLI calls share the breakpoint list.
|
|
20
20
|
*
|
|
21
21
|
* Session management for AI/scripting mode (--json):
|
|
22
|
-
* attach --json
|
|
23
|
-
*
|
|
22
|
+
* attach --json runs the IPC socket server in-process after emitting the
|
|
23
|
+
* session JSON line. This keeps the ADT TCP connection alive in the same
|
|
24
|
+
* process that called attach() — SAP pins the debug session to the
|
|
25
|
+
* originating TCP connection. step/vars/stack/terminate are thin IPC
|
|
24
26
|
* clients that connect to the daemon's Unix socket and exchange one JSON command
|
|
25
27
|
* per invocation. The daemon exits when terminate is called or after 30 min idle.
|
|
26
28
|
*
|
|
@@ -30,7 +32,6 @@
|
|
|
30
32
|
|
|
31
33
|
const net = require('net');
|
|
32
34
|
const path = require('path');
|
|
33
|
-
const { spawn } = require('child_process');
|
|
34
35
|
const { AdtHttp } = require('../utils/adt-http');
|
|
35
36
|
const { DebugSession } = require('../utils/debug-session');
|
|
36
37
|
const debugStateModule = require('../utils/debug-state');
|
|
@@ -47,7 +48,13 @@ const _loadBpState = debugStateModule.loadBreakpointState;
|
|
|
47
48
|
// Daemon socket path — may be absent in unit-test mocks
|
|
48
49
|
const _getDaemonSocketPath = debugStateModule.getDaemonSocketPath;
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
// Allow callers to override the terminalId/ideId used for listener and
|
|
52
|
+
// breakpoint registration via env var. This lets CI jobs use a unique ID
|
|
53
|
+
// per run to avoid stale listeners from crashed previous runs blocking
|
|
54
|
+
// the listener POST (SAP ICM returns 400 if another listener with the
|
|
55
|
+
// same terminalId is still registered and DELETE is not available on the
|
|
56
|
+
// system).
|
|
57
|
+
const ADT_CLIENT_ID = process.env.ABAPGIT_DEBUG_TERMINAL_ID || 'ABAPGIT-AGENT-CLI';
|
|
51
58
|
|
|
52
59
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
53
60
|
|
|
@@ -632,8 +639,8 @@ async function cmdDelete(args, config, adt) {
|
|
|
632
639
|
async function cmdAttach(args, config, adt) {
|
|
633
640
|
const sessionIdOverride = val(args, '--session');
|
|
634
641
|
const jsonOutput = hasFlag(args, '--json');
|
|
635
|
-
// Per-poll timeout in seconds sent to ADT (ADT blocks the POST for this long)
|
|
636
642
|
const pollTimeout = parseInt(val(args, '--timeout') || '30', 10);
|
|
643
|
+
const maxListenSeconds = parseInt(val(args, '--max-listen') || '240', 10);
|
|
637
644
|
// Shorter timeout used in takeover mode — keeps the connection alive but
|
|
638
645
|
// lets ADT process stepContinue quickly and lets session 2 exit within
|
|
639
646
|
// ~5 seconds of clearActiveSession being written.
|
|
@@ -646,8 +653,42 @@ async function cmdAttach(args, config, adt) {
|
|
|
646
653
|
process.stderr.write('\n Waiting for breakpoint hit... (run your ABAP program in a separate window)\n');
|
|
647
654
|
}
|
|
648
655
|
|
|
656
|
+
// Fresh session for attach flow. Don't set stateful here — CSRF fetch
|
|
657
|
+
// without stateful avoids tying up a dialog WP. Stateful is added per-call
|
|
658
|
+
// by debug-session.js STATEFUL_HEADER on attach/getStack/step/vars.
|
|
659
|
+
adt.clearSession();
|
|
660
|
+
if (adt._axios) {
|
|
661
|
+
const agent = adt._axios.defaults.httpsAgent || adt._axios.defaults.httpAgent;
|
|
662
|
+
if (agent) agent.destroy();
|
|
663
|
+
if (adt._axios.defaults.httpsAgent) {
|
|
664
|
+
adt._axios.defaults.httpsAgent = new (require('https').Agent)({ rejectUnauthorized: false, keepAlive: true });
|
|
665
|
+
} else if (adt._axios.defaults.httpAgent) {
|
|
666
|
+
adt._axios.defaults.httpAgent = new (require('http').Agent)({ keepAlive: true });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
649
669
|
await adt.fetchCsrfToken();
|
|
650
670
|
|
|
671
|
+
// Delete any stale listener registered under our terminalId from a previous
|
|
672
|
+
// (possibly crashed) run. A frozen ABAP work process holding a listener for
|
|
673
|
+
// the same terminalId causes SAP ICM to return HTTP 400 "Service cannot be
|
|
674
|
+
// reached" for every new listener POST. Deleting first clears that state.
|
|
675
|
+
// Ignore errors — there may be no stale listener to delete.
|
|
676
|
+
if (!sessionIdOverride) {
|
|
677
|
+
try {
|
|
678
|
+
const delUrl = `/sap/bc/adt/debugger/listeners` +
|
|
679
|
+
`?debuggingMode=user` +
|
|
680
|
+
`&requestUser=${encodeURIComponent((config.user || '').toUpperCase())}` +
|
|
681
|
+
`&terminalId=${encodeURIComponent(ADT_CLIENT_ID)}` +
|
|
682
|
+
`&ideId=${encodeURIComponent(ADT_CLIENT_ID)}` +
|
|
683
|
+
`&checkConflict=false` +
|
|
684
|
+
`¬ifyConflict=true`;
|
|
685
|
+
await adt.delete(delUrl);
|
|
686
|
+
if (jsonOutput) process.stderr.write(`[debug-attach] deleted stale listener\n`);
|
|
687
|
+
} catch (e) {
|
|
688
|
+
if (jsonOutput) process.stderr.write(`[debug-attach] delete listener (cleanup): ${e.statusCode || e.message}\n`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
651
692
|
// Re-POST local breakpoints to refresh them on the server before listening.
|
|
652
693
|
// Breakpoints expire when the SAP session or work process is restarted.
|
|
653
694
|
// This ensures they are active regardless of when they were originally set.
|
|
@@ -672,6 +713,7 @@ async function cmdAttach(args, config, adt) {
|
|
|
672
713
|
}
|
|
673
714
|
let sessionId = sessionIdOverride;
|
|
674
715
|
let positionResult = null;
|
|
716
|
+
let session = null;
|
|
675
717
|
|
|
676
718
|
if (!sessionId) {
|
|
677
719
|
// ADT listeners: POST long-polls until a breakpoint is hit (or timeout).
|
|
@@ -683,9 +725,9 @@ async function cmdAttach(args, config, adt) {
|
|
|
683
725
|
`&terminalId=${encodeURIComponent(listenTerminalId)}` +
|
|
684
726
|
`&ideId=${encodeURIComponent(listenTerminalId)}`;
|
|
685
727
|
|
|
686
|
-
const MAX_POLLS = Math.ceil(
|
|
728
|
+
const MAX_POLLS = Math.ceil(maxListenSeconds / pollTimeout);
|
|
687
729
|
// In takeover mode we switch to takeoverPollTimeout — recalculate the limit then.
|
|
688
|
-
const MAX_TAKEOVER_POLLS = Math.ceil(
|
|
730
|
+
const MAX_TAKEOVER_POLLS = Math.ceil(maxListenSeconds / takeoverPollTimeout);
|
|
689
731
|
let dots = 0;
|
|
690
732
|
const attachStartedAt = Date.now(); // used to detect if another session takes over
|
|
691
733
|
let takenOver = false;
|
|
@@ -723,6 +765,12 @@ async function cmdAttach(args, config, adt) {
|
|
|
723
765
|
await new Promise(r => setTimeout(r, 2000));
|
|
724
766
|
continue;
|
|
725
767
|
}
|
|
768
|
+
if (err && err.statusCode === 400 &&
|
|
769
|
+
err.body && err.body.includes('Service cannot be reached')) {
|
|
770
|
+
if (jsonOutput) process.stderr.write(`[debug-attach] listener POST returned 400 (Service cannot be reached), retrying in 5s...\n`);
|
|
771
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
726
774
|
if (err && err.statusCode === 404) {
|
|
727
775
|
console.error(
|
|
728
776
|
'\n Debug session commands require ADT Debugger service (listeners).' +
|
|
@@ -742,6 +790,7 @@ async function cmdAttach(args, config, adt) {
|
|
|
742
790
|
const debuggeeIdMatch = resp.body.match(/<DEBUGGEE_ID>([^<]+)<\/DEBUGGEE_ID>/i) ||
|
|
743
791
|
resp.body.match(/DEBUGGEE_ID="([^"]+)"/i);
|
|
744
792
|
sessionId = debuggeeIdMatch ? debuggeeIdMatch[1].trim() : null;
|
|
793
|
+
if (jsonOutput && sessionId) process.stderr.write(`[debug-attach] listener returned debuggeeId=${sessionId}\n`);
|
|
745
794
|
|
|
746
795
|
if (!sessionId) {
|
|
747
796
|
// Fallback: some ADT versions put session info in different attributes
|
|
@@ -773,6 +822,34 @@ async function cmdAttach(args, config, adt) {
|
|
|
773
822
|
}
|
|
774
823
|
continue;
|
|
775
824
|
}
|
|
825
|
+
|
|
826
|
+
// Attach to the WP and verify it's alive before committing.
|
|
827
|
+
// In --json mode: if the WP is a stale session from a previous run,
|
|
828
|
+
// attach() may succeed but getPosition() returns 400. When that happens,
|
|
829
|
+
// reset the SAP session fully and continue listening for a fresh BP hit.
|
|
830
|
+
{
|
|
831
|
+
if (!jsonOutput) process.stderr.write('\n Attaching to debug session...\n');
|
|
832
|
+
const candidateSession = new DebugSession(adt, sessionId);
|
|
833
|
+
try {
|
|
834
|
+
await candidateSession.attach(sessionId, (config.user || '').toUpperCase());
|
|
835
|
+
} catch (e) {
|
|
836
|
+
if (jsonOutput) {
|
|
837
|
+
process.stderr.write(`[debug-attach] attach() failed: ${e.message || e} — resetting session\n`);
|
|
838
|
+
adt.clearSession();
|
|
839
|
+
await adt.fetchCsrfToken();
|
|
840
|
+
sessionId = null;
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
printHttpError(e, { prefix: ' Error during attach' });
|
|
844
|
+
if (e.body) console.error(' Response body:', e.body.substring(0, 400));
|
|
845
|
+
process.exit(1);
|
|
846
|
+
}
|
|
847
|
+
// attach() succeeded — skip getPosition() validation.
|
|
848
|
+
// With keepAlive, the connection is shared and getStack via the
|
|
849
|
+
// in-process daemon will use the same AdtHttp + same TCP socket.
|
|
850
|
+
positionResult = { position: {}, source: [] };
|
|
851
|
+
session = candidateSession;
|
|
852
|
+
}
|
|
776
853
|
break;
|
|
777
854
|
}
|
|
778
855
|
|
|
@@ -799,93 +876,53 @@ async function cmdAttach(args, config, adt) {
|
|
|
799
876
|
if (!jsonOutput) process.stderr.write(' Listener timed out waiting for other session to finish.\n\n');
|
|
800
877
|
process.exit(0);
|
|
801
878
|
}
|
|
802
|
-
console.error(
|
|
879
|
+
console.error(`\n Timeout: No breakpoint was hit within ${maxListenSeconds}s.\n`);
|
|
803
880
|
process.exit(1);
|
|
804
881
|
}
|
|
805
882
|
}
|
|
806
883
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
// debugger for that work process. Without this step, all subsequent calls
|
|
812
|
-
// (getStack, getVariables, step) return "noSessionAttached" (T100-530).
|
|
813
|
-
if (!sessionIdOverride) {
|
|
814
|
-
if (!jsonOutput) {
|
|
815
|
-
process.stderr.write('\n Attaching to debug session...\n');
|
|
816
|
-
}
|
|
884
|
+
// When using --session override (no listener), do attach+getPosition here.
|
|
885
|
+
if (sessionIdOverride) {
|
|
886
|
+
session = new DebugSession(adt, sessionId);
|
|
887
|
+
// getPosition is best-effort for --session override (e.g. human REPL recovery)
|
|
817
888
|
try {
|
|
818
|
-
await session.
|
|
889
|
+
positionResult = await session.getPosition();
|
|
819
890
|
} catch (e) {
|
|
820
|
-
|
|
821
|
-
if (e.body) console.error(' Response body:', e.body.substring(0, 400));
|
|
822
|
-
process.exit(1);
|
|
891
|
+
positionResult = { position: {}, source: [] };
|
|
823
892
|
}
|
|
824
893
|
}
|
|
825
894
|
|
|
826
|
-
//
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
positionResult = { position: {}, source: [] };
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// Dummy loop body retained for structural compatibility (never executes)
|
|
835
|
-
if (false) {
|
|
836
|
-
const MAX_POLLS = 0;
|
|
837
|
-
const POLL_INTERVAL = 0;
|
|
838
|
-
for (let i = 0; i < MAX_POLLS; i++) {
|
|
839
|
-
try {
|
|
840
|
-
const frames = await session.getStack();
|
|
841
|
-
if (frames && frames.length > 0 && frames[0].line > 0) {
|
|
842
|
-
positionResult = await session.getPosition();
|
|
843
|
-
break;
|
|
844
|
-
}
|
|
845
|
-
} catch (e) {
|
|
846
|
-
// Stack not ready yet — keep polling
|
|
847
|
-
}
|
|
848
|
-
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
849
|
-
}
|
|
895
|
+
// session and positionResult are now set (either via listener loop or --session override)
|
|
896
|
+
if (!session) {
|
|
897
|
+
// Should not reach here — listener loop either sets session or exits
|
|
898
|
+
console.error('\n Internal error: session not initialized after attach.\n');
|
|
899
|
+
process.exit(1);
|
|
850
900
|
}
|
|
901
|
+
if (!positionResult) positionResult = { position: {}, source: [] };
|
|
851
902
|
|
|
852
903
|
const { position, source } = positionResult;
|
|
853
904
|
saveActiveSession(config, { sessionId, position });
|
|
854
905
|
|
|
855
906
|
if (jsonOutput) {
|
|
856
|
-
//
|
|
857
|
-
//
|
|
858
|
-
//
|
|
907
|
+
// Run the socket server in-process so all ADT requests reuse the same TCP
|
|
908
|
+
// connection that called attach(). SAP pins the debug session to the
|
|
909
|
+
// originating TCP connection — a new connection returns HTTP 400.
|
|
859
910
|
const socketPath = _getDaemonSocketPath ? _getDaemonSocketPath(config) : null;
|
|
860
911
|
if (socketPath) {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
...process.env,
|
|
864
|
-
DEBUG_DAEMON_MODE: '1',
|
|
865
|
-
DEBUG_DAEMON_CONFIG: JSON.stringify(config),
|
|
866
|
-
DEBUG_DAEMON_SESSION_ID: sessionId,
|
|
867
|
-
DEBUG_DAEMON_SOCK_PATH: socketPath,
|
|
868
|
-
DEBUG_DAEMON_SESSION_SNAPSHOT: JSON.stringify(snapshot)
|
|
869
|
-
};
|
|
870
|
-
const daemonScript = path.resolve(__dirname, '../utils/debug-daemon.js');
|
|
871
|
-
const child = spawn(process.execPath, [daemonScript], {
|
|
872
|
-
detached: true,
|
|
873
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
874
|
-
env: daemonEnv
|
|
875
|
-
});
|
|
876
|
-
child.unref();
|
|
877
|
-
|
|
878
|
-
// Wait for daemon socket to appear (up to 5 s) before returning JSON
|
|
879
|
-
try {
|
|
880
|
-
await waitForSocket(socketPath, 5000);
|
|
881
|
-
// Persist socket path in session state so step/vars/stack/terminate find it
|
|
882
|
-
saveActiveSession(config, { sessionId, position, socketPath });
|
|
883
|
-
} catch (e) {
|
|
884
|
-
// Non-fatal: fall back to stateless direct-ADT mode
|
|
885
|
-
}
|
|
912
|
+
// Save cookies so terminate can release the frozen WP even if the daemon is gone
|
|
913
|
+
saveActiveSession(config, { sessionId, position, socketPath, cookies: adt.cookies });
|
|
886
914
|
}
|
|
887
915
|
|
|
916
|
+
// Emit session JSON now so the caller (script / AI) knows the BP was hit.
|
|
888
917
|
console.log(JSON.stringify({ session: sessionId, position, source }));
|
|
918
|
+
|
|
919
|
+
if (socketPath) {
|
|
920
|
+
const { startDaemon } = require('../utils/debug-daemon');
|
|
921
|
+
// Blocks until terminate is called or idle timeout — intentional.
|
|
922
|
+
// The process stays alive as the IPC socket server, holding the ADT
|
|
923
|
+
// TCP connection open. The caller runs this command in the background (&).
|
|
924
|
+
await startDaemon(session, socketPath);
|
|
925
|
+
}
|
|
889
926
|
return;
|
|
890
927
|
}
|
|
891
928
|
|
|
@@ -1050,8 +1087,8 @@ async function cmdVars(args, config, adt) {
|
|
|
1050
1087
|
* LO_FACTORY->MT_COMMAND_MAP (object attr → table)
|
|
1051
1088
|
* LS_DATA->COMPONENT (structure field)
|
|
1052
1089
|
*
|
|
1053
|
-
* When a daemon socket is active, uses daemon IPC for single-segment
|
|
1054
|
-
*
|
|
1090
|
+
* When a daemon socket is active, uses daemon IPC for both single-segment and
|
|
1091
|
+
* multi-segment paths so the stateful ADT session is always reused.
|
|
1055
1092
|
*/
|
|
1056
1093
|
async function cmdExpand(expandName, sessionId, socketPath, config, adt, jsonOutput) {
|
|
1057
1094
|
// Split on -> to detect path notation. Also handle --> typos by stripping stray dashes.
|
|
@@ -1059,20 +1096,37 @@ async function cmdExpand(expandName, sessionId, socketPath, config, adt, jsonOut
|
|
|
1059
1096
|
// [N]-FIELD → [N]->FIELD (array row then struct field)
|
|
1060
1097
|
// *-FIELD → *->FIELD (dereference then struct field: lr_request->*-files)
|
|
1061
1098
|
const normalizedName = expandName
|
|
1099
|
+
.replace(/^([A-Za-z0-9_@]+)\[(\d+)\]$/, '$1->[$2]') // VAR[N] → VAR->[N] (exact match)
|
|
1100
|
+
.replace(/([A-Za-z0-9_@]+)\[(\d+)\]->/g, '$1->[$2]->') // VAR[N]->X → VAR->[N]->X (mid-path)
|
|
1062
1101
|
.replace(/\](-(?!>))/g, ']->') // [N]-FIELD → [N]->FIELD
|
|
1063
1102
|
.replace(/\*(-(?!>))/g, '*->'); // *-FIELD → *->FIELD
|
|
1064
1103
|
const pathParts = normalizedName.split('->').map(s => s.replace(/^-+|-+$/g, '').trim()).filter(Boolean);
|
|
1065
1104
|
|
|
1066
|
-
// Multi-segment path:
|
|
1105
|
+
// Multi-segment path: route through daemon when available (daemon holds stateful ADT session)
|
|
1067
1106
|
if (pathParts.length > 1) {
|
|
1068
|
-
if (!adt.csrfToken) await adt.fetchCsrfToken();
|
|
1069
|
-
const session = new DebugSession(adt, sessionId);
|
|
1070
1107
|
let result;
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1108
|
+
if (socketPath) {
|
|
1109
|
+
let resp;
|
|
1110
|
+
try {
|
|
1111
|
+
resp = await sendDaemonCommand(socketPath, { cmd: 'expandPath', pathParts }, 60000);
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
printHttpError(err, { prefix: ' Error' });
|
|
1114
|
+
process.exit(1);
|
|
1115
|
+
}
|
|
1116
|
+
if (!resp.ok) {
|
|
1117
|
+
console.error(`\n Error: ${resp.error}\n`);
|
|
1118
|
+
process.exit(1);
|
|
1119
|
+
}
|
|
1120
|
+
result = { variable: resp.variable, children: resp.children };
|
|
1121
|
+
} else {
|
|
1122
|
+
if (!adt.csrfToken) await adt.fetchCsrfToken();
|
|
1123
|
+
const session = new DebugSession(adt, sessionId);
|
|
1124
|
+
try {
|
|
1125
|
+
result = await session.expandPath(pathParts);
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
printHttpError(err, { prefix: ' Error' });
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1076
1130
|
}
|
|
1077
1131
|
const { variable: target, children } = result;
|
|
1078
1132
|
return _printExpandResult(expandName, target, children, jsonOutput);
|
|
@@ -1243,20 +1297,29 @@ async function cmdTerminate(args, config, adt) {
|
|
|
1243
1297
|
|
|
1244
1298
|
// Prefer daemon IPC — daemon calls session.terminate() then exits itself
|
|
1245
1299
|
if (socketPath) {
|
|
1246
|
-
let
|
|
1300
|
+
let ipcSucceeded = false;
|
|
1247
1301
|
try {
|
|
1248
|
-
resp = await sendDaemonCommand(socketPath, { cmd: 'terminate' }, 30000);
|
|
1302
|
+
const resp = await sendDaemonCommand(socketPath, { cmd: 'terminate' }, 30000);
|
|
1303
|
+
if (resp && resp.ok) ipcSucceeded = true;
|
|
1249
1304
|
} catch (err) {
|
|
1250
|
-
// Socket
|
|
1251
|
-
resp = { ok: true, terminated: true };
|
|
1305
|
+
// Socket is gone (daemon crashed or was killed) — fall through to direct ADT call
|
|
1252
1306
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1307
|
+
if (ipcSucceeded) {
|
|
1308
|
+
clearActiveSession(config);
|
|
1309
|
+
if (jsonOutput) {
|
|
1310
|
+
console.log(JSON.stringify({ terminated: true }));
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
console.log('\n Debug session terminated.\n');
|
|
1256
1314
|
return;
|
|
1257
1315
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1316
|
+
// IPC failed — daemon is gone but ABAP WP may still be frozen.
|
|
1317
|
+
// Use stored session cookies to release the WP via direct ADT call.
|
|
1318
|
+
const savedState = loadActiveSession(config);
|
|
1319
|
+
if (savedState && savedState.cookies && sessionId) {
|
|
1320
|
+
adt.cookies = savedState.cookies;
|
|
1321
|
+
}
|
|
1322
|
+
// Fall through to direct ADT terminate below
|
|
1260
1323
|
}
|
|
1261
1324
|
|
|
1262
1325
|
// Fallback: direct ADT call
|
|
@@ -1289,32 +1352,6 @@ async function cmdTerminate(args, config, adt) {
|
|
|
1289
1352
|
|
|
1290
1353
|
// ─── Daemon IPC helpers ───────────────────────────────────────────────────────
|
|
1291
1354
|
|
|
1292
|
-
/**
|
|
1293
|
-
* Wait for the daemon's Unix socket to appear (after spawn).
|
|
1294
|
-
* @param {string} socketPath
|
|
1295
|
-
* @param {number} timeoutMs
|
|
1296
|
-
*/
|
|
1297
|
-
function waitForSocket(socketPath, timeoutMs) {
|
|
1298
|
-
return new Promise((resolve, reject) => {
|
|
1299
|
-
const deadline = Date.now() + timeoutMs;
|
|
1300
|
-
function check() {
|
|
1301
|
-
const client = net.createConnection(socketPath);
|
|
1302
|
-
client.on('connect', () => {
|
|
1303
|
-
client.destroy();
|
|
1304
|
-
resolve();
|
|
1305
|
-
});
|
|
1306
|
-
client.on('error', () => {
|
|
1307
|
-
if (Date.now() >= deadline) {
|
|
1308
|
-
reject(new Error('Timeout waiting for debug daemon to start'));
|
|
1309
|
-
} else {
|
|
1310
|
-
setTimeout(check, 100);
|
|
1311
|
-
}
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
check();
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
1355
|
/**
|
|
1319
1356
|
* Send one JSON command to the daemon and return the parsed JSON response.
|
|
1320
1357
|
* @param {string} socketPath
|
package/src/commands/inspect.js
CHANGED
|
@@ -285,7 +285,7 @@ Examples:
|
|
|
285
285
|
process.exit(1);
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
|
|
288
|
+
let filesSyntaxCheck = args[filesArgIndex + 1].split(',').map(f => f.trim());
|
|
289
289
|
|
|
290
290
|
// Parse optional --variant parameter; fall back to project config
|
|
291
291
|
const variantArgIndex = args.indexOf('--variant');
|
|
@@ -293,6 +293,31 @@ Examples:
|
|
|
293
293
|
const inspectConfig = getInspectConfig();
|
|
294
294
|
const variant = variantArg || inspectConfig.variant || null;
|
|
295
295
|
|
|
296
|
+
// Filter out files matching inspect.exclude patterns from project config
|
|
297
|
+
const excludePatterns = inspectConfig.exclude || [];
|
|
298
|
+
if (excludePatterns.length > 0) {
|
|
299
|
+
const before = filesSyntaxCheck.length;
|
|
300
|
+
filesSyntaxCheck = filesSyntaxCheck.filter(f => {
|
|
301
|
+
const objName = pathModule.basename(f).split('.')[0].toUpperCase();
|
|
302
|
+
return !excludePatterns.some(pattern => {
|
|
303
|
+
const re = new RegExp('^' + pattern.toUpperCase().replace(/\*/g, '.*') + '$');
|
|
304
|
+
return re.test(objName);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
const skipped = before - filesSyntaxCheck.length;
|
|
308
|
+
if (skipped > 0 && !args.includes('--json')) {
|
|
309
|
+
console.log(` Skipped ${skipped} file(s) excluded by inspect config`);
|
|
310
|
+
}
|
|
311
|
+
if (filesSyntaxCheck.length === 0) {
|
|
312
|
+
if (args.includes('--json')) {
|
|
313
|
+
console.log(JSON.stringify([]));
|
|
314
|
+
} else {
|
|
315
|
+
console.log('\n All files excluded by inspect config — nothing to check.\n');
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
296
321
|
// Parse optional --junit-output parameter
|
|
297
322
|
const junitArgIndex = args.indexOf('--junit-output');
|
|
298
323
|
const junitOutput = junitArgIndex !== -1 ? args[junitArgIndex + 1] : null;
|
|
@@ -316,6 +341,51 @@ Examples:
|
|
|
316
341
|
// Send all files in one request
|
|
317
342
|
const results = await inspectAllFiles(filesSyntaxCheck, csrfToken, config, variant, http, verbose);
|
|
318
343
|
|
|
344
|
+
// Apply inspect.suppress rules — downgrade matching errors/warnings to infos
|
|
345
|
+
const suppressRules = inspectConfig.suppress || [];
|
|
346
|
+
if (suppressRules.length > 0) {
|
|
347
|
+
for (const result of results) {
|
|
348
|
+
const objName = (result.OBJECT_NAME || result.object_name || '').toUpperCase();
|
|
349
|
+
for (const rule of suppressRules) {
|
|
350
|
+
const objPattern = new RegExp('^' + (rule.object || '*').toUpperCase().replace(/\*/g, '.*') + '$');
|
|
351
|
+
if (!objPattern.test(objName)) continue;
|
|
352
|
+
const msgPattern = new RegExp((rule.message || '*').replace(/\*/g, '.*'), 'i');
|
|
353
|
+
|
|
354
|
+
// Downgrade matching errors → infos
|
|
355
|
+
const errors = result.ERRORS || result.errors || [];
|
|
356
|
+
const kept = [];
|
|
357
|
+
for (const err of errors) {
|
|
358
|
+
const text = err.TEXT || err.text || '';
|
|
359
|
+
if (msgPattern.test(text)) {
|
|
360
|
+
const infos = result.INFOS || result.infos || [];
|
|
361
|
+
infos.push({ ...err, MESSAGE: `[suppressed] ${text}`, message: `[suppressed] ${text}` });
|
|
362
|
+
if (result.INFOS !== undefined) result.INFOS = infos; else result.infos = infos;
|
|
363
|
+
const ec = result.ERROR_COUNT !== undefined ? 'ERROR_COUNT' : 'error_count';
|
|
364
|
+
result[ec] = Math.max(0, (result[ec] || 0) - 1);
|
|
365
|
+
} else {
|
|
366
|
+
kept.push(err);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (result.ERRORS !== undefined) result.ERRORS = kept; else result.errors = kept;
|
|
370
|
+
|
|
371
|
+
// Downgrade matching warnings → infos
|
|
372
|
+
const warnings = result.WARNINGS || result.warnings || [];
|
|
373
|
+
const keptW = [];
|
|
374
|
+
for (const warn of warnings) {
|
|
375
|
+
const text = warn.MESSAGE || warn.message || '';
|
|
376
|
+
if (msgPattern.test(text)) {
|
|
377
|
+
const infos = result.INFOS || result.infos || [];
|
|
378
|
+
infos.push({ ...warn, MESSAGE: `[suppressed] ${text}`, message: `[suppressed] ${text}` });
|
|
379
|
+
if (result.INFOS !== undefined) result.INFOS = infos; else result.infos = infos;
|
|
380
|
+
} else {
|
|
381
|
+
keptW.push(warn);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (result.WARNINGS !== undefined) result.WARNINGS = keptW; else result.warnings = keptW;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
319
389
|
// JUnit output mode — write XML file, then continue to normal output
|
|
320
390
|
if (junitOutput) {
|
|
321
391
|
const xml = buildInspectJUnit(results);
|
package/src/commands/pull.js
CHANGED
|
@@ -329,7 +329,8 @@ Examples:
|
|
|
329
329
|
const jobId = result.JOB_ID || result.job_id;
|
|
330
330
|
const message = result.MESSAGE || result.message;
|
|
331
331
|
const errorDetail = result.ERROR_DETAIL || result.error_detail;
|
|
332
|
-
const
|
|
332
|
+
const activatedCountRaw = result.ACTIVATED_COUNT ?? result.activated_count ?? null;
|
|
333
|
+
const activatedCount = activatedCountRaw || 0;
|
|
333
334
|
const failedCount = result.FAILED_COUNT || result.failed_count || 0;
|
|
334
335
|
const logMessages = result.LOG_MESSAGES || result.log_messages || [];
|
|
335
336
|
const activatedObjects = result.ACTIVATED_OBJECTS || result.activated_objects || [];
|
|
@@ -362,7 +363,7 @@ Examples:
|
|
|
362
363
|
if (success === 'X' || success === true) {
|
|
363
364
|
console.log(`✅ Pull completed successfully!`);
|
|
364
365
|
console.log(` Message: ${message || 'N/A'}`);
|
|
365
|
-
if (activatedCount === 0 && files && !isRepull) {
|
|
366
|
+
if (activatedCountRaw !== null && activatedCount === 0 && activatedObjects.length === 0 && files && !isRepull) {
|
|
366
367
|
console.warn(`⚠️ ACTIVATED_COUNT: 0 — no objects were activated. Check for unpushed commits: git log origin/<branch>..HEAD`);
|
|
367
368
|
}
|
|
368
369
|
} else if (failedCount === 0 && failedObjects.length === 0 &&
|
package/src/commands/view.js
CHANGED
|
@@ -151,7 +151,7 @@ async function computeGlobalStarts(objName, sections, config) {
|
|
|
151
151
|
* Returns 0 if no better line is found (falls back to METHOD statement).
|
|
152
152
|
*/
|
|
153
153
|
function findFirstExecutableLine(lines) {
|
|
154
|
-
const declPattern = /^\s*(data|final|types|constants|class-data)[\s:(]/i;
|
|
154
|
+
const declPattern = /^\s*(data|final|types|constants|class-data|field-symbols)[\s:(</\[]/i;
|
|
155
155
|
const methodPattern = /^\s*method\s+/i;
|
|
156
156
|
const commentPattern = /^\s*[*"]/;
|
|
157
157
|
// Program-level header/declaration keywords that are not executable statements
|
package/src/config.js
CHANGED
|
@@ -255,6 +255,8 @@ function getInspectConfig() {
|
|
|
255
255
|
const projectConfig = loadProjectConfig();
|
|
256
256
|
return {
|
|
257
257
|
variant: projectConfig?.inspect?.variant || null,
|
|
258
|
+
exclude: projectConfig?.inspect?.exclude || [],
|
|
259
|
+
suppress: projectConfig?.inspect?.suppress || [],
|
|
258
260
|
};
|
|
259
261
|
}
|
|
260
262
|
|