@saptools/cf-hana 0.4.1 → 0.5.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/CHANGELOG.md +18 -0
- package/README.md +43 -1
- package/dist/cli.js +740 -48
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +35 -4
- package/dist/index.js +704 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
7
7
|
var CLI_NAME = "cf-hana";
|
|
8
|
-
var CLI_VERSION = "0.
|
|
8
|
+
var CLI_VERSION = "0.5.0";
|
|
9
9
|
var ENV_PREFIX = "CF_HANA";
|
|
10
10
|
var DEFAULT_QUERY_TIMEOUT_MS = 6e4;
|
|
11
11
|
var DEFAULT_CONNECT_TIMEOUT_MS = 6e4;
|
|
@@ -18,6 +18,27 @@ var DEFAULT_RESULT_TTL_MINUTES = 10080;
|
|
|
18
18
|
var DEFAULT_RESULT_SEARCH_LIMIT = 20;
|
|
19
19
|
var DEFAULT_MAX_RESULT_STORE_BYTES = 256 * 1024 * 1024;
|
|
20
20
|
var MAX_RESULT_STORE_BYTES = resolveMaxResultStoreBytes();
|
|
21
|
+
function resolvePositiveIntEnv(suffix, defaultValue) {
|
|
22
|
+
const raw = readEnv(envName(suffix));
|
|
23
|
+
if (raw === void 0) {
|
|
24
|
+
return defaultValue;
|
|
25
|
+
}
|
|
26
|
+
const parsed = Number(raw);
|
|
27
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : defaultValue;
|
|
28
|
+
}
|
|
29
|
+
var DEFAULT_TUNNEL_FALLBACK_BUDGET_MS = resolvePositiveIntEnv(
|
|
30
|
+
"TUNNEL_FALLBACK_BUDGET_MS",
|
|
31
|
+
25e3
|
|
32
|
+
);
|
|
33
|
+
var DEFAULT_TUNNEL_CANDIDATE_TIMEOUT_MS = resolvePositiveIntEnv(
|
|
34
|
+
"TUNNEL_CANDIDATE_TIMEOUT_MS",
|
|
35
|
+
15e3
|
|
36
|
+
);
|
|
37
|
+
var DEFAULT_TUNNEL_MAX_CANDIDATES = resolvePositiveIntEnv("TUNNEL_MAX_CANDIDATES", 3);
|
|
38
|
+
var DEFAULT_TUNNEL_KEEPALIVE_SECONDS = resolvePositiveIntEnv(
|
|
39
|
+
"TUNNEL_KEEPALIVE_SECONDS",
|
|
40
|
+
1200
|
|
41
|
+
);
|
|
21
42
|
function resolveMaxResultStoreBytes() {
|
|
22
43
|
if (readEnv(envName("DRIVER")) !== "fake") {
|
|
23
44
|
return DEFAULT_MAX_RESULT_STORE_BYTES;
|
|
@@ -1852,6 +1873,16 @@ async function cfEnvDirect(appName) {
|
|
|
1852
1873
|
delete env["SAP_PASSWORD"];
|
|
1853
1874
|
return await execWithRetries(bin, [...argsPrefix, "env", appName], env);
|
|
1854
1875
|
}
|
|
1876
|
+
async function cfApps(ctx) {
|
|
1877
|
+
return await runCf(["apps"], ctx);
|
|
1878
|
+
}
|
|
1879
|
+
async function cfAppsDirect() {
|
|
1880
|
+
const { bin, argsPrefix } = resolveCfBin();
|
|
1881
|
+
const env = { ...process.env };
|
|
1882
|
+
delete env["SAP_EMAIL"];
|
|
1883
|
+
delete env["SAP_PASSWORD"];
|
|
1884
|
+
return await execWithRetries(bin, [...argsPrefix, "apps"], env);
|
|
1885
|
+
}
|
|
1855
1886
|
async function readCurrentCfTarget() {
|
|
1856
1887
|
const { bin, argsPrefix } = resolveCfBin();
|
|
1857
1888
|
const env = { ...process.env };
|
|
@@ -1901,6 +1932,26 @@ function parseTargetFields(stdout) {
|
|
|
1901
1932
|
}
|
|
1902
1933
|
return map;
|
|
1903
1934
|
}
|
|
1935
|
+
function parseCfAppsOutput(stdout) {
|
|
1936
|
+
const lines = stdout.split(/\r?\n/);
|
|
1937
|
+
const headerIndex = lines.findIndex((line) => /^\s*name\s+\S/i.test(line));
|
|
1938
|
+
if (headerIndex === -1) {
|
|
1939
|
+
return [];
|
|
1940
|
+
}
|
|
1941
|
+
const rows = [];
|
|
1942
|
+
for (const line of lines.slice(headerIndex + 1)) {
|
|
1943
|
+
const trimmed = line.trim();
|
|
1944
|
+
if (trimmed.length === 0) {
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
const [name, state] = trimmed.split(/\s+/);
|
|
1948
|
+
if (name === void 0 || state === void 0) {
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
rows.push({ name, state });
|
|
1952
|
+
}
|
|
1953
|
+
return rows;
|
|
1954
|
+
}
|
|
1904
1955
|
function formatCurrentCfAppSelector(target, appName) {
|
|
1905
1956
|
const name = appName.trim();
|
|
1906
1957
|
if (!name) {
|
|
@@ -2212,7 +2263,10 @@ async function resolveAppBindings(rawSelector, options) {
|
|
|
2212
2263
|
source: "live",
|
|
2213
2264
|
selectorSource: target.selectorSource,
|
|
2214
2265
|
regionConfirmed: target.regionConfirmed,
|
|
2215
|
-
selectorCanBePinned: target.selectorCanBePinned
|
|
2266
|
+
selectorCanBePinned: target.selectorCanBePinned,
|
|
2267
|
+
apiEndpoint: target.apiEndpoint,
|
|
2268
|
+
orgName: target.orgName,
|
|
2269
|
+
spaceName: target.spaceName
|
|
2216
2270
|
};
|
|
2217
2271
|
}
|
|
2218
2272
|
async function readExplicitBindings(target, options) {
|
|
@@ -2278,6 +2332,7 @@ function toConnectionTarget(binding, role) {
|
|
|
2278
2332
|
// src/driver/fake.ts
|
|
2279
2333
|
import { appendFile } from "fs/promises";
|
|
2280
2334
|
var catalogFailureInjected = false;
|
|
2335
|
+
var connectFailureInjectedOnce = false;
|
|
2281
2336
|
function catalogObjects() {
|
|
2282
2337
|
return [
|
|
2283
2338
|
{ SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "EXISTING_TABLE", OBJECT_TYPE: "TABLE" },
|
|
@@ -2486,10 +2541,28 @@ var FakeConnection = class {
|
|
|
2486
2541
|
return this.closed;
|
|
2487
2542
|
}
|
|
2488
2543
|
};
|
|
2544
|
+
function maybeFailConnect() {
|
|
2545
|
+
const mode = readEnv(envName("FAKE_FAIL_CONNECT"));
|
|
2546
|
+
if (mode !== "once" && mode !== "always") {
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
if (mode === "once" && connectFailureInjectedOnce) {
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
connectFailureInjectedOnce = true;
|
|
2553
|
+
const cause = Object.assign(
|
|
2554
|
+
new Error("Could not connect to any host: [ fake-host:443 - forced fixture failure ]"),
|
|
2555
|
+
{ code: "EHDBOPENCONN" }
|
|
2556
|
+
);
|
|
2557
|
+
throw new CfHanaError("CONNECTION", `Failed to connect to HANA: ${cause.message}`, { cause });
|
|
2558
|
+
}
|
|
2489
2559
|
function createFakeDriver() {
|
|
2490
2560
|
return {
|
|
2491
2561
|
name: "fake",
|
|
2492
|
-
connect: (_params) =>
|
|
2562
|
+
connect: (_params) => {
|
|
2563
|
+
maybeFailConnect();
|
|
2564
|
+
return Promise.resolve(new FakeConnection());
|
|
2565
|
+
}
|
|
2493
2566
|
};
|
|
2494
2567
|
}
|
|
2495
2568
|
|
|
@@ -2714,7 +2787,8 @@ async function connectHdb(params) {
|
|
|
2714
2787
|
user: params.user,
|
|
2715
2788
|
password: params.password,
|
|
2716
2789
|
ca: params.certificate,
|
|
2717
|
-
useTLS: true
|
|
2790
|
+
useTLS: true,
|
|
2791
|
+
...params.servername === void 0 ? {} : { servername: params.servername }
|
|
2718
2792
|
});
|
|
2719
2793
|
await openClient(client, params.connectTimeoutMs);
|
|
2720
2794
|
try {
|
|
@@ -2995,6 +3069,605 @@ function applyAutoLimit(sql, limit) {
|
|
|
2995
3069
|
};
|
|
2996
3070
|
}
|
|
2997
3071
|
|
|
3072
|
+
// src/tunnel/cache.ts
|
|
3073
|
+
import { createHash as createHash2 } from "crypto";
|
|
3074
|
+
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile2, rename as rename2, rm as rm5, writeFile as writeFile3 } from "fs/promises";
|
|
3075
|
+
import { homedir as homedir4 } from "os";
|
|
3076
|
+
import { join as join6 } from "path";
|
|
3077
|
+
|
|
3078
|
+
// src/tunnel/process.ts
|
|
3079
|
+
import { spawn } from "child_process";
|
|
3080
|
+
import { mkdtemp as mkdtemp2, rm as rm4 } from "fs/promises";
|
|
3081
|
+
import { connect as netConnect, createServer } from "net";
|
|
3082
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3083
|
+
import { join as join5 } from "path";
|
|
3084
|
+
var PORT_POLL_INTERVAL_MS = 100;
|
|
3085
|
+
var DEFAULT_PORT_PROBE_TIMEOUT_MS = 500;
|
|
3086
|
+
function assertPositiveSafeInteger(label, value) {
|
|
3087
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
3088
|
+
throw new CfHanaError("CONFIG", `${label} must be a positive integer`);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
async function allocateLocalPort() {
|
|
3092
|
+
return await new Promise((resolve, reject) => {
|
|
3093
|
+
const server = createServer();
|
|
3094
|
+
server.once("error", reject);
|
|
3095
|
+
server.listen(0, "127.0.0.1", () => {
|
|
3096
|
+
const address = server.address();
|
|
3097
|
+
server.close(() => {
|
|
3098
|
+
if (address === null || typeof address === "string") {
|
|
3099
|
+
reject(new CfHanaError("CONNECTION", "Could not allocate a local port for a tunnel"));
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
resolve(address.port);
|
|
3103
|
+
});
|
|
3104
|
+
});
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
function probeLocalPort(port, timeoutMs = DEFAULT_PORT_PROBE_TIMEOUT_MS) {
|
|
3108
|
+
return new Promise((resolve) => {
|
|
3109
|
+
let settled = false;
|
|
3110
|
+
const socket = netConnect({ host: "127.0.0.1", port });
|
|
3111
|
+
const finish = (result) => {
|
|
3112
|
+
if (settled) {
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
settled = true;
|
|
3116
|
+
clearTimeout(timer);
|
|
3117
|
+
socket.destroy();
|
|
3118
|
+
resolve(result);
|
|
3119
|
+
};
|
|
3120
|
+
const timer = setTimeout(() => {
|
|
3121
|
+
finish(false);
|
|
3122
|
+
}, timeoutMs);
|
|
3123
|
+
timer.unref();
|
|
3124
|
+
socket.once("connect", () => {
|
|
3125
|
+
finish(true);
|
|
3126
|
+
});
|
|
3127
|
+
socket.once("error", () => {
|
|
3128
|
+
finish(false);
|
|
3129
|
+
});
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
function killTunnelProcess(pid) {
|
|
3133
|
+
if (pid === void 0) {
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
try {
|
|
3137
|
+
process.kill(pid, "SIGTERM");
|
|
3138
|
+
} catch {
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
async function withScopedCfSession(selectorSource, target, sapCredentials, work) {
|
|
3142
|
+
if (selectorSource === "ambient") {
|
|
3143
|
+
return await work();
|
|
3144
|
+
}
|
|
3145
|
+
if (sapCredentials === void 0) {
|
|
3146
|
+
throw new CredentialsNotFoundError(
|
|
3147
|
+
"SAP credentials are required to open a tunnel session for an explicit selector"
|
|
3148
|
+
);
|
|
3149
|
+
}
|
|
3150
|
+
const cfHome = await mkdtemp2(join5(tmpdir2(), "saptools-cf-hana-tunnel-"));
|
|
3151
|
+
try {
|
|
3152
|
+
const ctx = { cfHome };
|
|
3153
|
+
await cfApi(target.apiEndpoint, ctx);
|
|
3154
|
+
await cfAuth(sapCredentials.email, sapCredentials.password, ctx);
|
|
3155
|
+
await cfTargetSpace(target.orgName, target.spaceName, ctx);
|
|
3156
|
+
return await work(ctx);
|
|
3157
|
+
} finally {
|
|
3158
|
+
await rm4(cfHome, { recursive: true, force: true });
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
function raceTunnelReadiness(child, localPort, boundMs, probePort, killProcess) {
|
|
3162
|
+
return new Promise((resolve) => {
|
|
3163
|
+
let settled = false;
|
|
3164
|
+
const timers = {};
|
|
3165
|
+
const finish = (result) => {
|
|
3166
|
+
if (settled) {
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
settled = true;
|
|
3170
|
+
clearInterval(timers.poll);
|
|
3171
|
+
clearTimeout(timers.deadline);
|
|
3172
|
+
child.removeListener("exit", onAbort);
|
|
3173
|
+
child.removeListener("error", onAbort);
|
|
3174
|
+
if (result === void 0) {
|
|
3175
|
+
killProcess(child.pid);
|
|
3176
|
+
} else {
|
|
3177
|
+
child.unref();
|
|
3178
|
+
}
|
|
3179
|
+
resolve(result);
|
|
3180
|
+
};
|
|
3181
|
+
function onAbort() {
|
|
3182
|
+
finish();
|
|
3183
|
+
}
|
|
3184
|
+
child.on("exit", onAbort);
|
|
3185
|
+
child.on("error", onAbort);
|
|
3186
|
+
timers.poll = setInterval(() => {
|
|
3187
|
+
void probePort(localPort).then((open) => {
|
|
3188
|
+
if (open && child.pid !== void 0) {
|
|
3189
|
+
finish({ localPort, pid: child.pid });
|
|
3190
|
+
}
|
|
3191
|
+
});
|
|
3192
|
+
}, PORT_POLL_INTERVAL_MS);
|
|
3193
|
+
timers.deadline = setTimeout(finish, boundMs);
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
async function spawnTunnel(params, deps = {}) {
|
|
3197
|
+
assertPositiveSafeInteger("tunnel keepalive seconds", params.keepaliveSeconds);
|
|
3198
|
+
const remainingMs = params.deadline - Date.now();
|
|
3199
|
+
if (remainingMs <= 0) {
|
|
3200
|
+
return void 0;
|
|
3201
|
+
}
|
|
3202
|
+
const boundMs = Math.min(remainingMs, params.candidateTimeoutMs);
|
|
3203
|
+
const spawnProcess = deps.spawnProcess ?? spawn;
|
|
3204
|
+
const probePort = deps.probePort ?? probeLocalPort;
|
|
3205
|
+
const allocatePort = deps.allocatePort ?? allocateLocalPort;
|
|
3206
|
+
const killProcess = deps.killProcess ?? killTunnelProcess;
|
|
3207
|
+
const localPort = await allocatePort();
|
|
3208
|
+
const { bin, argsPrefix } = resolveCfBin();
|
|
3209
|
+
const forward = `${String(localPort)}:${params.hanaHost}:${String(params.hanaPort)}`;
|
|
3210
|
+
const args = [
|
|
3211
|
+
...argsPrefix,
|
|
3212
|
+
"ssh",
|
|
3213
|
+
params.app,
|
|
3214
|
+
"-L",
|
|
3215
|
+
forward,
|
|
3216
|
+
"-c",
|
|
3217
|
+
`sleep ${String(params.keepaliveSeconds)}`
|
|
3218
|
+
];
|
|
3219
|
+
const env = params.cfHome === void 0 ? { ...process.env } : { ...process.env, CF_HOME: params.cfHome };
|
|
3220
|
+
const child = spawnProcess(bin, args, { detached: true, stdio: "ignore", env });
|
|
3221
|
+
return await raceTunnelReadiness(child, localPort, boundMs, probePort, killProcess);
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
// src/tunnel/cache.ts
|
|
3225
|
+
var DEFAULT_ESTABLISHING_STALE_MS = 2 * DEFAULT_TUNNEL_FALLBACK_BUDGET_MS;
|
|
3226
|
+
var DEFAULT_POLL_INTERVAL_MS = 300;
|
|
3227
|
+
function isRecord3(value) {
|
|
3228
|
+
return typeof value === "object" && value !== null;
|
|
3229
|
+
}
|
|
3230
|
+
function defaultIsProcessAlive(pid) {
|
|
3231
|
+
try {
|
|
3232
|
+
process.kill(pid, 0);
|
|
3233
|
+
return true;
|
|
3234
|
+
} catch (error) {
|
|
3235
|
+
return isRecord3(error) && error["code"] !== "ESRCH";
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
function sleep(ms) {
|
|
3239
|
+
return new Promise((resolve) => {
|
|
3240
|
+
setTimeout(resolve, ms);
|
|
3241
|
+
});
|
|
3242
|
+
}
|
|
3243
|
+
function tunnelCacheRoot(saptoolsRoot) {
|
|
3244
|
+
return join6(saptoolsRoot ?? join6(homedir4(), ".saptools"), "cf-hana", "tunnel");
|
|
3245
|
+
}
|
|
3246
|
+
function tunnelCacheKey(host) {
|
|
3247
|
+
return createHash2("sha256").update(host).digest("hex");
|
|
3248
|
+
}
|
|
3249
|
+
function tunnelCachePath(host, saptoolsRoot) {
|
|
3250
|
+
return join6(tunnelCacheRoot(saptoolsRoot), `${tunnelCacheKey(host)}.json`);
|
|
3251
|
+
}
|
|
3252
|
+
function isOrgKey(value) {
|
|
3253
|
+
return isRecord3(value) && typeof value["apiEndpoint"] === "string" && typeof value["orgName"] === "string";
|
|
3254
|
+
}
|
|
3255
|
+
function hasEstablishingFields(value) {
|
|
3256
|
+
return value["status"] === "establishing" && typeof value["host"] === "string" && typeof value["startedAt"] === "string" && typeof value["ownerPid"] === "number" && isOrgKey(value["orgKey"]);
|
|
3257
|
+
}
|
|
3258
|
+
function hasReadyFields(value) {
|
|
3259
|
+
return value["status"] === "ready" && typeof value["host"] === "string" && typeof value["localPort"] === "number" && typeof value["pid"] === "number" && typeof value["app"] === "string" && typeof value["expiresAt"] === "string" && isOrgKey(value["orgKey"]);
|
|
3260
|
+
}
|
|
3261
|
+
function isTunnelCacheRecord(value) {
|
|
3262
|
+
return isRecord3(value) && value["version"] === 1 && (hasEstablishingFields(value) || hasReadyFields(value));
|
|
3263
|
+
}
|
|
3264
|
+
async function parseTunnelCacheFile(path) {
|
|
3265
|
+
try {
|
|
3266
|
+
const raw = JSON.parse(await readFile2(path, "utf8"));
|
|
3267
|
+
return isTunnelCacheRecord(raw) ? raw : void 0;
|
|
3268
|
+
} catch {
|
|
3269
|
+
return void 0;
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
async function readTunnelCacheEntry(host, options = {}) {
|
|
3273
|
+
const entry = await parseTunnelCacheFile(tunnelCachePath(host, options.saptoolsRoot));
|
|
3274
|
+
return entry?.host === host ? entry : void 0;
|
|
3275
|
+
}
|
|
3276
|
+
function isExpired(entry, now) {
|
|
3277
|
+
const expiresAt = Date.parse(entry.expiresAt);
|
|
3278
|
+
return !Number.isFinite(expiresAt) || now.getTime() >= expiresAt;
|
|
3279
|
+
}
|
|
3280
|
+
async function isTunnelUsable(entry, options = {}) {
|
|
3281
|
+
const isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
3282
|
+
if (!isAlive(entry.pid)) {
|
|
3283
|
+
return false;
|
|
3284
|
+
}
|
|
3285
|
+
if (isExpired(entry, options.now?.() ?? /* @__PURE__ */ new Date())) {
|
|
3286
|
+
return false;
|
|
3287
|
+
}
|
|
3288
|
+
const probePort = options.probePort ?? probeLocalPort;
|
|
3289
|
+
return await probePort(entry.localPort);
|
|
3290
|
+
}
|
|
3291
|
+
function isEstablishingStale(entry, options) {
|
|
3292
|
+
const isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
3293
|
+
if (!isAlive(entry.ownerPid)) {
|
|
3294
|
+
return true;
|
|
3295
|
+
}
|
|
3296
|
+
const startedAt = Date.parse(entry.startedAt);
|
|
3297
|
+
if (!Number.isFinite(startedAt)) {
|
|
3298
|
+
return true;
|
|
3299
|
+
}
|
|
3300
|
+
const now = options.now?.() ?? /* @__PURE__ */ new Date();
|
|
3301
|
+
const staleAfterMs = options.staleAfterMs ?? DEFAULT_ESTABLISHING_STALE_MS;
|
|
3302
|
+
return now.getTime() - startedAt >= staleAfterMs;
|
|
3303
|
+
}
|
|
3304
|
+
async function writeEstablishingMarker(host, ownerPid, orgKey, options) {
|
|
3305
|
+
const root = tunnelCacheRoot(options.saptoolsRoot);
|
|
3306
|
+
await mkdir4(root, { recursive: true, mode: 448 });
|
|
3307
|
+
const record = {
|
|
3308
|
+
version: 1,
|
|
3309
|
+
status: "establishing",
|
|
3310
|
+
host,
|
|
3311
|
+
startedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
3312
|
+
ownerPid,
|
|
3313
|
+
orgKey
|
|
3314
|
+
};
|
|
3315
|
+
try {
|
|
3316
|
+
await writeFile3(tunnelCachePath(host, options.saptoolsRoot), `${JSON.stringify(record)}
|
|
3317
|
+
`, {
|
|
3318
|
+
encoding: "utf8",
|
|
3319
|
+
mode: 384,
|
|
3320
|
+
flag: "wx"
|
|
3321
|
+
});
|
|
3322
|
+
return true;
|
|
3323
|
+
} catch (error) {
|
|
3324
|
+
if (isRecord3(error) && error["code"] === "EEXIST") {
|
|
3325
|
+
return false;
|
|
3326
|
+
}
|
|
3327
|
+
throw error;
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
async function claimEstablishing(host, ownerPid, orgKey, options = {}) {
|
|
3331
|
+
if (await writeEstablishingMarker(host, ownerPid, orgKey, options)) {
|
|
3332
|
+
return { outcome: "claimed" };
|
|
3333
|
+
}
|
|
3334
|
+
const existing = await readTunnelCacheEntry(host, options);
|
|
3335
|
+
if (existing === void 0) {
|
|
3336
|
+
return await writeEstablishingMarker(host, ownerPid, orgKey, options) ? { outcome: "claimed" } : { outcome: "wait" };
|
|
3337
|
+
}
|
|
3338
|
+
if (existing.status === "ready") {
|
|
3339
|
+
return { outcome: "already-ready", record: existing };
|
|
3340
|
+
}
|
|
3341
|
+
if (!isEstablishingStale(existing, options)) {
|
|
3342
|
+
return { outcome: "wait" };
|
|
3343
|
+
}
|
|
3344
|
+
await rm5(tunnelCachePath(host, options.saptoolsRoot), { force: true });
|
|
3345
|
+
return await writeEstablishingMarker(host, ownerPid, orgKey, options) ? { outcome: "claimed" } : { outcome: "wait" };
|
|
3346
|
+
}
|
|
3347
|
+
async function waitForEstablishment(host, deadline, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, options = {}) {
|
|
3348
|
+
for (; ; ) {
|
|
3349
|
+
const entry = await readTunnelCacheEntry(host, options);
|
|
3350
|
+
if (entry === void 0) {
|
|
3351
|
+
return void 0;
|
|
3352
|
+
}
|
|
3353
|
+
if (entry.status === "ready") {
|
|
3354
|
+
return entry;
|
|
3355
|
+
}
|
|
3356
|
+
const remainingMs = deadline - Date.now();
|
|
3357
|
+
if (remainingMs <= 0) {
|
|
3358
|
+
return void 0;
|
|
3359
|
+
}
|
|
3360
|
+
await sleep(Math.min(pollIntervalMs, remainingMs));
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
async function finalizeEstablishingReady(host, record, options = {}) {
|
|
3364
|
+
const root = tunnelCacheRoot(options.saptoolsRoot);
|
|
3365
|
+
const path = tunnelCachePath(host, options.saptoolsRoot);
|
|
3366
|
+
const tempPath = `${path}.tmp-${String(process.pid)}`;
|
|
3367
|
+
const stored = { version: 1, status: "ready", host, ...record };
|
|
3368
|
+
await mkdir4(root, { recursive: true, mode: 448 });
|
|
3369
|
+
await rm5(tempPath, { force: true });
|
|
3370
|
+
await writeFile3(tempPath, `${JSON.stringify(stored)}
|
|
3371
|
+
`, { encoding: "utf8", mode: 384 });
|
|
3372
|
+
await rename2(tempPath, path);
|
|
3373
|
+
}
|
|
3374
|
+
async function finalizeEstablishingFailed(host, options = {}) {
|
|
3375
|
+
await rm5(tunnelCachePath(host, options.saptoolsRoot), { force: true });
|
|
3376
|
+
}
|
|
3377
|
+
async function evictTunnelCache(host, options = {}) {
|
|
3378
|
+
await rm5(tunnelCachePath(host, options.saptoolsRoot), { force: true });
|
|
3379
|
+
}
|
|
3380
|
+
function sameOrg(a, b) {
|
|
3381
|
+
return a.apiEndpoint === b.apiEndpoint && a.orgName === b.orgName;
|
|
3382
|
+
}
|
|
3383
|
+
async function reapOneFile(path, currentOrgKey, options) {
|
|
3384
|
+
const entry = await parseTunnelCacheFile(path);
|
|
3385
|
+
if (entry === void 0) {
|
|
3386
|
+
return;
|
|
3387
|
+
}
|
|
3388
|
+
const killProcess = options.killProcess ?? killTunnelProcess;
|
|
3389
|
+
if (entry.status === "establishing") {
|
|
3390
|
+
if (isEstablishingStale(entry, options)) {
|
|
3391
|
+
await rm5(path, { force: true });
|
|
3392
|
+
}
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3395
|
+
const isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
3396
|
+
if (!isAlive(entry.pid)) {
|
|
3397
|
+
await rm5(path, { force: true });
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
const now = options.now?.() ?? /* @__PURE__ */ new Date();
|
|
3401
|
+
if (isExpired(entry, now) || !sameOrg(entry.orgKey, currentOrgKey)) {
|
|
3402
|
+
killProcess(entry.pid);
|
|
3403
|
+
await rm5(path, { force: true });
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
async function reapStaleAndCrossOrgTunnels(currentOrgKey, options = {}) {
|
|
3407
|
+
const root = tunnelCacheRoot(options.saptoolsRoot);
|
|
3408
|
+
let files;
|
|
3409
|
+
try {
|
|
3410
|
+
files = await readdir2(root);
|
|
3411
|
+
} catch {
|
|
3412
|
+
return;
|
|
3413
|
+
}
|
|
3414
|
+
await Promise.all(
|
|
3415
|
+
files.filter((file) => file.endsWith(".json")).map(async (file) => {
|
|
3416
|
+
await reapOneFile(join6(root, file), currentOrgKey, options);
|
|
3417
|
+
})
|
|
3418
|
+
);
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
// src/tunnel/candidates.ts
|
|
3422
|
+
var RUNNING_STATE = "started";
|
|
3423
|
+
var VALID_APP_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
3424
|
+
function isValidAppName(name) {
|
|
3425
|
+
return VALID_APP_NAME.test(name);
|
|
3426
|
+
}
|
|
3427
|
+
function discoveredAppNames(stdout, targetAppName) {
|
|
3428
|
+
if (stdout === void 0) {
|
|
3429
|
+
return [];
|
|
3430
|
+
}
|
|
3431
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3432
|
+
const names = [];
|
|
3433
|
+
for (const row of parseCfAppsOutput(stdout)) {
|
|
3434
|
+
if (row.state !== RUNNING_STATE) {
|
|
3435
|
+
continue;
|
|
3436
|
+
}
|
|
3437
|
+
if (row.name === targetAppName || seen.has(row.name)) {
|
|
3438
|
+
continue;
|
|
3439
|
+
}
|
|
3440
|
+
if (!isValidAppName(row.name)) {
|
|
3441
|
+
continue;
|
|
3442
|
+
}
|
|
3443
|
+
seen.add(row.name);
|
|
3444
|
+
names.push(row.name);
|
|
3445
|
+
}
|
|
3446
|
+
return names;
|
|
3447
|
+
}
|
|
3448
|
+
function buildCandidateList(targetAppName, discoveredStdout, maxCandidates) {
|
|
3449
|
+
const discovered = discoveredAppNames(discoveredStdout, targetAppName);
|
|
3450
|
+
return [targetAppName, ...discovered.slice(0, maxCandidates)];
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
// src/tunnel/classifier.ts
|
|
3454
|
+
var CONNECT_TIMEOUT_MESSAGE = "HANA connection timed out";
|
|
3455
|
+
var OPEN_CONNECTION_CODE = "EHDBOPENCONN";
|
|
3456
|
+
var MAX_CAUSE_DEPTH = 5;
|
|
3457
|
+
function causeCode(value) {
|
|
3458
|
+
return typeof value === "object" && value !== null ? value.code : void 0;
|
|
3459
|
+
}
|
|
3460
|
+
function causeOf(value) {
|
|
3461
|
+
return typeof value === "object" && value !== null ? value.cause : void 0;
|
|
3462
|
+
}
|
|
3463
|
+
function hasOpenConnectionCause(cause, depth) {
|
|
3464
|
+
if (depth > MAX_CAUSE_DEPTH) {
|
|
3465
|
+
return false;
|
|
3466
|
+
}
|
|
3467
|
+
if (causeCode(cause) === OPEN_CONNECTION_CODE) {
|
|
3468
|
+
return true;
|
|
3469
|
+
}
|
|
3470
|
+
return hasOpenConnectionCause(causeOf(cause), depth + 1);
|
|
3471
|
+
}
|
|
3472
|
+
function isConnectivityFailure(error) {
|
|
3473
|
+
if (!(error instanceof CfHanaError)) {
|
|
3474
|
+
return false;
|
|
3475
|
+
}
|
|
3476
|
+
if (error.code === "TIMEOUT") {
|
|
3477
|
+
return error.message.includes(CONNECT_TIMEOUT_MESSAGE);
|
|
3478
|
+
}
|
|
3479
|
+
if (error.code !== "CONNECTION") {
|
|
3480
|
+
return false;
|
|
3481
|
+
}
|
|
3482
|
+
return hasOpenConnectionCause(error.cause, 0);
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
// src/tunnel/fallback.ts
|
|
3486
|
+
var EXPIRY_SAFETY_MARGIN_MS = 3e4;
|
|
3487
|
+
function directParamsOf(config) {
|
|
3488
|
+
return {
|
|
3489
|
+
host: config.host,
|
|
3490
|
+
port: config.port,
|
|
3491
|
+
user: config.user,
|
|
3492
|
+
password: config.password,
|
|
3493
|
+
schema: config.schema,
|
|
3494
|
+
certificate: config.certificate,
|
|
3495
|
+
connectTimeoutMs: config.connectTimeoutMs
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
3498
|
+
function tunneledParams(directParams, localPort) {
|
|
3499
|
+
return { ...directParams, host: "127.0.0.1", port: localPort, servername: directParams.host };
|
|
3500
|
+
}
|
|
3501
|
+
function orgKeyOf(config) {
|
|
3502
|
+
return { apiEndpoint: config.apiEndpoint, orgName: config.orgName };
|
|
3503
|
+
}
|
|
3504
|
+
function tunnelExpiryIso() {
|
|
3505
|
+
return new Date(
|
|
3506
|
+
Date.now() + DEFAULT_TUNNEL_KEEPALIVE_SECONDS * 1e3 - EXPIRY_SAFETY_MARGIN_MS
|
|
3507
|
+
).toISOString();
|
|
3508
|
+
}
|
|
3509
|
+
async function discardQuietly(work) {
|
|
3510
|
+
await work.catch(() => {
|
|
3511
|
+
});
|
|
3512
|
+
}
|
|
3513
|
+
async function tryReadyRecord(record, driver, config, directParams, cacheOptions) {
|
|
3514
|
+
try {
|
|
3515
|
+
const connection = await driver.connect(tunneledParams(directParams, record.localPort));
|
|
3516
|
+
config.onTunnelStatus?.(`connected via SSH tunnel through ${record.app}`);
|
|
3517
|
+
return connection;
|
|
3518
|
+
} catch (error) {
|
|
3519
|
+
if (isConnectivityFailure(error)) {
|
|
3520
|
+
await discardQuietly(evictTunnelCache(config.host, cacheOptions));
|
|
3521
|
+
return void 0;
|
|
3522
|
+
}
|
|
3523
|
+
throw error;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
async function discoverAppsStdout(ctx) {
|
|
3527
|
+
try {
|
|
3528
|
+
return ctx === void 0 ? await cfAppsDirect() : await cfApps(ctx);
|
|
3529
|
+
} catch {
|
|
3530
|
+
return;
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
async function buildCandidates(config, ctx, hintApp) {
|
|
3534
|
+
const stdout = await discoverAppsStdout(ctx);
|
|
3535
|
+
const base = buildCandidateList(config.appName, stdout, DEFAULT_TUNNEL_MAX_CANDIDATES);
|
|
3536
|
+
return hintApp === void 0 || base.includes(hintApp) ? base : [hintApp, ...base];
|
|
3537
|
+
}
|
|
3538
|
+
async function finalizeCandidateConnection(app, spawned, driver, config, directParams, cacheOptions) {
|
|
3539
|
+
const readyRecord = {
|
|
3540
|
+
localPort: spawned.localPort,
|
|
3541
|
+
pid: spawned.pid,
|
|
3542
|
+
app,
|
|
3543
|
+
orgKey: orgKeyOf(config),
|
|
3544
|
+
expiresAt: tunnelExpiryIso()
|
|
3545
|
+
};
|
|
3546
|
+
try {
|
|
3547
|
+
const connection = await driver.connect(tunneledParams(directParams, spawned.localPort));
|
|
3548
|
+
await discardQuietly(finalizeEstablishingReady(config.host, readyRecord, cacheOptions));
|
|
3549
|
+
config.onTunnelStatus?.(`connected via SSH tunnel through ${app}`);
|
|
3550
|
+
return connection;
|
|
3551
|
+
} catch (error) {
|
|
3552
|
+
if (isConnectivityFailure(error)) {
|
|
3553
|
+
await discardQuietly(finalizeEstablishingFailed(config.host, cacheOptions));
|
|
3554
|
+
killTunnelProcess(spawned.pid);
|
|
3555
|
+
return void 0;
|
|
3556
|
+
}
|
|
3557
|
+
await discardQuietly(finalizeEstablishingReady(config.host, readyRecord, cacheOptions));
|
|
3558
|
+
throw error;
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
async function tryCandidate(app, ctx, driver, config, directParams, deadline, overrides) {
|
|
3562
|
+
const cacheOptions = overrides.cache ?? {};
|
|
3563
|
+
config.onTunnelStatus?.(`trying to reach ${config.host} via app ${app}...`);
|
|
3564
|
+
const claim = await claimEstablishing(config.host, process.pid, orgKeyOf(config), cacheOptions);
|
|
3565
|
+
if (claim.outcome === "already-ready") {
|
|
3566
|
+
return await tryReadyRecord(claim.record, driver, config, directParams, cacheOptions);
|
|
3567
|
+
}
|
|
3568
|
+
if (claim.outcome === "wait") {
|
|
3569
|
+
const ready = await waitForEstablishment(config.host, deadline, void 0, cacheOptions);
|
|
3570
|
+
return ready === void 0 ? void 0 : await tryReadyRecord(ready, driver, config, directParams, cacheOptions);
|
|
3571
|
+
}
|
|
3572
|
+
const spawned = await spawnTunnel(
|
|
3573
|
+
{
|
|
3574
|
+
cfHome: ctx?.cfHome,
|
|
3575
|
+
app,
|
|
3576
|
+
hanaHost: config.host,
|
|
3577
|
+
hanaPort: config.port,
|
|
3578
|
+
keepaliveSeconds: DEFAULT_TUNNEL_KEEPALIVE_SECONDS,
|
|
3579
|
+
deadline,
|
|
3580
|
+
candidateTimeoutMs: DEFAULT_TUNNEL_CANDIDATE_TIMEOUT_MS
|
|
3581
|
+
},
|
|
3582
|
+
overrides.process ?? {}
|
|
3583
|
+
);
|
|
3584
|
+
if (spawned === void 0) {
|
|
3585
|
+
await discardQuietly(finalizeEstablishingFailed(config.host, cacheOptions));
|
|
3586
|
+
return void 0;
|
|
3587
|
+
}
|
|
3588
|
+
return await finalizeCandidateConnection(app, spawned, driver, config, directParams, cacheOptions);
|
|
3589
|
+
}
|
|
3590
|
+
async function tryCachedTunnel(driver, config, directParams, cacheOptions) {
|
|
3591
|
+
if (config.refreshTunnel) {
|
|
3592
|
+
await discardQuietly(evictTunnelCache(config.host, cacheOptions));
|
|
3593
|
+
return { connection: void 0, hintApp: void 0 };
|
|
3594
|
+
}
|
|
3595
|
+
const entry = await readTunnelCacheEntry(config.host, cacheOptions);
|
|
3596
|
+
if (entry?.status !== "ready") {
|
|
3597
|
+
return { connection: void 0, hintApp: void 0 };
|
|
3598
|
+
}
|
|
3599
|
+
if (!await isTunnelUsable(entry, cacheOptions)) {
|
|
3600
|
+
await discardQuietly(evictTunnelCache(config.host, cacheOptions));
|
|
3601
|
+
return { connection: void 0, hintApp: entry.app };
|
|
3602
|
+
}
|
|
3603
|
+
const connection = await tryReadyRecord(entry, driver, config, directParams, cacheOptions);
|
|
3604
|
+
return { connection, hintApp: entry.app };
|
|
3605
|
+
}
|
|
3606
|
+
async function tryDirectConnect(driver, config, directParams) {
|
|
3607
|
+
if (config.tunnelMode !== "auto") {
|
|
3608
|
+
return { connection: void 0, directError: void 0 };
|
|
3609
|
+
}
|
|
3610
|
+
try {
|
|
3611
|
+
return { connection: await driver.connect(directParams), directError: void 0 };
|
|
3612
|
+
} catch (error) {
|
|
3613
|
+
if (!isConnectivityFailure(error)) {
|
|
3614
|
+
throw error;
|
|
3615
|
+
}
|
|
3616
|
+
return { connection: void 0, directError: error };
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
function buildExhaustionError(host, directError, candidates) {
|
|
3620
|
+
if (directError !== void 0) {
|
|
3621
|
+
return directError;
|
|
3622
|
+
}
|
|
3623
|
+
return new CfHanaError(
|
|
3624
|
+
"CONNECTION",
|
|
3625
|
+
`Could not establish an SSH tunnel to ${host} through any candidate app (tried: ${candidates.length > 0 ? candidates.join(", ") : "none"})`
|
|
3626
|
+
);
|
|
3627
|
+
}
|
|
3628
|
+
async function runCandidateLoop(ctx, driver, config, directParams, deadline, hintApp, overrides) {
|
|
3629
|
+
const candidatesTried = await buildCandidates(config, ctx, hintApp);
|
|
3630
|
+
for (const app of candidatesTried) {
|
|
3631
|
+
if (Date.now() >= deadline) {
|
|
3632
|
+
break;
|
|
3633
|
+
}
|
|
3634
|
+
const connection = await tryCandidate(app, ctx, driver, config, directParams, deadline, overrides);
|
|
3635
|
+
if (connection !== void 0) {
|
|
3636
|
+
return { connection, candidatesTried };
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
return { connection: void 0, candidatesTried };
|
|
3640
|
+
}
|
|
3641
|
+
async function connectWithTunnelFallback(driver, config, overrides = {}) {
|
|
3642
|
+
const directParams = directParamsOf(config);
|
|
3643
|
+
const cacheOptions = overrides.cache ?? {};
|
|
3644
|
+
await discardQuietly(reapStaleAndCrossOrgTunnels(orgKeyOf(config), cacheOptions));
|
|
3645
|
+
const cached = await tryCachedTunnel(driver, config, directParams, cacheOptions);
|
|
3646
|
+
if (cached.connection !== void 0) {
|
|
3647
|
+
return cached.connection;
|
|
3648
|
+
}
|
|
3649
|
+
const direct = await tryDirectConnect(driver, config, directParams);
|
|
3650
|
+
if (direct.connection !== void 0) {
|
|
3651
|
+
return direct.connection;
|
|
3652
|
+
}
|
|
3653
|
+
const deadline = Date.now() + DEFAULT_TUNNEL_FALLBACK_BUDGET_MS;
|
|
3654
|
+
const target = {
|
|
3655
|
+
apiEndpoint: config.apiEndpoint,
|
|
3656
|
+
orgName: config.orgName,
|
|
3657
|
+
spaceName: config.spaceName
|
|
3658
|
+
};
|
|
3659
|
+
const loopResult = await withScopedCfSession(
|
|
3660
|
+
config.selectorSource,
|
|
3661
|
+
target,
|
|
3662
|
+
config.sapCredentials,
|
|
3663
|
+
(ctx) => runCandidateLoop(ctx, driver, config, directParams, deadline, cached.hintApp, overrides)
|
|
3664
|
+
);
|
|
3665
|
+
if (loopResult.connection !== void 0) {
|
|
3666
|
+
return loopResult.connection;
|
|
3667
|
+
}
|
|
3668
|
+
throw buildExhaustionError(config.host, direct.directError, loopResult.candidatesTried);
|
|
3669
|
+
}
|
|
3670
|
+
|
|
2998
3671
|
// src/connection.ts
|
|
2999
3672
|
function assertValidAutoLimit(value) {
|
|
3000
3673
|
if (value !== false && (!Number.isSafeInteger(value) || value <= 0 || value >= Number.MAX_SAFE_INTEGER)) {
|
|
@@ -3088,15 +3761,7 @@ var Connection = class _Connection {
|
|
|
3088
3761
|
driverConnection;
|
|
3089
3762
|
config;
|
|
3090
3763
|
static async open(driver, config) {
|
|
3091
|
-
const driverConnection = await driver
|
|
3092
|
-
host: config.host,
|
|
3093
|
-
port: config.port,
|
|
3094
|
-
user: config.user,
|
|
3095
|
-
password: config.password,
|
|
3096
|
-
schema: config.schema,
|
|
3097
|
-
certificate: config.certificate,
|
|
3098
|
-
connectTimeoutMs: config.connectTimeoutMs
|
|
3099
|
-
});
|
|
3764
|
+
const driverConnection = await connectWithTunnelFallback(driver, config);
|
|
3100
3765
|
return new _Connection(driverConnection, config);
|
|
3101
3766
|
}
|
|
3102
3767
|
async query(sql, params, options) {
|
|
@@ -3378,6 +4043,31 @@ function toPoolOptions(options) {
|
|
|
3378
4043
|
}
|
|
3379
4044
|
return options.pool ?? {};
|
|
3380
4045
|
}
|
|
4046
|
+
function buildConnectionConfig(resolved, target, options) {
|
|
4047
|
+
const sapCredentials = readSapCredentials({ email: options.email, password: options.password });
|
|
4048
|
+
return {
|
|
4049
|
+
host: target.host,
|
|
4050
|
+
port: target.port,
|
|
4051
|
+
user: target.user,
|
|
4052
|
+
password: target.password,
|
|
4053
|
+
schema: target.schema,
|
|
4054
|
+
certificate: target.certificate,
|
|
4055
|
+
connectTimeoutMs: options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
|
|
4056
|
+
queryTimeoutMs: options.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
4057
|
+
readOnly: options.readOnly ?? false,
|
|
4058
|
+
allowDestructive: options.allowDestructive ?? false,
|
|
4059
|
+
autoLimit: options.autoLimit ?? DEFAULT_AUTO_LIMIT,
|
|
4060
|
+
appName: resolved.appName,
|
|
4061
|
+
orgName: resolved.orgName,
|
|
4062
|
+
spaceName: resolved.spaceName,
|
|
4063
|
+
apiEndpoint: resolved.apiEndpoint,
|
|
4064
|
+
selectorSource: resolved.selectorSource,
|
|
4065
|
+
tunnelMode: options.tunnel === true ? "always" : "auto",
|
|
4066
|
+
refreshTunnel: options.refreshTunnel ?? false,
|
|
4067
|
+
...sapCredentials === void 0 ? {} : { sapCredentials },
|
|
4068
|
+
...options.onTunnelStatus === void 0 ? {} : { onTunnelStatus: options.onTunnelStatus }
|
|
4069
|
+
};
|
|
4070
|
+
}
|
|
3381
4071
|
function toSelectedBindingInfo(bindings, selected) {
|
|
3382
4072
|
const availableBindingNames = bindings.flatMap(
|
|
3383
4073
|
(binding) => binding.name === void 0 ? [] : [binding.name]
|
|
@@ -3405,19 +4095,7 @@ var HanaClient = class _HanaClient {
|
|
|
3405
4095
|
const bindingInfo = toSelectedBindingInfo(resolved.bindings, binding);
|
|
3406
4096
|
const target = toConnectionTarget(binding, role);
|
|
3407
4097
|
const driver = createDriver();
|
|
3408
|
-
const config =
|
|
3409
|
-
host: target.host,
|
|
3410
|
-
port: target.port,
|
|
3411
|
-
user: target.user,
|
|
3412
|
-
password: target.password,
|
|
3413
|
-
schema: target.schema,
|
|
3414
|
-
certificate: target.certificate,
|
|
3415
|
-
connectTimeoutMs: options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
|
|
3416
|
-
queryTimeoutMs: options.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
3417
|
-
readOnly: options.readOnly ?? false,
|
|
3418
|
-
allowDestructive: options.allowDestructive ?? false,
|
|
3419
|
-
autoLimit: options.autoLimit ?? DEFAULT_AUTO_LIMIT
|
|
3420
|
-
};
|
|
4098
|
+
const config = buildConnectionConfig(resolved, target, options);
|
|
3421
4099
|
const poolOptions = toPoolOptions(options);
|
|
3422
4100
|
const info = {
|
|
3423
4101
|
selector: resolved.selector,
|
|
@@ -3614,7 +4292,7 @@ async function connect(selector, options) {
|
|
|
3614
4292
|
}
|
|
3615
4293
|
|
|
3616
4294
|
// src/cli-results.ts
|
|
3617
|
-
import { writeFile as
|
|
4295
|
+
import { writeFile as writeFile5 } from "fs/promises";
|
|
3618
4296
|
|
|
3619
4297
|
// src/result-inspect.ts
|
|
3620
4298
|
function assertPositiveInteger(name, value) {
|
|
@@ -3886,19 +4564,19 @@ function searchResultSession(session, searchTerm, options) {
|
|
|
3886
4564
|
|
|
3887
4565
|
// src/result-store.ts
|
|
3888
4566
|
import { randomBytes } from "crypto";
|
|
3889
|
-
import { mkdir as
|
|
3890
|
-
import { homedir as
|
|
3891
|
-
import { join as
|
|
4567
|
+
import { mkdir as mkdir5, readFile as readFile3, readdir as readdir3, rename as rename3, rm as rm6, writeFile as writeFile4 } from "fs/promises";
|
|
4568
|
+
import { homedir as homedir5 } from "os";
|
|
4569
|
+
import { join as join7 } from "path";
|
|
3892
4570
|
var RESULT_REF_PATTERN = /^q[0-9a-f]{8}$/;
|
|
3893
4571
|
var MANIFEST_FILE_NAME = "manifest.json";
|
|
3894
4572
|
function resultsRoot(saptoolsRoot) {
|
|
3895
|
-
return
|
|
4573
|
+
return join7(saptoolsRoot ?? join7(homedir5(), ".saptools"), "cf-hana", "results");
|
|
3896
4574
|
}
|
|
3897
4575
|
function sessionDirectory(ref, saptoolsRoot) {
|
|
3898
|
-
return
|
|
4576
|
+
return join7(resultsRoot(saptoolsRoot), ref);
|
|
3899
4577
|
}
|
|
3900
4578
|
function manifestPath(ref, saptoolsRoot) {
|
|
3901
|
-
return
|
|
4579
|
+
return join7(sessionDirectory(ref, saptoolsRoot), MANIFEST_FILE_NAME);
|
|
3902
4580
|
}
|
|
3903
4581
|
function encodeCell(value) {
|
|
3904
4582
|
if (value === null) {
|
|
@@ -3997,11 +4675,11 @@ function toStoredSession(input, ref, now) {
|
|
|
3997
4675
|
result: encodeResult(input.result)
|
|
3998
4676
|
};
|
|
3999
4677
|
}
|
|
4000
|
-
function
|
|
4678
|
+
function isRecord4(value) {
|
|
4001
4679
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4002
4680
|
}
|
|
4003
4681
|
function isStoredSession(value) {
|
|
4004
|
-
if (!
|
|
4682
|
+
if (!isRecord4(value) || !isRecord4(value["result"]) || !isRecord4(value["info"])) {
|
|
4005
4683
|
return false;
|
|
4006
4684
|
}
|
|
4007
4685
|
const result = value["result"];
|
|
@@ -4009,7 +4687,7 @@ function isStoredSession(value) {
|
|
|
4009
4687
|
}
|
|
4010
4688
|
async function readStoredSession(path) {
|
|
4011
4689
|
try {
|
|
4012
|
-
const parsed = JSON.parse(await
|
|
4690
|
+
const parsed = JSON.parse(await readFile3(path, "utf8"));
|
|
4013
4691
|
return isStoredSession(parsed) ? parsed : void 0;
|
|
4014
4692
|
} catch (error) {
|
|
4015
4693
|
if (error.code === "ENOENT") {
|
|
@@ -4028,7 +4706,7 @@ function toSession(stored, saptoolsRoot) {
|
|
|
4028
4706
|
}
|
|
4029
4707
|
async function listSessionRefs(saptoolsRoot) {
|
|
4030
4708
|
try {
|
|
4031
|
-
const entries = await
|
|
4709
|
+
const entries = await readdir3(resultsRoot(saptoolsRoot), { withFileTypes: true });
|
|
4032
4710
|
return entries.filter((entry) => entry.isDirectory() && RESULT_REF_PATTERN.test(entry.name)).map((entry) => entry.name);
|
|
4033
4711
|
} catch (error) {
|
|
4034
4712
|
if (error.code === "ENOENT") {
|
|
@@ -4049,17 +4727,17 @@ async function createResultSession(input, options = {}) {
|
|
|
4049
4727
|
const root = resultsRoot(options.saptoolsRoot);
|
|
4050
4728
|
const finalDirectory = sessionDirectory(ref, options.saptoolsRoot);
|
|
4051
4729
|
const tempDirectory = `${finalDirectory}.tmp-${process.pid.toString()}`;
|
|
4052
|
-
await
|
|
4053
|
-
await
|
|
4054
|
-
await
|
|
4730
|
+
await mkdir5(root, { recursive: true, mode: 448 });
|
|
4731
|
+
await rm6(tempDirectory, { recursive: true, force: true });
|
|
4732
|
+
await mkdir5(tempDirectory, { mode: 448 });
|
|
4055
4733
|
try {
|
|
4056
|
-
await
|
|
4734
|
+
await writeFile4(join7(tempDirectory, MANIFEST_FILE_NAME), serialized, {
|
|
4057
4735
|
encoding: "utf8",
|
|
4058
4736
|
mode: 384
|
|
4059
4737
|
});
|
|
4060
|
-
await
|
|
4738
|
+
await rename3(tempDirectory, finalDirectory);
|
|
4061
4739
|
} catch (error) {
|
|
4062
|
-
await
|
|
4740
|
+
await rm6(tempDirectory, { recursive: true, force: true });
|
|
4063
4741
|
throw error;
|
|
4064
4742
|
}
|
|
4065
4743
|
return toSession(stored, options.saptoolsRoot);
|
|
@@ -4102,7 +4780,7 @@ async function pruneResultSessions(options = {}) {
|
|
|
4102
4780
|
for (const ref of refs) {
|
|
4103
4781
|
const stored = await readStoredSession(manifestPath(ref, options.saptoolsRoot));
|
|
4104
4782
|
if (stored === void 0 || Date.parse(stored.expiresAt) <= now) {
|
|
4105
|
-
await
|
|
4783
|
+
await rm6(sessionDirectory(ref, options.saptoolsRoot), { recursive: true, force: true });
|
|
4106
4784
|
removed += 1;
|
|
4107
4785
|
}
|
|
4108
4786
|
}
|
|
@@ -4112,7 +4790,7 @@ async function clearResultSessions(options = {}) {
|
|
|
4112
4790
|
const refs = await listSessionRefs(options.saptoolsRoot);
|
|
4113
4791
|
await Promise.all(
|
|
4114
4792
|
refs.map(async (ref) => {
|
|
4115
|
-
await
|
|
4793
|
+
await rm6(sessionDirectory(ref, options.saptoolsRoot), { recursive: true, force: true });
|
|
4116
4794
|
})
|
|
4117
4795
|
);
|
|
4118
4796
|
return refs.length;
|
|
@@ -4282,7 +4960,7 @@ async function runExport(ref, options) {
|
|
|
4282
4960
|
}
|
|
4283
4961
|
const session = await readResultSession(ref);
|
|
4284
4962
|
const selected = selectResultCell(session, options.row, options.column);
|
|
4285
|
-
await
|
|
4963
|
+
await writeFile5(options.output, cellExportValue(selected.value, selected.typeName), {
|
|
4286
4964
|
mode: 384
|
|
4287
4965
|
});
|
|
4288
4966
|
print(`wrote=${options.output}`);
|
|
@@ -4350,8 +5028,12 @@ function printResolvedTarget(info) {
|
|
|
4350
5028
|
`
|
|
4351
5029
|
);
|
|
4352
5030
|
}
|
|
5031
|
+
function printTunnelStatus(message) {
|
|
5032
|
+
process.stderr.write(`${CLI_NAME}: ${message}
|
|
5033
|
+
`);
|
|
5034
|
+
}
|
|
4353
5035
|
async function connectForCli(selector, options) {
|
|
4354
|
-
const client = await connect(selector, options);
|
|
5036
|
+
const client = await connect(selector, { ...options, onTunnelStatus: printTunnelStatus });
|
|
4355
5037
|
printResolvedTarget(client.info);
|
|
4356
5038
|
return client;
|
|
4357
5039
|
}
|
|
@@ -4416,6 +5098,8 @@ function toConnectOptions(opts) {
|
|
|
4416
5098
|
readOnly: opts.readOnly,
|
|
4417
5099
|
allowDestructive: opts.allowDestructive,
|
|
4418
5100
|
autoLimit: opts.autoLimit ? opts.limit ?? DEFAULT_AUTO_LIMIT : false,
|
|
5101
|
+
tunnel: opts.tunnel,
|
|
5102
|
+
refreshTunnel: opts.refreshTunnel,
|
|
4419
5103
|
...opts.binding === void 0 ? {} : { bindingName: opts.binding },
|
|
4420
5104
|
...opts.bindingIndex === void 0 ? {} : { bindingIndex: opts.bindingIndex },
|
|
4421
5105
|
...opts.timeout === void 0 ? {} : { queryTimeoutMs: opts.timeout, connectTimeoutMs: opts.timeout }
|
|
@@ -4512,7 +5196,15 @@ function formatInfo(info) {
|
|
|
4512
5196
|
].join("\n");
|
|
4513
5197
|
}
|
|
4514
5198
|
function withConnectionOptions(command) {
|
|
4515
|
-
return command.option("--refresh", "deprecated compatibility flag; binding discovery is already live", false).option("--role <role>", "HANA user role: runtime or hdi", "runtime").option("--binding <name>", "select a HANA binding by service name").option("--binding-index <n>", "select a HANA binding by index", parseIntOption2).option("--read-only", "block every DML and DDL statement", false).option("--allow-destructive", "permit destructive statements", false).option("--timeout <ms>", "connection and query timeout in milliseconds", parseIntOption2).option("--limit <n>", "row cap auto-applied to bare SELECT statements", parseIntOption2).option("--no-auto-limit", "disable the automatic SELECT row cap").option("--refresh-metadata", "bypass the 30-minute table/view metadata suggestion cache", false)
|
|
5199
|
+
return command.option("--refresh", "deprecated compatibility flag; binding discovery is already live", false).option("--role <role>", "HANA user role: runtime or hdi", "runtime").option("--binding <name>", "select a HANA binding by service name").option("--binding-index <n>", "select a HANA binding by index", parseIntOption2).option("--read-only", "block every DML and DDL statement", false).option("--allow-destructive", "permit destructive statements", false).option("--timeout <ms>", "connection and query timeout in milliseconds", parseIntOption2).option("--limit <n>", "row cap auto-applied to bare SELECT statements", parseIntOption2).option("--no-auto-limit", "disable the automatic SELECT row cap").option("--refresh-metadata", "bypass the 30-minute table/view metadata suggestion cache", false).option(
|
|
5200
|
+
"--tunnel",
|
|
5201
|
+
"skip the direct connection attempt and connect via an SSH tunnel immediately",
|
|
5202
|
+
false
|
|
5203
|
+
).option(
|
|
5204
|
+
"--refresh-tunnel",
|
|
5205
|
+
"bypass a cached/live SSH tunnel and force a fresh establishment attempt",
|
|
5206
|
+
false
|
|
5207
|
+
);
|
|
4516
5208
|
}
|
|
4517
5209
|
function withFormattedConnectionOptions(command) {
|
|
4518
5210
|
return withConnectionOptions(command).option(
|