abapgit-agent 1.10.0 → 1.11.0
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/CLAUDE.md +71 -35
- package/bin/abapgit-agent +14 -4
- package/package.json +3 -1
- package/src/commands/debug.js +3 -3
- package/src/commands/pull.js +95 -13
- package/src/commands/status.js +2 -0
- package/src/commands/transport.js +290 -0
- package/src/config.js +60 -1
- package/src/utils/adt-http.js +30 -5
- package/src/utils/debug-daemon.js +24 -1
- package/src/utils/debug-repl.js +23 -0
- package/src/utils/debug-session.js +162 -40
- package/src/utils/transport-selector.js +289 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport command - List and manage SAP transport requests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const VALID_SCOPES = ['mine', 'task', 'all'];
|
|
6
|
+
const VALID_SUBCOMMANDS = ['list', 'create', 'check', 'release'];
|
|
7
|
+
const VALID_TYPES = ['workbench', 'customizing'];
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
name: 'transport',
|
|
11
|
+
description: 'List and manage SAP transport requests',
|
|
12
|
+
requiresAbapConfig: true,
|
|
13
|
+
requiresVersionCheck: false,
|
|
14
|
+
|
|
15
|
+
async execute(args, context) {
|
|
16
|
+
const { loadConfig, AbapHttp, getTransportSettings } = context;
|
|
17
|
+
|
|
18
|
+
const jsonOutput = args.includes('--json');
|
|
19
|
+
|
|
20
|
+
// Determine subcommand (first positional arg, default to 'list')
|
|
21
|
+
const subcommand = args[0] && !args[0].startsWith('-') ? args[0] : 'list';
|
|
22
|
+
|
|
23
|
+
if (!VALID_SUBCOMMANDS.includes(subcommand)) {
|
|
24
|
+
console.error(`❌ Error: Unknown subcommand '${subcommand}'. Use: ${VALID_SUBCOMMANDS.join(', ')}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check project-level transport settings
|
|
29
|
+
const transportSettings = getTransportSettings();
|
|
30
|
+
|
|
31
|
+
if (subcommand === 'create' && !transportSettings.allowCreate) {
|
|
32
|
+
console.error(`❌ transport create is disabled for this project.`);
|
|
33
|
+
if (transportSettings.reason) console.error(` Reason: ${transportSettings.reason}`);
|
|
34
|
+
console.error(` This safeguard is configured in .abapgit-agent.json`);
|
|
35
|
+
const err = new Error('transport create disabled');
|
|
36
|
+
err._isTransportError = true;
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (subcommand === 'release' && !transportSettings.allowRelease) {
|
|
41
|
+
console.error(`❌ transport release is disabled for this project.`);
|
|
42
|
+
if (transportSettings.reason) console.error(` Reason: ${transportSettings.reason}`);
|
|
43
|
+
console.error(` This safeguard is configured in .abapgit-agent.json`);
|
|
44
|
+
const err = new Error('transport release disabled');
|
|
45
|
+
err._isTransportError = true;
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const config = loadConfig();
|
|
50
|
+
const http = new AbapHttp(config);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
switch (subcommand) {
|
|
54
|
+
case 'list':
|
|
55
|
+
await this._list(args, http, jsonOutput);
|
|
56
|
+
break;
|
|
57
|
+
case 'create':
|
|
58
|
+
await this._create(args, http, jsonOutput);
|
|
59
|
+
break;
|
|
60
|
+
case 'check':
|
|
61
|
+
await this._check(args, http, jsonOutput);
|
|
62
|
+
break;
|
|
63
|
+
case 'release':
|
|
64
|
+
await this._release(args, http, jsonOutput);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(`❌ Error: ${error.message}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async _list(args, http, jsonOutput) {
|
|
74
|
+
const scopeIdx = args.indexOf('--scope');
|
|
75
|
+
const scope = scopeIdx !== -1 ? args[scopeIdx + 1] : 'mine';
|
|
76
|
+
|
|
77
|
+
if (!VALID_SCOPES.includes(scope)) {
|
|
78
|
+
console.error(`❌ Error: Invalid scope '${scope}'. Valid values: ${VALID_SCOPES.join(', ')}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = await http.get(`/sap/bc/z_abapgit_agent/transport?scope=${scope}`);
|
|
83
|
+
|
|
84
|
+
if (jsonOutput) {
|
|
85
|
+
console.log(JSON.stringify(result, null, 2));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const transports = result.TRANSPORTS || result.transports || [];
|
|
90
|
+
const scopeLabel = { mine: 'mine', task: 'task — where I own or have a task', all: 'all' }[scope];
|
|
91
|
+
|
|
92
|
+
console.log(`\n📋 Open Transport Requests (${scopeLabel})\n`);
|
|
93
|
+
|
|
94
|
+
if (transports.length === 0) {
|
|
95
|
+
console.log(' No open transport requests found.');
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log(' To create one: abapgit-agent transport create');
|
|
98
|
+
console.log(' To see more: abapgit-agent transport list --scope task');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Table header
|
|
103
|
+
const numW = 2;
|
|
104
|
+
const trW = 12;
|
|
105
|
+
const descW = 33;
|
|
106
|
+
const ownerW = 12;
|
|
107
|
+
const dateW = 10;
|
|
108
|
+
|
|
109
|
+
const pad = (s, w) => String(s || '').substring(0, w).padEnd(w);
|
|
110
|
+
const header = ` ${'#'.padEnd(numW)} ${pad('Number', trW)} ${pad('Description', descW)} ${pad('Owner', ownerW)} ${'Date'}`;
|
|
111
|
+
const divider = ` ${'─'.repeat(numW)} ${'─'.repeat(trW)} ${'─'.repeat(descW)} ${'─'.repeat(ownerW)} ${'─'.repeat(dateW)}`;
|
|
112
|
+
|
|
113
|
+
console.log(header);
|
|
114
|
+
console.log(divider);
|
|
115
|
+
|
|
116
|
+
transports.forEach((t, i) => {
|
|
117
|
+
const num = t.NUMBER || t.number || '';
|
|
118
|
+
const desc = t.DESCRIPTION || t.description || '';
|
|
119
|
+
const owner = t.OWNER || t.owner || '';
|
|
120
|
+
const date = t.DATE || t.date || '';
|
|
121
|
+
console.log(` ${String(i + 1).padEnd(numW)} ${pad(num, trW)} ${pad(desc, descW)} ${pad(owner, ownerW)} ${date}`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
console.log('');
|
|
125
|
+
console.log(` ${transports.length} transport(s) found.`);
|
|
126
|
+
console.log('');
|
|
127
|
+
console.log(` To use one: abapgit-agent pull --transport ${(transports[0].NUMBER || transports[0].number || 'DEVK900001')}`);
|
|
128
|
+
|
|
129
|
+
if (scope === 'mine') {
|
|
130
|
+
console.log(' To switch scope:');
|
|
131
|
+
console.log(' transport list --scope task transports where I have a task');
|
|
132
|
+
console.log(' transport list --scope all all open transports');
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
async _create(args, http, jsonOutput) {
|
|
137
|
+
const descIdx = args.indexOf('--description');
|
|
138
|
+
let description = descIdx !== -1 ? args[descIdx + 1] : null;
|
|
139
|
+
|
|
140
|
+
const typeIdx = args.indexOf('--type');
|
|
141
|
+
const type = typeIdx !== -1 ? args[typeIdx + 1] : 'workbench';
|
|
142
|
+
|
|
143
|
+
if (!VALID_TYPES.includes(type)) {
|
|
144
|
+
console.error(`❌ Error: Invalid type '${type}'. Valid values: ${VALID_TYPES.join(', ')}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Prompt for description if not provided and running in TTY
|
|
149
|
+
if (!description && !jsonOutput && process.stdin.isTTY) {
|
|
150
|
+
description = await this._prompt('Description: ');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const csrfToken = await http.fetchCsrfToken();
|
|
154
|
+
const result = await http.post(
|
|
155
|
+
'/sap/bc/z_abapgit_agent/transport',
|
|
156
|
+
{ action: 'CREATE', description: description || '', type },
|
|
157
|
+
{ csrfToken }
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (jsonOutput) {
|
|
161
|
+
console.log(JSON.stringify(result, null, 2));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const number = result.NUMBER || result.number;
|
|
166
|
+
const success = result.SUCCESS === true || result.success === true ||
|
|
167
|
+
result.SUCCESS === 'X' || result.success === 'X';
|
|
168
|
+
const typeLabel = type === 'customizing' ? 'Customizing' : 'Workbench';
|
|
169
|
+
|
|
170
|
+
if (success && number) {
|
|
171
|
+
console.log(`\n✅ Transport ${number} created (${typeLabel} request).\n`);
|
|
172
|
+
console.log(` To use it now: abapgit-agent pull --transport ${number}`);
|
|
173
|
+
} else {
|
|
174
|
+
const error = result.ERROR || result.error || result.MESSAGE || result.message || 'Could not create transport request';
|
|
175
|
+
console.error(`❌ Error: ${error}`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async _check(args, http, jsonOutput) {
|
|
181
|
+
const numIdx = args.indexOf('--number');
|
|
182
|
+
if (numIdx === -1 || !args[numIdx + 1]) {
|
|
183
|
+
console.error('❌ Error: --number is required for transport check');
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
const number = args[numIdx + 1];
|
|
187
|
+
|
|
188
|
+
const csrfToken = await http.fetchCsrfToken();
|
|
189
|
+
const result = await http.post(
|
|
190
|
+
'/sap/bc/z_abapgit_agent/transport',
|
|
191
|
+
{ action: 'CHECK', number },
|
|
192
|
+
{ csrfToken }
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (jsonOutput) {
|
|
196
|
+
console.log(JSON.stringify(result, null, 2));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const passed = result.PASSED === true || result.passed === true ||
|
|
201
|
+
result.PASSED === 'X' || result.passed === 'X';
|
|
202
|
+
const issues = result.ISSUES || result.issues || [];
|
|
203
|
+
const desc = result.DESCRIPTION || result.description || '';
|
|
204
|
+
const owner = result.OWNER || result.owner || '';
|
|
205
|
+
const date = result.DATE || result.date || '';
|
|
206
|
+
|
|
207
|
+
console.log(`\n🔍 Checking transport ${number}...`);
|
|
208
|
+
if (desc) console.log(`\n Description: ${desc}`);
|
|
209
|
+
if (owner) console.log(` Owner: ${owner}`);
|
|
210
|
+
if (date) console.log(` Date: ${date}`);
|
|
211
|
+
|
|
212
|
+
if (passed || issues.length === 0) {
|
|
213
|
+
console.log(`\n✅ Transport check passed — no issues found.`);
|
|
214
|
+
console.log(` Ready to release: abapgit-agent transport release --number ${number}`);
|
|
215
|
+
} else {
|
|
216
|
+
console.log(`\n⚠️ Transport check completed with warnings/errors:\n`);
|
|
217
|
+
|
|
218
|
+
const typeW = 6;
|
|
219
|
+
const objW = 21;
|
|
220
|
+
const msgW = 44;
|
|
221
|
+
const pad = (s, w) => String(s || '').substring(0, w).padEnd(w);
|
|
222
|
+
|
|
223
|
+
console.log(` ${'Type'.padEnd(typeW)} ${'Object'.padEnd(objW)} Message`);
|
|
224
|
+
console.log(` ${'─'.repeat(typeW)} ${'─'.repeat(objW)} ${'─'.repeat(msgW)}`);
|
|
225
|
+
|
|
226
|
+
for (const issue of issues) {
|
|
227
|
+
const type = issue.TYPE || issue.type || '';
|
|
228
|
+
const objType = issue.OBJ_TYPE || issue.obj_type || '';
|
|
229
|
+
const objName = issue.OBJ_NAME || issue.obj_name || '';
|
|
230
|
+
const text = issue.TEXT || issue.text || '';
|
|
231
|
+
const icon = type === 'E' ? '❌' : type === 'W' ? '⚠️ ' : 'ℹ️ ';
|
|
232
|
+
const obj = objType && objName ? `${objType} ${objName}` : objType || objName;
|
|
233
|
+
console.log(` ${icon.padEnd(typeW)} ${pad(obj, objW)} ${text}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log(`\n ${issues.length} issue(s) found. Fix before releasing.`);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async _release(args, http, jsonOutput) {
|
|
241
|
+
const numIdx = args.indexOf('--number');
|
|
242
|
+
if (numIdx === -1 || !args[numIdx + 1]) {
|
|
243
|
+
console.error('❌ Error: --number is required for transport release');
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
const number = args[numIdx + 1];
|
|
247
|
+
|
|
248
|
+
const csrfToken = await http.fetchCsrfToken();
|
|
249
|
+
const result = await http.post(
|
|
250
|
+
'/sap/bc/z_abapgit_agent/transport',
|
|
251
|
+
{ action: 'RELEASE', number },
|
|
252
|
+
{ csrfToken }
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (jsonOutput) {
|
|
256
|
+
console.log(JSON.stringify(result, null, 2));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const success = result.SUCCESS === true || result.success === true ||
|
|
261
|
+
result.SUCCESS === 'X' || result.success === 'X';
|
|
262
|
+
const message = result.MESSAGE || result.message || '';
|
|
263
|
+
const error = result.ERROR || result.error || '';
|
|
264
|
+
const desc = result.DESCRIPTION || result.description || '';
|
|
265
|
+
|
|
266
|
+
if (success) {
|
|
267
|
+
console.log(`\n🚀 Releasing transport ${number}...`);
|
|
268
|
+
if (desc) console.log(`\n Description: ${desc}`);
|
|
269
|
+
console.log(`\n✅ Transport ${number} released successfully.`);
|
|
270
|
+
} else {
|
|
271
|
+
console.error(`\n❌ Could not release transport ${number}.`);
|
|
272
|
+
if (error) {
|
|
273
|
+
console.error(`\n Error: ${error}`);
|
|
274
|
+
} else if (message) {
|
|
275
|
+
console.error(`\n Error: ${message}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
_prompt(question) {
|
|
281
|
+
const readline = require('readline');
|
|
282
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
283
|
+
return new Promise((resolve) => {
|
|
284
|
+
rl.question(question, (answer) => {
|
|
285
|
+
rl.close();
|
|
286
|
+
resolve(answer.trim());
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
};
|
package/src/config.js
CHANGED
|
@@ -141,6 +141,62 @@ function getProjectInfo() {
|
|
|
141
141
|
return projectConfig?.project || null;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Get conflict detection configuration from project-level config
|
|
146
|
+
* Precedence: CLI flag > project config > default ('abort')
|
|
147
|
+
* @returns {Object} Conflict detection config with mode and reason
|
|
148
|
+
*/
|
|
149
|
+
function getConflictSettings() {
|
|
150
|
+
const projectConfig = loadProjectConfig();
|
|
151
|
+
|
|
152
|
+
if (projectConfig?.conflictDetection) {
|
|
153
|
+
const validModes = ['ignore', 'abort'];
|
|
154
|
+
const mode = projectConfig.conflictDetection.mode;
|
|
155
|
+
if (mode && !validModes.includes(mode)) {
|
|
156
|
+
console.warn(`⚠️ Warning: Invalid conflictDetection.mode '${mode}' in .abapgit-agent.json. Must be one of: ${validModes.join(', ')}. Falling back to 'abort'.`);
|
|
157
|
+
return { mode: 'abort', reason: null };
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
mode: mode || 'abort',
|
|
161
|
+
reason: projectConfig.conflictDetection.reason || null
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Default: abort (conflict detection on by default)
|
|
166
|
+
return { mode: 'abort', reason: null };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get transport hook configuration from project-level config
|
|
171
|
+
* @returns {{ hook: string|null, description: string|null }}
|
|
172
|
+
*/
|
|
173
|
+
function getTransportHookConfig() {
|
|
174
|
+
const projectConfig = loadProjectConfig();
|
|
175
|
+
if (projectConfig?.transports?.hook) {
|
|
176
|
+
return {
|
|
177
|
+
hook: projectConfig.transports.hook.path || null,
|
|
178
|
+
description: projectConfig.transports.hook.description || null
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return { hook: null, description: null };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get transport operation settings from project-level config
|
|
186
|
+
* @returns {{ allowCreate: boolean, allowRelease: boolean, reason: string|null }}
|
|
187
|
+
*/
|
|
188
|
+
function getTransportSettings() {
|
|
189
|
+
const projectConfig = loadProjectConfig();
|
|
190
|
+
if (projectConfig?.transports) {
|
|
191
|
+
return {
|
|
192
|
+
allowCreate: projectConfig.transports.allowCreate !== false,
|
|
193
|
+
allowRelease: projectConfig.transports.allowRelease !== false,
|
|
194
|
+
reason: projectConfig.transports.reason || null
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return { allowCreate: true, allowRelease: true, reason: null };
|
|
198
|
+
}
|
|
199
|
+
|
|
144
200
|
module.exports = {
|
|
145
201
|
loadConfig,
|
|
146
202
|
getAbapConfig,
|
|
@@ -150,5 +206,8 @@ module.exports = {
|
|
|
150
206
|
getWorkflowConfig,
|
|
151
207
|
getSafeguards,
|
|
152
208
|
getProjectInfo,
|
|
153
|
-
|
|
209
|
+
getConflictSettings,
|
|
210
|
+
loadProjectConfig,
|
|
211
|
+
getTransportHookConfig,
|
|
212
|
+
getTransportSettings
|
|
154
213
|
};
|
package/src/utils/adt-http.js
CHANGED
|
@@ -199,12 +199,37 @@ class AdtHttp {
|
|
|
199
199
|
return;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
// Update cookies from any response that sets them
|
|
202
|
+
// Update cookies from any response that sets them.
|
|
203
|
+
// Merge by name so that updated values (e.g. SAP_SESSIONID) replace
|
|
204
|
+
// their old counterparts rather than accumulating duplicates.
|
|
205
|
+
// Duplicate SAP_SESSIONID cookies would cause the ICM to route requests
|
|
206
|
+
// to a stale work process ("Service cannot be reached").
|
|
203
207
|
if (res.headers['set-cookie']) {
|
|
204
|
-
const
|
|
205
|
-
? res.headers['set-cookie'].map(c => c.split(';')[0])
|
|
206
|
-
: res.headers['set-cookie'].split(';')[0];
|
|
207
|
-
|
|
208
|
+
const incoming = Array.isArray(res.headers['set-cookie'])
|
|
209
|
+
? res.headers['set-cookie'].map(c => c.split(';')[0])
|
|
210
|
+
: [res.headers['set-cookie'].split(';')[0]];
|
|
211
|
+
// Parse existing cookies into a Map (preserves insertion order)
|
|
212
|
+
const jar = new Map();
|
|
213
|
+
if (this.cookies) {
|
|
214
|
+
this.cookies.split(';').forEach(pair => {
|
|
215
|
+
const trimmed = pair.trim();
|
|
216
|
+
if (trimmed) {
|
|
217
|
+
const eq = trimmed.indexOf('=');
|
|
218
|
+
const k = eq === -1 ? trimmed : trimmed.slice(0, eq);
|
|
219
|
+
jar.set(k.trim(), trimmed);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
// Overwrite with incoming cookies
|
|
224
|
+
incoming.forEach(pair => {
|
|
225
|
+
const trimmed = pair.trim();
|
|
226
|
+
if (trimmed) {
|
|
227
|
+
const eq = trimmed.indexOf('=');
|
|
228
|
+
const k = eq === -1 ? trimmed : trimmed.slice(0, eq);
|
|
229
|
+
jar.set(k.trim(), trimmed);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
this.cookies = [...jar.values()].join('; ');
|
|
208
233
|
}
|
|
209
234
|
|
|
210
235
|
let respBody = '';
|
|
@@ -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 = '';
|
|
@@ -170,7 +192,8 @@ async function _handleLine(socket, line, session, cleanupAndExit, resetIdle) {
|
|
|
170
192
|
_send(socket, {
|
|
171
193
|
ok: false,
|
|
172
194
|
error: err.message || JSON.stringify(err),
|
|
173
|
-
statusCode: err.statusCode
|
|
195
|
+
statusCode: err.statusCode,
|
|
196
|
+
body: err.body || null
|
|
174
197
|
});
|
|
175
198
|
}
|
|
176
199
|
}
|
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
|
|