first commit
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s

This commit is contained in:
John
2026-09-23 17:31:02 +05:30
commit 69b4e68baf
51657 changed files with 3864077 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
'use strict';
// SAP B1 DI API — calls compiled C# console exe (SapDiConsole.exe).
// The exe uses standalone Company.Connect() — no SAP client needed.
// Build: cd services/SapDiConsole && build.bat
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const { DEFAULT_COMPANY } = require('./companyConfig');
const EXE_PATH = path.join(__dirname, 'SapDiConsole', 'bin', 'Release', 'net472', 'SapDiConsole.exe');
const PS_SCRIPT = path.join(__dirname, 'sap-di-pr.ps1'); // fallback if exe not built yet
function buildPayload(body, companyDB) {
// Host of the SAP B1 SLD/License services (same box as the SQL server here).
const licHost = process.env.SAP_B1_LICENSE_HOST || process.env.SQL_HOST || '192.9.205.132';
return {
Server: process.env.SQL_SERVER_NAME || 'SAP',
CompanyDB: companyDB || DEFAULT_COMPANY,
// SAP B1 10.0 DI API authenticates via the SLD/License service — required
// or Connect() returns -132 despite valid DB + SBO credentials.
LicenseServer: process.env.SAP_B1_LICENSE_SERVER || `${licHost}:30010`,
SLDServer: process.env.SAP_B1_SLD_SERVER || `${licHost}:40000`,
// DbServerType must match the SQL Server version. Server is SQL 2022 and
// the DI API (v10.00.331) exposes dst_MSSQL2022 = 17. NOTE: Program.cs
// currently hardcodes dst_MSSQL2022, so this payload value is only used if
// that line is switched back to the env-driven numeric cast. Override via env.
DbServerType: process.env.SAP_B1_DB_SERVER_TYPE || '17',
// DI API goes through the approval engine properly — no bypass issue — use manager directly
UserName: (process.env.SAP_B1_USER || 'manager').trim(),
Password: (process.env.SAP_B1_PASSWORD || '').trim(),
DbUserName: process.env.SQL_USER || 'sa',
DbPassword: process.env.SQL_PASSWORD || '',
...body,
};
}
// ════════════════════════════════════════════════════════════════
// PERSISTENT PROCESS POOL — one SapDiConsole.exe kept alive PER SAP
// company, reused across requests, instead of spawning a fresh process
// (and paying Company.Connect()'s ~30s SLD/License round trip) on every
// single call. This was the confirmed root cause of "creating a Purchase
// Request takes long" — measured live at ~33s per create before this.
// Protocol: newline-delimited JSON both ways — write one request line to
// the child's stdin, read one response line back from its stdout, in the
// same order (Program.cs processes requests strictly one at a time, so
// responses come back in submission order — no request ID needed).
// ════════════════════════════════════════════════════════════════
const _pool = {}; // companyDB → { child, resolvers: [{resolve,reject}], buffer }
function getProc(companyDB) {
const db = companyDB || DEFAULT_COMPANY;
const entry = _pool[db];
if (entry && !entry.child.killed && entry.child.exitCode === null) return entry;
console.log(`[DI-API] Spawning persistent connection for ${db}`);
const child = spawn(EXE_PATH, [], { stdio: ['pipe', 'pipe', 'pipe'] });
const fresh = { child, resolvers: [], buffer: '' };
child.stdout.on('data', (d) => {
fresh.buffer += d.toString();
let idx;
while ((idx = fresh.buffer.indexOf('\n')) >= 0) {
const line = fresh.buffer.slice(0, idx).trim();
fresh.buffer = fresh.buffer.slice(idx + 1);
if (!line) continue;
const waiter = fresh.resolvers.shift();
if (!waiter) continue; // unexpected extra line — ignore rather than crash
try { waiter.resolve(JSON.parse(line)); }
catch (e) { waiter.reject(new Error('DI API returned invalid JSON: ' + e.message)); }
}
});
child.stderr.on('data', (d) => process.stdout.write(d)); // stream DI-API progress lines to Node console
const fail = (err) => {
const pending = fresh.resolvers.splice(0);
pending.forEach((w) => w.reject(err));
if (_pool[db] === fresh) delete _pool[db]; // let the next call respawn
};
child.on('exit', (code) => fail(new Error(`DI API process for ${db} exited unexpectedly (code ${code})`)));
child.on('error', (err) => fail(new Error(`DI API process for ${db} failed to start: ${err.message}`)));
_pool[db] = fresh;
return fresh;
}
function sendToProc(companyDB, payload) {
const entry = getProc(companyDB);
return new Promise((resolve, reject) => {
entry.resolvers.push({ resolve, reject });
entry.child.stdin.write(JSON.stringify(payload) + '\n');
});
}
// Best-effort cleanup so a Node restart doesn't leave orphaned SAP sessions
// (and license seats) held by SapDiConsole.exe processes.
function shutdownPool() {
Object.values(_pool).forEach((entry) => {
try { entry.child.stdin.write(JSON.stringify({ Action: 'Shutdown' }) + '\n'); } catch (_e) {}
setTimeout(() => { try { entry.child.kill(); } catch (_e) {} }, 2000);
});
}
process.on('exit', shutdownPool);
process.on('SIGINT', () => { shutdownPool(); process.exit(0); });
process.on('SIGTERM', () => { shutdownPool(); process.exit(0); });
// One-shot fallback (no persistence) — only used when the compiled exe isn't
// built at all, matching the previous behavior exactly for that case.
function runProcessOnce(cmd, args, stdinData) {
return new Promise((resolve, reject) => {
const child = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
child.stdout.on('data', d => { stdout += d.toString(); });
child.stderr.on('data', d => {
stderr += d.toString();
process.stdout.write(d); // stream DI-API progress lines to Node console
});
child.on('close', code => {
// stdout must contain only the JSON Write-Output line; stderr has log messages
const json = stdout.trim().split('\n').reverse().find(l => l.trim().startsWith('{'));
try {
const result = JSON.parse(json);
if (result.success) resolve(result);
else reject(new Error(result.error || 'DI API error'));
} catch {
reject(new Error(stderr.trim() || stdout.trim() || `Process exited with code ${code}`));
}
});
child.on('error', err => reject(new Error(`Spawn failed: ${err.message}`)));
if (stdinData) {
child.stdin.write(stdinData);
child.stdin.end();
}
});
}
async function addPurchaseRequest(body, companyDB) {
const payload = buildPayload(body, companyDB);
if (fs.existsSync(EXE_PATH)) {
console.log(`[DI-API] ${payload.UserName}@${payload.CompanyDB} (pooled connection)`);
const result = await sendToProc(companyDB, payload);
if (result.success) return result;
throw new Error(result.error || 'DI API error');
}
console.log(`[DI-API] Using PowerShell → ${payload.UserName}@${payload.CompanyDB}`);
return runProcessOnce('powershell.exe', [
'-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
'-File', PS_SCRIPT,
'-PayloadJson', JSON.stringify(payload),
], null);
}
// Deletes a component line from a Production Order — Service Layer (OData
// PATCH) has no way to do this at all (verified live: omitting a line from
// the array is silently ignored, the line stays). DI API's dedicated
// ProductionOrders_Lines interface supports SetCurrentLine + Delete() even
// on an already-Released order. Used for a Deviation-superseded line the
// user wants physically gone from SAP (the app's own audit trail — the
// deviation record — is untouched either way, it doesn't read SAP's line
// list for its own history).
async function removeProductionOrderLine(docEntry, lineNum, companyDB) {
const payload = buildPayload({ Action: 'RemoveProductionOrderLine', DocEntry: docEntry, LineNum: lineNum }, companyDB);
if (fs.existsSync(EXE_PATH)) {
console.log(`[DI-API] RemoveProductionOrderLine DocEntry=${docEntry} Line=${lineNum} → ${payload.UserName}@${payload.CompanyDB} (pooled connection)`);
const result = await sendToProc(companyDB, payload);
if (result.success) return result;
throw new Error(result.error || 'DI API error');
}
throw new Error('SapDiConsole.exe not built — run services/SapDiConsole/build.bat');
}
module.exports = { addPurchaseRequest, removeProductionOrderLine };