abapgit-agent 1.10.1 → 1.11.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/.abapGitAgent.example +1 -0
- package/abap/CLAUDE.md +71 -35
- package/bin/abapgit-agent +7 -4
- package/package.json +2 -1
- package/src/commands/debug.js +1 -1
- package/src/commands/pull.js +79 -11
- package/src/commands/status.js +2 -0
- package/src/commands/transport.js +290 -0
- package/src/config.js +64 -3
- package/src/utils/abap-http.js +5 -5
- package/src/utils/adt-http.js +7 -7
- package/src/utils/debug-daemon.js +22 -0
- package/src/utils/debug-repl.js +23 -0
- package/src/utils/debug-session.js +88 -18
- package/src/utils/transport-selector.js +289 -0
- package/src/utils/version-check.js +4 -3
package/src/config.js
CHANGED
|
@@ -34,7 +34,8 @@ function loadConfig() {
|
|
|
34
34
|
language: process.env.ABAP_LANGUAGE || 'EN',
|
|
35
35
|
gitUsername: process.env.GIT_USERNAME,
|
|
36
36
|
gitPassword: process.env.GIT_PASSWORD,
|
|
37
|
-
transport: process.env.ABAP_TRANSPORT
|
|
37
|
+
transport: process.env.ABAP_TRANSPORT,
|
|
38
|
+
protocol: process.env.ABAP_PROTOCOL || 'https'
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -51,7 +52,8 @@ function getAbapConfig() {
|
|
|
51
52
|
password: cfg.password,
|
|
52
53
|
language: cfg.language || 'EN',
|
|
53
54
|
gitUsername: cfg.gitUsername || process.env.GIT_USERNAME,
|
|
54
|
-
gitPassword: cfg.gitPassword || process.env.GIT_PASSWORD
|
|
55
|
+
gitPassword: cfg.gitPassword || process.env.GIT_PASSWORD,
|
|
56
|
+
protocol: cfg.protocol || process.env.ABAP_PROTOCOL || 'https'
|
|
55
57
|
};
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -141,6 +143,62 @@ function getProjectInfo() {
|
|
|
141
143
|
return projectConfig?.project || null;
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Get conflict detection configuration from project-level config
|
|
148
|
+
* Precedence: CLI flag > project config > default ('abort')
|
|
149
|
+
* @returns {Object} Conflict detection config with mode and reason
|
|
150
|
+
*/
|
|
151
|
+
function getConflictSettings() {
|
|
152
|
+
const projectConfig = loadProjectConfig();
|
|
153
|
+
|
|
154
|
+
if (projectConfig?.conflictDetection) {
|
|
155
|
+
const validModes = ['ignore', 'abort'];
|
|
156
|
+
const mode = projectConfig.conflictDetection.mode;
|
|
157
|
+
if (mode && !validModes.includes(mode)) {
|
|
158
|
+
console.warn(`⚠️ Warning: Invalid conflictDetection.mode '${mode}' in .abapgit-agent.json. Must be one of: ${validModes.join(', ')}. Falling back to 'abort'.`);
|
|
159
|
+
return { mode: 'abort', reason: null };
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
mode: mode || 'abort',
|
|
163
|
+
reason: projectConfig.conflictDetection.reason || null
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Default: abort (conflict detection on by default)
|
|
168
|
+
return { mode: 'abort', reason: null };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get transport hook configuration from project-level config
|
|
173
|
+
* @returns {{ hook: string|null, description: string|null }}
|
|
174
|
+
*/
|
|
175
|
+
function getTransportHookConfig() {
|
|
176
|
+
const projectConfig = loadProjectConfig();
|
|
177
|
+
if (projectConfig?.transports?.hook) {
|
|
178
|
+
return {
|
|
179
|
+
hook: projectConfig.transports.hook.path || null,
|
|
180
|
+
description: projectConfig.transports.hook.description || null
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return { hook: null, description: null };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get transport operation settings from project-level config
|
|
188
|
+
* @returns {{ allowCreate: boolean, allowRelease: boolean, reason: string|null }}
|
|
189
|
+
*/
|
|
190
|
+
function getTransportSettings() {
|
|
191
|
+
const projectConfig = loadProjectConfig();
|
|
192
|
+
if (projectConfig?.transports) {
|
|
193
|
+
return {
|
|
194
|
+
allowCreate: projectConfig.transports.allowCreate !== false,
|
|
195
|
+
allowRelease: projectConfig.transports.allowRelease !== false,
|
|
196
|
+
reason: projectConfig.transports.reason || null
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return { allowCreate: true, allowRelease: true, reason: null };
|
|
200
|
+
}
|
|
201
|
+
|
|
144
202
|
module.exports = {
|
|
145
203
|
loadConfig,
|
|
146
204
|
getAbapConfig,
|
|
@@ -150,5 +208,8 @@ module.exports = {
|
|
|
150
208
|
getWorkflowConfig,
|
|
151
209
|
getSafeguards,
|
|
152
210
|
getProjectInfo,
|
|
153
|
-
|
|
211
|
+
getConflictSettings,
|
|
212
|
+
loadProjectConfig,
|
|
213
|
+
getTransportHookConfig,
|
|
214
|
+
getTransportSettings
|
|
154
215
|
};
|
package/src/utils/abap-http.js
CHANGED
|
@@ -129,7 +129,7 @@ class AbapHttp {
|
|
|
129
129
|
* @returns {Promise<string>} CSRF token
|
|
130
130
|
*/
|
|
131
131
|
async fetchCsrfToken() {
|
|
132
|
-
const url = new URL(`/sap/bc/z_abapgit_agent/health`,
|
|
132
|
+
const url = new URL(`/sap/bc/z_abapgit_agent/health`, `${this.config.protocol || 'https'}://${this.config.host}:${this.config.sapport}`);
|
|
133
133
|
|
|
134
134
|
return new Promise((resolve, reject) => {
|
|
135
135
|
const options = {
|
|
@@ -144,10 +144,10 @@ class AbapHttp {
|
|
|
144
144
|
'X-CSRF-Token': 'fetch',
|
|
145
145
|
'Content-Type': 'application/json'
|
|
146
146
|
},
|
|
147
|
-
agent: new https.Agent({ rejectUnauthorized: false })
|
|
147
|
+
agent: this.config.protocol === 'http' ? undefined : new https.Agent({ rejectUnauthorized: false })
|
|
148
148
|
};
|
|
149
149
|
|
|
150
|
-
const req = https.request(options, (res) => {
|
|
150
|
+
const req = (this.config.protocol === 'http' ? http : https).request(options, (res) => {
|
|
151
151
|
const csrfToken = res.headers['x-csrf-token'];
|
|
152
152
|
|
|
153
153
|
// Save cookies from response
|
|
@@ -222,7 +222,7 @@ class AbapHttp {
|
|
|
222
222
|
*/
|
|
223
223
|
async _makeRequest(method, urlPath, data = null, options = {}) {
|
|
224
224
|
return new Promise((resolve, reject) => {
|
|
225
|
-
const url = new URL(urlPath,
|
|
225
|
+
const url = new URL(urlPath, `${this.config.protocol || 'https'}://${this.config.host}:${this.config.sapport}`);
|
|
226
226
|
|
|
227
227
|
const headers = {
|
|
228
228
|
'Content-Type': 'application/json',
|
|
@@ -250,7 +250,7 @@ class AbapHttp {
|
|
|
250
250
|
path: url.pathname + url.search,
|
|
251
251
|
method,
|
|
252
252
|
headers,
|
|
253
|
-
agent: new https.Agent({ rejectUnauthorized: false })
|
|
253
|
+
agent: this.config.protocol === 'http' ? undefined : new https.Agent({ rejectUnauthorized: false })
|
|
254
254
|
};
|
|
255
255
|
|
|
256
256
|
const req = (url.protocol === 'https:' ? https : http).request(reqOptions, (res) => {
|
package/src/utils/adt-http.js
CHANGED
|
@@ -75,7 +75,7 @@ class AdtHttp {
|
|
|
75
75
|
*/
|
|
76
76
|
async fetchCsrfToken() {
|
|
77
77
|
return new Promise((resolve, reject) => {
|
|
78
|
-
const url = new URL('/sap/bc/adt/discovery',
|
|
78
|
+
const url = new URL('/sap/bc/adt/discovery', `${this.config.protocol || 'https'}://${this.config.host}:${this.config.sapport}`);
|
|
79
79
|
const options = {
|
|
80
80
|
hostname: url.hostname,
|
|
81
81
|
port: url.port,
|
|
@@ -88,10 +88,10 @@ class AdtHttp {
|
|
|
88
88
|
'X-CSRF-Token': 'fetch',
|
|
89
89
|
'Accept': 'application/atomsvc+xml'
|
|
90
90
|
},
|
|
91
|
-
agent: new https.Agent({ rejectUnauthorized: false })
|
|
91
|
+
agent: this.config.protocol === 'http' ? undefined : new https.Agent({ rejectUnauthorized: false })
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
-
const req = https.request(options, (res) => {
|
|
94
|
+
const req = (this.config.protocol === 'http' ? http : https).request(options, (res) => {
|
|
95
95
|
const csrfToken = res.headers['x-csrf-token'];
|
|
96
96
|
const setCookie = res.headers['set-cookie'];
|
|
97
97
|
if (setCookie) {
|
|
@@ -140,7 +140,7 @@ class AdtHttp {
|
|
|
140
140
|
|
|
141
141
|
async _makeRequest(method, urlPath, body = null, options = {}) {
|
|
142
142
|
return new Promise((resolve, reject) => {
|
|
143
|
-
const url = new URL(urlPath,
|
|
143
|
+
const url = new URL(urlPath, `${this.config.protocol || 'https'}://${this.config.host}:${this.config.sapport}`);
|
|
144
144
|
|
|
145
145
|
const headers = {
|
|
146
146
|
'Content-Type': options.contentType || 'application/atom+xml',
|
|
@@ -169,7 +169,7 @@ class AdtHttp {
|
|
|
169
169
|
path: url.pathname + url.search,
|
|
170
170
|
method,
|
|
171
171
|
headers,
|
|
172
|
-
agent: new https.Agent({ rejectUnauthorized: false })
|
|
172
|
+
agent: this.config.protocol === 'http' ? undefined : new https.Agent({ rejectUnauthorized: false })
|
|
173
173
|
};
|
|
174
174
|
|
|
175
175
|
const req = (url.protocol === 'https:' ? https : http).request(reqOptions, (res) => {
|
|
@@ -272,7 +272,7 @@ class AdtHttp {
|
|
|
272
272
|
*/
|
|
273
273
|
async postFire(urlPath, body = null, options = {}) {
|
|
274
274
|
return new Promise((resolve, reject) => {
|
|
275
|
-
const url = new URL(urlPath,
|
|
275
|
+
const url = new URL(urlPath, `${this.config.protocol || 'https'}://${this.config.host}:${this.config.sapport}`);
|
|
276
276
|
|
|
277
277
|
const headers = {
|
|
278
278
|
'Content-Type': options.contentType || 'application/atom+xml',
|
|
@@ -301,7 +301,7 @@ class AdtHttp {
|
|
|
301
301
|
path: url.pathname + url.search,
|
|
302
302
|
method: 'POST',
|
|
303
303
|
headers,
|
|
304
|
-
agent: new https.Agent({ rejectUnauthorized: false })
|
|
304
|
+
agent: this.config.protocol === 'http' ? undefined : new https.Agent({ rejectUnauthorized: false })
|
|
305
305
|
};
|
|
306
306
|
|
|
307
307
|
const req = (url.protocol === 'https:' ? https : http).request(reqOptions, (_res) => {
|
|
@@ -67,6 +67,17 @@ async function startDaemon(config, sessionId, socketPath, snapshot) {
|
|
|
67
67
|
|
|
68
68
|
const session = new DebugSession(adt, sessionId);
|
|
69
69
|
|
|
70
|
+
// Pin the SAP_SESSIONID so _restorePinnedSession() works inside the daemon.
|
|
71
|
+
// DebugSession.attach() normally sets pinnedSessionId, but the daemon skips
|
|
72
|
+
// attach() and reconstructs the session from the snapshot. Without this,
|
|
73
|
+
// any CSRF refresh that AdtHttp performs internally (401/403 retry → HEAD
|
|
74
|
+
// request → new Set-Cookie) silently overwrites the session cookie and routes
|
|
75
|
+
// subsequent IPC calls to the wrong ABAP work process → HTTP 400.
|
|
76
|
+
if (snapshot && snapshot.cookies) {
|
|
77
|
+
const m = snapshot.cookies.match(/SAP_SESSIONID=([^;]*)/);
|
|
78
|
+
if (m) session.pinnedSessionId = m[1];
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
// Remove stale socket file from a previous crash
|
|
71
82
|
try { fs.unlinkSync(socketPath); } catch (e) { /* ignore ENOENT */ }
|
|
72
83
|
|
|
@@ -85,6 +96,17 @@ async function startDaemon(config, sessionId, socketPath, snapshot) {
|
|
|
85
96
|
process.exit(code || 0);
|
|
86
97
|
}
|
|
87
98
|
|
|
99
|
+
// On SIGTERM (e.g. pkill from ensure_breakpoint cleanup), attempt to release
|
|
100
|
+
// the frozen ABAP work process before exiting. Without this, killing the
|
|
101
|
+
// daemon leaves the work process paused at the breakpoint until SAP's own
|
|
102
|
+
// session-timeout fires (up to several minutes).
|
|
103
|
+
process.once('SIGTERM', async () => {
|
|
104
|
+
try {
|
|
105
|
+
await session.terminate();
|
|
106
|
+
} catch (e) { /* ignore — best effort */ }
|
|
107
|
+
cleanupAndExit(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
88
110
|
const server = net.createServer((socket) => {
|
|
89
111
|
resetIdle();
|
|
90
112
|
let buf = '';
|
package/src/utils/debug-repl.js
CHANGED
|
@@ -76,6 +76,11 @@ async function startRepl(session, initialState, onBeforeExit) {
|
|
|
76
76
|
|
|
77
77
|
renderState(position, source, variables);
|
|
78
78
|
|
|
79
|
+
// Keep the ADT stateful session alive with periodic getStack() pings.
|
|
80
|
+
// SAP's ICM drops session affinity after ~60 s of idle, causing stepContinue
|
|
81
|
+
// to route to the wrong work process (HTTP 400) when the user eventually quits.
|
|
82
|
+
session.startKeepalive();
|
|
83
|
+
|
|
79
84
|
const rl = readline.createInterface({
|
|
80
85
|
input: process.stdin,
|
|
81
86
|
output: process.stdout,
|
|
@@ -178,6 +183,7 @@ async function startRepl(session, initialState, onBeforeExit) {
|
|
|
178
183
|
|
|
179
184
|
} else if (cmd === 'q' || cmd === 'quit') {
|
|
180
185
|
console.log('\n Detaching debugger — program will continue running...');
|
|
186
|
+
session.stopKeepalive();
|
|
181
187
|
try {
|
|
182
188
|
await session.detach();
|
|
183
189
|
} catch (e) {
|
|
@@ -190,6 +196,7 @@ async function startRepl(session, initialState, onBeforeExit) {
|
|
|
190
196
|
|
|
191
197
|
} else if (cmd === 'kill') {
|
|
192
198
|
console.log('\n Terminating program (hard abort)...');
|
|
199
|
+
session.stopKeepalive();
|
|
193
200
|
try {
|
|
194
201
|
await session.terminate();
|
|
195
202
|
} catch (e) {
|
|
@@ -213,7 +220,23 @@ async function startRepl(session, initialState, onBeforeExit) {
|
|
|
213
220
|
});
|
|
214
221
|
|
|
215
222
|
rl.on('close', async () => {
|
|
223
|
+
// stdin closed (EOF or Ctrl+D) without an explicit 'q' or 'kill' command.
|
|
224
|
+
// Detach so the ABAP work process is released — without this the WP stays
|
|
225
|
+
// frozen in SM50 (e.g. when attach is started with </dev/null stdin and
|
|
226
|
+
// readline gets immediate EOF before the user can type a command).
|
|
227
|
+
if (!exitCleanupDone) {
|
|
228
|
+
session.stopKeepalive();
|
|
229
|
+
try { await session.detach(); } catch (e) { /* ignore */ }
|
|
230
|
+
}
|
|
216
231
|
await runExitCleanup();
|
|
232
|
+
// Drain stdout/stderr before exiting so the OS TCP stack has flushed the
|
|
233
|
+
// outbound stepContinue request bytes. process.exit() tears down file
|
|
234
|
+
// descriptors immediately — without this the WP can stay frozen if the
|
|
235
|
+
// socket buffer hasn't been written to the NIC yet.
|
|
236
|
+
await new Promise(resolve => {
|
|
237
|
+
if (process.stdout.writableEnded) return resolve();
|
|
238
|
+
process.stdout.write('', resolve);
|
|
239
|
+
});
|
|
217
240
|
process.exit(0);
|
|
218
241
|
});
|
|
219
242
|
|
|
@@ -38,7 +38,7 @@ const STATEFUL_HEADER = { 'X-sap-adt-sessiontype': 'stateful' };
|
|
|
38
38
|
* @param {number} maxRetries - Max additional attempts after the first (default 3)
|
|
39
39
|
* @param {number} delayMs - Wait between retries in ms (default 1000)
|
|
40
40
|
*/
|
|
41
|
-
async function retryOnIcmError(fn, maxRetries =
|
|
41
|
+
async function retryOnIcmError(fn, maxRetries = 12, delayMs = 2000) {
|
|
42
42
|
let lastErr;
|
|
43
43
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
44
44
|
try {
|
|
@@ -64,6 +64,56 @@ class DebugSession {
|
|
|
64
64
|
constructor(adtHttp, sessionId) {
|
|
65
65
|
this.http = adtHttp;
|
|
66
66
|
this.sessionId = sessionId;
|
|
67
|
+
this.pinnedSessionId = null;
|
|
68
|
+
this._keepaliveTimer = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Start a periodic keepalive that pings ADT every 30 seconds.
|
|
73
|
+
* SAP's ICM drops stateful session affinity after ~60 s of idle, causing
|
|
74
|
+
* subsequent debug requests to route to the wrong work process (HTTP 400).
|
|
75
|
+
* Calling getStack() regularly keeps the connection warm.
|
|
76
|
+
* Call stopKeepalive() before detach/terminate to avoid racing the close.
|
|
77
|
+
*/
|
|
78
|
+
startKeepalive() {
|
|
79
|
+
if (this._keepaliveTimer) return;
|
|
80
|
+
this._keepaliveTimer = setInterval(async () => {
|
|
81
|
+
try { await this.getStack(); } catch (e) { /* best-effort */ }
|
|
82
|
+
}, 30000);
|
|
83
|
+
if (this._keepaliveTimer.unref) this._keepaliveTimer.unref();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
stopKeepalive() {
|
|
87
|
+
if (this._keepaliveTimer) {
|
|
88
|
+
clearInterval(this._keepaliveTimer);
|
|
89
|
+
this._keepaliveTimer = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Restore the pinned SAP_SESSIONID into the HTTP client's cookie jar.
|
|
95
|
+
*
|
|
96
|
+
* SAP ADT debug sessions are bound to a specific frozen ABAP work process
|
|
97
|
+
* via the SAP_SESSIONID cookie. AdtHttp updates this cookie automatically
|
|
98
|
+
* from every response's Set-Cookie header and replaces it on CSRF refresh
|
|
99
|
+
* (401/403 retry). When the cookie rotates mid-session the next request
|
|
100
|
+
* routes to a different ABAP session that has no debug state, causing
|
|
101
|
+
* HTTP 400 "Service cannot be reached".
|
|
102
|
+
*
|
|
103
|
+
* This method reverts any rotation that occurred since attach() by replacing
|
|
104
|
+
* the current SAP_SESSIONID value with the one captured at attach time.
|
|
105
|
+
* It is a no-op when called before attach() (pinnedSessionId is null).
|
|
106
|
+
*/
|
|
107
|
+
_restorePinnedSession() {
|
|
108
|
+
if (!this.pinnedSessionId || !this.http.cookies) return;
|
|
109
|
+
|
|
110
|
+
// Replace whatever SAP_SESSIONID= value is currently in the cookie jar
|
|
111
|
+
// with the pinned one. The cookie jar is a semicolon-separated string,
|
|
112
|
+
// e.g. "SAP_SESSIONID=ABC123; sap-usercontext=xyz".
|
|
113
|
+
this.http.cookies = this.http.cookies.replace(
|
|
114
|
+
/SAP_SESSIONID=[^;]*/,
|
|
115
|
+
`SAP_SESSIONID=${this.pinnedSessionId}`
|
|
116
|
+
);
|
|
67
117
|
}
|
|
68
118
|
|
|
69
119
|
/**
|
|
@@ -95,6 +145,14 @@ class DebugSession {
|
|
|
95
145
|
this.sessionId = debugSessionId;
|
|
96
146
|
}
|
|
97
147
|
|
|
148
|
+
// Pin the SAP_SESSIONID cookie that was active when we attached.
|
|
149
|
+
// All subsequent stateful operations must present this exact cookie so
|
|
150
|
+
// that SAP routes them to the same frozen ABAP work process.
|
|
151
|
+
if (this.http.cookies) {
|
|
152
|
+
const match = this.http.cookies.match(/SAP_SESSIONID=([^;]*)/);
|
|
153
|
+
if (match) this.pinnedSessionId = match[1];
|
|
154
|
+
}
|
|
155
|
+
|
|
98
156
|
return this.sessionId;
|
|
99
157
|
}
|
|
100
158
|
|
|
@@ -126,6 +184,7 @@ class DebugSession {
|
|
|
126
184
|
// (no suspended session left). Treat both 200 and 500 as "continued".
|
|
127
185
|
if (method === 'stepContinue') {
|
|
128
186
|
return retryOnIcmError(async () => {
|
|
187
|
+
this._restorePinnedSession();
|
|
129
188
|
try {
|
|
130
189
|
await this.http.post(`/sap/bc/adt/debugger?method=${method}`, '', {
|
|
131
190
|
contentType: 'application/vnd.sap.as+xml',
|
|
@@ -146,6 +205,7 @@ class DebugSession {
|
|
|
146
205
|
}
|
|
147
206
|
|
|
148
207
|
return retryOnIcmError(async () => {
|
|
208
|
+
this._restorePinnedSession();
|
|
149
209
|
await this.http.post(`/sap/bc/adt/debugger?method=${method}`, '', {
|
|
150
210
|
contentType: 'application/vnd.sap.as+xml',
|
|
151
211
|
headers: { ...STATEFUL_HEADER, 'Accept': 'application/xml' }
|
|
@@ -173,6 +233,8 @@ class DebugSession {
|
|
|
173
233
|
* @returns {Promise<Array<{ name: string, type: string, value: string }>>}
|
|
174
234
|
*/
|
|
175
235
|
async getVariables(name = null) {
|
|
236
|
+
this._restorePinnedSession();
|
|
237
|
+
|
|
176
238
|
const CT_CHILD = 'application/vnd.sap.as+xml; charset=UTF-8; dataname=com.sap.adt.debugger.ChildVariables';
|
|
177
239
|
const CT_VARS = 'application/vnd.sap.as+xml; charset=UTF-8; dataname=com.sap.adt.debugger.Variables';
|
|
178
240
|
|
|
@@ -254,6 +316,8 @@ class DebugSession {
|
|
|
254
316
|
* @returns {Promise<Array<{ id: string, name: string, type: string, value: string }>>}
|
|
255
317
|
*/
|
|
256
318
|
async getVariableChildren(parentId, meta = {}) {
|
|
319
|
+
this._restorePinnedSession();
|
|
320
|
+
|
|
257
321
|
const CT_VARS = 'application/vnd.sap.as+xml; charset=UTF-8; dataname=com.sap.adt.debugger.Variables';
|
|
258
322
|
const CT_CHILD = 'application/vnd.sap.as+xml; charset=UTF-8; dataname=com.sap.adt.debugger.ChildVariables';
|
|
259
323
|
|
|
@@ -372,6 +436,7 @@ class DebugSession {
|
|
|
372
436
|
*/
|
|
373
437
|
async getStack() {
|
|
374
438
|
return retryOnIcmError(async () => {
|
|
439
|
+
this._restorePinnedSession();
|
|
375
440
|
// Try newer dedicated stack endpoint first (abap-adt-api v7+ approach)
|
|
376
441
|
try {
|
|
377
442
|
const { body } = await this.http.get(
|
|
@@ -383,9 +448,8 @@ class DebugSession {
|
|
|
383
448
|
const frames = parseStack(body);
|
|
384
449
|
if (frames.length > 0) return frames;
|
|
385
450
|
} catch (e) {
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
// Otherwise fall through to POST approach
|
|
451
|
+
// Fall through to POST approach for any GET failure (including 400 on systems
|
|
452
|
+
// that don't support the dedicated /debugger/stack endpoint)
|
|
389
453
|
}
|
|
390
454
|
// Fallback: POST approach (older ADT versions)
|
|
391
455
|
const { body } = await this.http.post(
|
|
@@ -583,32 +647,38 @@ class DebugSession {
|
|
|
583
647
|
|
|
584
648
|
/**
|
|
585
649
|
* Terminate the debug session.
|
|
650
|
+
* Retries on transient ICM 400 errors so the ABAP work process is reliably
|
|
651
|
+
* released even when the system is under load (e.g. during test:all).
|
|
586
652
|
*/
|
|
587
653
|
async terminate() {
|
|
588
|
-
await
|
|
589
|
-
|
|
590
|
-
|
|
654
|
+
await retryOnIcmError(async () => {
|
|
655
|
+
this._restorePinnedSession();
|
|
656
|
+
await this.http.post('/sap/bc/adt/debugger?method=terminateDebuggee', '', {
|
|
657
|
+
contentType: 'application/vnd.sap.as+xml',
|
|
658
|
+
headers: STATEFUL_HEADER
|
|
659
|
+
});
|
|
591
660
|
});
|
|
592
661
|
}
|
|
593
662
|
|
|
594
663
|
/**
|
|
595
664
|
* Detach from the debuggee without killing it.
|
|
596
|
-
* Issues a stepContinue so the ABAP program resumes running.
|
|
665
|
+
* Issues a single stepContinue so the ABAP program resumes running.
|
|
666
|
+
*
|
|
667
|
+
* ADT returns HTTP 200 when the WP resumes (regardless of whether it
|
|
668
|
+
* later hits another breakpoint — there is no way to distinguish "still
|
|
669
|
+
* running" from "re-hit breakpoint" in the stepContinue response alone).
|
|
670
|
+
* Sending a second stepContinue to an already-running WP races with the
|
|
671
|
+
* program's own execution and can stall the WP mid-run (e.g. while a
|
|
672
|
+
* Code Inspector job is in flight), so we issue exactly one request.
|
|
597
673
|
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
* We use postFire() which resolves as soon as the request bytes are
|
|
601
|
-
* flushed to the TCP send buffer — no need to wait for a response.
|
|
602
|
-
* The existing session cookies are used so ADT recognises the request.
|
|
674
|
+
* Callers (e.g. the REPL 'q' handler) must delete all breakpoints before
|
|
675
|
+
* calling detach() to prevent an immediate re-hit on the same line.
|
|
603
676
|
*/
|
|
604
677
|
async detach() {
|
|
605
678
|
try {
|
|
606
|
-
await this.
|
|
607
|
-
contentType: 'application/vnd.sap.as+xml',
|
|
608
|
-
headers: STATEFUL_HEADER
|
|
609
|
-
});
|
|
679
|
+
await this.step('stepContinue');
|
|
610
680
|
} catch (e) {
|
|
611
|
-
// Ignore —
|
|
681
|
+
// Ignore — session may have already closed.
|
|
612
682
|
}
|
|
613
683
|
}
|
|
614
684
|
}
|