java-caller 2.2.4 → 2.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.
@@ -1,395 +1,416 @@
1
- #! /usr/bin/env node
2
- const debug = require("debug")("java-caller");
3
- const fse = require("fs-extra");
4
- const os = require("os");
5
- const path = require("path");
6
- const { spawn } = require("child_process");
7
- const util = require("util");
8
- const execPromise = util.promisify(require("child_process").exec);
9
-
10
- class JavaCaller {
11
- "use strict";
12
- minimumJavaVersion = 8;
13
- maximumJavaVersion;
14
- javaType;
15
- rootPath = ".";
16
-
17
- jar;
18
- classPath = ".";
19
- mainClass;
20
- output = "none"; // can be none or console
21
- status = null;
22
-
23
- javaSupportDir;
24
- javaExecutable = "java";
25
- additionalJavaArgs = [];
26
-
27
- javaHome;
28
- javaBin;
29
-
30
- prevPath;
31
- prevJavaHome;
32
-
33
- /**
34
- * Creates a JavaCaller instance
35
- * @param {object} [opts] - Run options
36
- * @param {string} [opts.jar] - Path to executable jar file
37
- * @param {string} [opts.classPath] - If jar parameter is not set, classpath to use. Use : as separator (it will be converted if runned on Windows)
38
- * @param {string} [opts.mainClass] - If classPath set, main class to call
39
- * @param {number} [opts.minimumJavaVersion] - Minimum java version to be used to call java command. If the java version found on machine is lower, java-caller will try to install and use the appropriate one
40
- * @param {number} [opts.maximumJavaVersion] - Maximum java version to be used to call java command. If the java version found on machine is upper, java-caller will try to install and use the appropriate one
41
- * @param {string} [opts.javaType] - jre or jdk (if not defined and installation is required, jre will be installed)
42
- * @param {string} [opts.rootPath] - If classPath elements are not relative to the current folder, you can define a root path. You may use __dirname if you classes / jars are in your module folder
43
- * @param {string} [opts.javaExecutable] - You can force to use a defined java executable, instead of letting java-caller find/install one
44
- * @param {string} [opts.additionalJavaArgs] - Additional parameters for JVM that will be added in every JavaCaller instance runs
45
- */
46
- constructor(opts) {
47
- this.jar = opts.jar || this.jar;
48
- this.classPath = opts.classPath || this.classPath;
49
- this.mainClass = opts.mainClass || this.mainClass;
50
- this.minimumJavaVersion = opts.minimumJavaVersion || this.minimumJavaVersion;
51
- this.maximumJavaVersion = opts.maximumJavaVersion || this.maximumJavaVersion;
52
- this.javaType = opts.javaType || this.javaType;
53
- this.rootPath = opts.rootPath || this.rootPath;
54
- this.javaCallerSupportDir = `${os.homedir() + path.sep}.java-caller`;
55
- this.javaExecutable = opts.javaExecutable || this.javaExecutable;
56
- this.additionalJavaArgs = opts.additionalJavaArgs || this.additionalJavaArgs;
57
- this.output = opts.output || this.output;
58
- }
59
-
60
- /**
61
- * Runs java command of a JavaCaller instance
62
- * @param {string[]} [userArguments] - Java command line arguments
63
- * @param {object} [runOptions] - Run options
64
- * @param {boolean} [runOptions.detached = false] - If set to true, node will node wait for the java command to be completed. In that case, childJavaProcess property will be returned, but stdout and stderr may be empty
65
- * @param {number} [runOptions.waitForErrorMs = 500] - If detached is true, number of milliseconds to wait to detect an error before exiting JavaCaller run
66
- * @param {string} [runOptions.cwd = .] - You can override cwd of spawn called by JavaCaller runner
67
- * @return {Promise<{status:number, stdout:string, stderr:string, childJavaProcess:ChildProcess}>} - Command result (status, stdout, stderr, childJavaProcess)
68
- */
69
- async run(userArguments, runOptions = {}) {
70
- runOptions.detached = runOptions.detached || false;
71
- runOptions.waitForErrorMs = runOptions.waitForErrorMs || 500;
72
- runOptions.cwd = runOptions.cwd || process.cwd();
73
-
74
- let javaExe = this.javaExecutable;
75
- if (javaExe.toLowerCase().includes(".exe") && !javaExe.includes(`'`)) {
76
- // Java executable has been overriden by caller : use it
77
- javaExe = `"${path.resolve(javaExe)}"`;
78
- } else {
79
- // Check if matching java version is present, install and update PATH if it is not
80
- await this.manageJavaInstall();
81
- }
82
-
83
- const classPathStr = this.classPath
84
- .split(":")
85
- .map(classPathElt => path.resolve(this.rootPath + path.sep + classPathElt))
86
- .join(path.delimiter);
87
- const javaArgs = this.buildArguments(classPathStr, (userArguments || []).concat(this.additionalJavaArgs));
88
- let stdout = "";
89
- let stderr = "";
90
- let child;
91
- const prom = new Promise(resolve => {
92
- // Spawn java command line
93
- debug(`Java command: ${javaExe} ${javaArgs.join(" ")}`);
94
- const spawnOptions = {
95
- detached: runOptions.detached,
96
- cwd: javaExe === "java" ? runOptions.cwd : undefined,
97
- env: Object.assign({}, process.env),
98
- stdio: this.output === "console" ? "inherit" : runOptions.detached ? "ignore" : "pipe",
99
- windowsHide: true,
100
- windowsVerbatimArguments: true
101
- };
102
- if (javaExe.includes(" ")) {
103
- spawnOptions.shell = true;
104
- }
105
- child = spawn(javaExe, javaArgs, spawnOptions);
106
-
107
- // Gather stdout and stderr if they must be returned
108
- if (spawnOptions.stdio === "pipe") {
109
- child.stdout.on("data", data => {
110
- stdout += data;
111
- });
112
- child.stderr.on("data", data => {
113
- stderr += data;
114
- });
115
- }
116
-
117
- // Catch error
118
- child.on("error", data => {
119
- this.status = 666;
120
- stderr += "Java spawn error: " + data;
121
- resolve();
122
- });
123
-
124
- // Catch status code
125
- child.on("close", code => {
126
- this.status = code;
127
- resolve();
128
- });
129
-
130
- // Detach from main process in case detached === true
131
- if (runOptions.detached) {
132
- child.unref();
133
- }
134
- });
135
-
136
- if (runOptions.detached) {
137
- // Detached mode: Just wait a little amount of time in case you want to check a command error
138
- await new Promise(resolve =>
139
- setTimeout(() => {
140
- resolve();
141
- }, runOptions.waitForErrorMs)
142
- );
143
- } else {
144
- // Not detached mode: wait for Promise to be resolved
145
- await prom;
146
- }
147
-
148
- // Build result
149
- const result = {
150
- status: this.status,
151
- stdout: stdout,
152
- stderr: stderr
153
- };
154
- if (child) {
155
- result.childJavaProcess = child;
156
- }
157
-
158
- // Restore previous values of PATH & JAVA_HOME
159
- process.env["PATH"] = this.prevPath || process.env["PATH"];
160
- process.env["JAVA_HOME"] = this.prevJavaHome || process.env["JAVA_HOME"];
161
-
162
- return result;
163
- }
164
-
165
- // Set first java arguments, then jar || classpath, then jar/class user arguments
166
- buildArguments(classPathStr, userArgs) {
167
- let javaArgs = [];
168
- let programArgs = [];
169
- for (const arg of userArgs) {
170
- if (arg.startsWith("-D") || arg.startsWith("-X")) {
171
- javaArgs.push(arg);
172
- } else {
173
- programArgs.push(arg);
174
- }
175
- }
176
- let allArgs = [];
177
- allArgs.push(...javaArgs);
178
- if (this.jar) {
179
- allArgs.push(...["-jar", os.platform() === "win32" ? `"${this.rootPath}/${this.jar}"` : `${this.rootPath}/${this.jar}`]);
180
- } else {
181
- allArgs.push(...["-cp", os.platform() === "win32" ? `"${classPathStr}"` : classPathStr, this.mainClass]);
182
- }
183
- allArgs.push(...programArgs);
184
- return allArgs;
185
- }
186
-
187
- // Install Java if the found java version is not matching the requirements
188
- async manageJavaInstall() {
189
- if (await this.getInstallInCache()) {
190
- return;
191
- }
192
- const javaVersion = await this.getJavaVersion();
193
- if (javaVersion === false || javaVersion < this.minimumJavaVersion || (this.maximumJavaVersion && javaVersion > this.maximumJavaVersion)) {
194
- // Check if the appropriate version has already been installed
195
- const { javaVersionHome = null, javaVersionBin = null } = await this.findJavaVersionHome();
196
- if (javaVersionHome) {
197
- // Matching java version has been found: use it
198
- this.javaHome = javaVersionHome;
199
- this.javaBin = javaVersionBin;
200
- await this.addJavaInPath();
201
- return;
202
- }
203
-
204
- // Inform user that the installation is pending
205
- const requiredMsg =
206
- this.minimumJavaVersion !== this.maximumJavaVersion
207
- ? `Java ${this.javaType ? this.javaType : "jre or jdk"} between ${this.minimumJavaVersion} and ${
208
- this.maximumJavaVersion
209
- } is required `
210
- : `Java ${this.javaType ? this.javaType : "jre or jdk"} ${this.minimumJavaVersion} is required`;
211
- console.log(requiredMsg);
212
- const javaVersionToInstall = this.maximumJavaVersion || this.minimumJavaVersion;
213
- const javaTypeToInstall = this.javaType || "jre";
214
- console.log(`Installing Java ${javaTypeToInstall} ${javaVersionToInstall} in ${this.javaCallerSupportDir}...`);
215
-
216
- // Create a directory for installing Java and ensure it contains a dummy package.json
217
- await fse.ensureDir(this.javaCallerSupportDir, { mode: "0777" });
218
- const packageJson = `${this.javaCallerSupportDir + path.sep}package.json`;
219
- if (!fse.existsSync(packageJson)) {
220
- const packageJsonContent = {
221
- name: "java-caller-support",
222
- version: "1.0.0",
223
- description: "Java installations by java-caller (https://github.com/nvuillam/node-java-caller)"
224
- };
225
- await fse.writeFile(packageJson, JSON.stringify(packageJsonContent), "utf8");
226
- }
227
-
228
- // Install appropriate java version using njre
229
- const njre = require("njre");
230
- const njreOptions = { type: javaTypeToInstall };
231
- const prevRequireMainFilename = require.main.filename; // hack require.main.filename to njre installs java where we want
232
- require.main.filename = packageJson;
233
- const installDir = await njre.install(this.maximumJavaVersion || this.minimumJavaVersion, njreOptions);
234
- require.main.filename = prevRequireMainFilename; // unhack require.main.filename
235
- console.log(`Installed Java ${javaTypeToInstall} ${javaVersionToInstall} in ${installDir}...`);
236
-
237
- // Call again this method: now matching java version will be found :)
238
- return await this.manageJavaInstall();
239
- }
240
- }
241
-
242
- // Get matching version in java-caller cache
243
- async getInstallInCache() {
244
- if (globalThis.JAVA_CALLER_VERSIONS_CACHE != null) {
245
- for (const { version, file, java_home, java_bin } of globalThis.JAVA_CALLER_VERSIONS_CACHE) {
246
- if (this.checkMatchingJavaVersion(version, file)) {
247
- this.javaHome = java_home;
248
- this.javaBin = java_bin;
249
- await this.addJavaInPath();
250
- return true;
251
- }
252
- }
253
- }
254
- return false;
255
- }
256
-
257
- // Returns system default java version
258
- async getJavaVersion() {
259
- try {
260
- const { stderr } = await execPromise("java -version");
261
- const match = /version "(.*?)"/.exec(stderr);
262
- const parts = match[1].split(".");
263
- let join = ".";
264
- let versionStr = "";
265
- for (const v of parts) {
266
- versionStr += v;
267
- if (join !== null) {
268
- versionStr += join;
269
- join = null;
270
- }
271
- }
272
- versionStr = versionStr.replace("_", "");
273
- let versionNb = parseFloat(versionStr);
274
- if (versionNb < 2) {
275
- versionStr = versionStr.substring(2);
276
- versionNb = parseFloat(versionStr[0] + "." + versionStr.substring(2));
277
- }
278
- debug(`Found default java version ${versionNb}`);
279
- return versionNb;
280
- } catch (e) {
281
- debug(`Java not found: ${e.message}`);
282
- return false;
283
- }
284
- }
285
-
286
- // Browse locally installed java versions
287
- // check if one matches with javaType , minimumJavaVersion and maximumJavaVersion
288
- async findJavaVersionHome() {
289
- let javaInstallsTopDir = this.javaCallerSupportDir + path.sep + "jre";
290
- if (fse.existsSync(javaInstallsTopDir)) {
291
- const dirContent = await fse.readdir(javaInstallsTopDir);
292
- let cacheFutureItem;
293
- let isFirst = true;
294
- const dirContentFiltered = dirContent.filter(file => {
295
- if (!fse.statSync(path.join(javaInstallsTopDir, file)).isDirectory()) {
296
- return false;
297
- }
298
- const versionMatches = file.includes("u")
299
- ? file.substring(0, file.indexOf("u")).match(/(\d+)/)
300
- : file.substring(0, file.indexOf(".")).match(/(\d+)/);
301
- const versionFound = parseInt(versionMatches[0], 10);
302
- const isMatching = this.checkMatchingJavaVersion(versionFound, file);
303
- if (isMatching && isFirst) {
304
- cacheFutureItem = { versionFound, file };
305
- isFirst = false;
306
- }
307
- return isMatching;
308
- });
309
- if (dirContentFiltered[0]) {
310
- const javaVersionHome = javaInstallsTopDir + path.sep + dirContentFiltered[0];
311
- const javaVersionBin = javaVersionHome + path.sep + this.getPlatformBinPath();
312
- if (fse.existsSync(javaVersionBin)) {
313
- debug(
314
- `Found matching java bin: ${javaVersionBin} for ${this.javaType ? this.javaType : "jre or jdk"} ${this.minimumJavaVersion}${
315
- this.maximumJavaVersion && this.maximumJavaVersion !== this.minimumJavaVersion ? " -> " + this.maximumJavaVersion : "+"
316
- }`
317
- );
318
- this.addInCache(cacheFutureItem.versionFound, cacheFutureItem.file, javaVersionHome, javaVersionBin);
319
- return { javaVersionHome, javaVersionBin };
320
- }
321
- }
322
- }
323
- return {};
324
- }
325
-
326
- checkMatchingJavaVersion(versionFound, file) {
327
- if (versionFound < this.minimumJavaVersion) {
328
- return false;
329
- }
330
- if (this.maximumJavaVersion != null && versionFound > this.maximumJavaVersion) {
331
- return false;
332
- }
333
- if (this.javaType === "jre" && !file.includes("jre")) {
334
- return false;
335
- } else if (this.javaType === "jdk" && file.includes("jre")) {
336
- return false;
337
- }
338
- return true;
339
- }
340
-
341
- // Add java bin dir in PATH or JAVA_HOME
342
- async addJavaInPath() {
343
- this.prevPath = process.env["PATH"];
344
- this.prevJavaHome = process.env["JAVA_HOME"];
345
- // Add java-caller installed java version in PATH
346
- if (this.javaBin) {
347
- process.env["PATH"] = process.env["PATH"].includes(this.javaBin)
348
- ? process.env["PATH"]
349
- : this.javaBin + path.delimiter + process.env["PATH"];
350
- process.env["JAVA_HOME"] = this.javaHome;
351
- }
352
- // If JAVA_HOME is set, but not jdk or jre, add it in PATH
353
- else if (process.env["JAVA_HOME"] && !process.env["PATH"].includes("jdk") && !process.env["PATH"].includes("jre")) {
354
- process.env["PATH"] = process.env["PATH"].includes(process.env["JAVA_HOME"])
355
- ? process.env["PATH"]
356
- : process.env["JAVA_HOME"] + path.sep + this.getPlatformBinPath() + path.delimiter + process.env["PATH"];
357
- }
358
- if (process.env["PATH"] !== this.prevPath) {
359
- debug("Updated PATH with value: " + process.env["PATH"]);
360
- }
361
- }
362
-
363
- getPlatformBinPath() {
364
- const platform = os.platform();
365
- let binPath;
366
- switch (platform) {
367
- case "darwin":
368
- binPath = `Contents${path.sep}Home${path.sep}bin`;
369
- break;
370
- case "win32":
371
- binPath = "bin";
372
- break;
373
- case "linux":
374
- binPath = "bin";
375
- break;
376
- default:
377
- this.fail(`unsupported platform: ${platform}`);
378
- }
379
- return binPath;
380
- }
381
-
382
- fail(reason) {
383
- console.error(reason);
384
- this.status = 666;
385
- }
386
-
387
- addInCache(version, file, java_home, java_bin) {
388
- if (globalThis.JAVA_CALLER_VERSIONS_CACHE == null) {
389
- globalThis.JAVA_CALLER_VERSIONS_CACHE = [];
390
- }
391
- globalThis.JAVA_CALLER_VERSIONS_CACHE.push({ version, file, java_home, java_bin });
392
- }
393
- }
394
-
395
- module.exports = { JavaCaller };
1
+ #! /usr/bin/env node
2
+ const debug = require("debug")("java-caller");
3
+ const fse = require("fs-extra");
4
+ const os = require("os");
5
+ const path = require("path");
6
+ const { spawn } = require("child_process");
7
+ const util = require("util");
8
+ const execPromise = util.promisify(require("child_process").exec);
9
+
10
+ class JavaCaller {
11
+ "use strict";
12
+ minimumJavaVersion = 8;
13
+ maximumJavaVersion;
14
+ javaType;
15
+ rootPath = ".";
16
+
17
+ jar;
18
+ classPath = ".";
19
+ useAbsoluteClassPaths = false;
20
+ mainClass;
21
+ output = "none"; // can be none or console
22
+ status = null;
23
+
24
+ javaSupportDir;
25
+ javaExecutable = "java";
26
+ additionalJavaArgs = [];
27
+ commandJavaArgs = [];
28
+
29
+ javaHome;
30
+ javaBin;
31
+
32
+ prevPath;
33
+ prevJavaHome;
34
+
35
+ /**
36
+ * Creates a JavaCaller instance
37
+ * @param {object} [opts] - Run options
38
+ * @param {string} [opts.jar] - Path to executable jar file
39
+ * @param {string | string[]} [opts.classPath] - If jar parameter is not set, classpath to use. Use : as separator (it will be converted if runned on Windows), or use a string array
40
+ * @param {boolean} [opts.useAbsoluteClassPaths] - Set to true if classpaths should not be based on the rootPath
41
+ * @param {string} [opts.mainClass] - If classPath set, main class to call
42
+ * @param {number} [opts.minimumJavaVersion] - Minimum java version to be used to call java command. If the java version found on machine is lower, java-caller will try to install and use the appropriate one
43
+ * @param {number} [opts.maximumJavaVersion] - Maximum java version to be used to call java command. If the java version found on machine is upper, java-caller will try to install and use the appropriate one
44
+ * @param {string} [opts.javaType] - jre or jdk (if not defined and installation is required, jre will be installed)
45
+ * @param {string} [opts.rootPath] - If classPath elements are not relative to the current folder, you can define a root path. You may use __dirname if you classes / jars are in your module folder
46
+ * @param {string} [opts.javaExecutable] - You can force to use a defined java executable, instead of letting java-caller find/install one
47
+ * @param {string} [opts.additionalJavaArgs] - Additional parameters for JVM that will be added in every JavaCaller instance runs
48
+ */
49
+ constructor(opts) {
50
+ this.jar = opts.jar || this.jar;
51
+ this.classPath = opts.classPath || this.classPath;
52
+ this.useAbsoluteClassPaths = opts.useAbsoluteClassPaths || this.useAbsoluteClassPaths;
53
+ this.mainClass = opts.mainClass || this.mainClass;
54
+ this.minimumJavaVersion = opts.minimumJavaVersion || this.minimumJavaVersion;
55
+ this.maximumJavaVersion = opts.maximumJavaVersion || this.maximumJavaVersion;
56
+ this.javaType = opts.javaType || this.javaType;
57
+ this.rootPath = opts.rootPath || this.rootPath;
58
+ this.javaCallerSupportDir = `${os.homedir() + path.sep}.java-caller`;
59
+ this.javaExecutable = opts.javaExecutable || this.javaExecutable;
60
+ this.additionalJavaArgs = opts.additionalJavaArgs || this.additionalJavaArgs;
61
+ this.output = opts.output || this.output;
62
+ }
63
+
64
+ /**
65
+ * Runs java command of a JavaCaller instance
66
+ * @param {string[]} [userArguments] - Java command line arguments
67
+ * @param {object} [runOptions] - Run options
68
+ * @param {boolean} [runOptions.detached = false] - If set to true, node will node wait for the java command to be completed. In that case, childJavaProcess property will be returned, but stdout and stderr may be empty
69
+ * @param {number} [runOptions.waitForErrorMs = 500] - If detached is true, number of milliseconds to wait to detect an error before exiting JavaCaller run
70
+ * @param {string} [runOptions.cwd = .] - You can override cwd of spawn called by JavaCaller runner
71
+ * @param {string} [runOptions.javaArgs = []] - You can override cwd of spawn called by JavaCaller runner
72
+ * @return {Promise<{status:number, stdout:string, stderr:string, childJavaProcess:ChildProcess}>} - Command result (status, stdout, stderr, childJavaProcess)
73
+ */
74
+ async run(userArguments, runOptions = {}) {
75
+ runOptions.detached = runOptions.detached || false;
76
+ runOptions.waitForErrorMs = runOptions.waitForErrorMs || 500;
77
+ runOptions.cwd = runOptions.cwd || process.cwd();
78
+ this.commandJavaArgs = (runOptions.javaArgs || []).concat(this.additionalJavaArgs);
79
+
80
+ let javaExe = this.javaExecutable;
81
+ if (javaExe.toLowerCase().includes(".exe") && !javaExe.includes(`'`)) {
82
+ // Java executable has been overridden by caller : use it
83
+ javaExe = `"${path.resolve(javaExe)}"`;
84
+ } else {
85
+ // Check if matching java version is present, install and update PATH if it is not
86
+ await this.manageJavaInstall();
87
+ }
88
+
89
+ const classPathStr = this.buildClasspathStr();
90
+
91
+ const javaArgs = this.buildArguments(classPathStr, (userArguments || []).concat(this.commandJavaArgs));
92
+ let stdout = "";
93
+ let stderr = "";
94
+ let child;
95
+ const prom = new Promise((resolve) => {
96
+ // Spawn java command line
97
+ debug(`Java command: ${javaExe} ${javaArgs.join(" ")}`);
98
+ const spawnOptions = {
99
+ detached: runOptions.detached,
100
+ cwd: javaExe === "java" ? runOptions.cwd : undefined,
101
+ env: Object.assign({}, process.env),
102
+ stdio: this.output === "console" ? "inherit" : runOptions.detached ? "ignore" : "pipe",
103
+ windowsHide: true,
104
+ windowsVerbatimArguments: true,
105
+ };
106
+ if (javaExe.includes(" ")) {
107
+ spawnOptions.shell = true;
108
+ }
109
+ child = spawn(javaExe, javaArgs, spawnOptions);
110
+
111
+ // Gather stdout and stderr if they must be returned
112
+ if (spawnOptions.stdio === "pipe") {
113
+ child.stdout.on("data", (data) => {
114
+ stdout += data;
115
+ });
116
+ child.stderr.on("data", (data) => {
117
+ stderr += data;
118
+ });
119
+ }
120
+
121
+ // Catch error
122
+ child.on("error", (data) => {
123
+ this.status = 666;
124
+ stderr += "Java spawn error: " + data;
125
+ resolve();
126
+ });
127
+
128
+ // Catch status code
129
+ child.on("close", (code) => {
130
+ this.status = code;
131
+ resolve();
132
+ });
133
+
134
+ // Detach from main process in case detached === true
135
+ if (runOptions.detached) {
136
+ child.unref();
137
+ }
138
+ });
139
+
140
+ if (runOptions.detached) {
141
+ // Detached mode: Just wait a little amount of time in case you want to check a command error
142
+ await new Promise((resolve) =>
143
+ setTimeout(() => {
144
+ resolve();
145
+ }, runOptions.waitForErrorMs)
146
+ );
147
+ } else {
148
+ // Not detached mode: wait for Promise to be resolved
149
+ await prom;
150
+ }
151
+
152
+ // Build result
153
+ const result = {
154
+ status: this.status,
155
+ stdout: stdout,
156
+ stderr: stderr,
157
+ };
158
+ if (child) {
159
+ result.childJavaProcess = child;
160
+ }
161
+
162
+ // Restore previous values of PATH & JAVA_HOME
163
+ process.env["PATH"] = this.prevPath || process.env["PATH"];
164
+ process.env["JAVA_HOME"] = this.prevJavaHome || process.env["JAVA_HOME"];
165
+
166
+ return result;
167
+ }
168
+
169
+ // Translate the classpath from a string or string array into a string
170
+ buildClasspathStr() {
171
+ let classPathList = [];
172
+
173
+ if (typeof this.classPath === "string") {
174
+ classPathList = this.classPath.split(":");
175
+ } else {
176
+ classPathList = this.classPath;
177
+ }
178
+
179
+ if (!this.useAbsoluteClassPaths) {
180
+ classPathList = classPathList.map((classPathElt) => path.resolve(this.rootPath + path.sep + classPathElt));
181
+ }
182
+
183
+ return classPathList.join(path.delimiter);
184
+ }
185
+
186
+ // Set first java arguments, then jar || classpath, then jar/class user arguments
187
+ buildArguments(classPathStr, userArgs) {
188
+ let javaArgs = [];
189
+ let programArgs = [];
190
+ for (const arg of userArgs) {
191
+ if (arg.startsWith("-D") || arg.startsWith("-X") || this.commandJavaArgs.includes(arg)) {
192
+ javaArgs.push(arg);
193
+ } else {
194
+ programArgs.push(arg);
195
+ }
196
+ }
197
+ let allArgs = [];
198
+ allArgs.push(...javaArgs);
199
+ if (this.jar) {
200
+ allArgs.push(...["-jar", os.platform() === "win32" ? `"${this.rootPath}/${this.jar}"` : `${this.rootPath}/${this.jar}`]);
201
+ } else {
202
+ allArgs.push(...["-cp", os.platform() === "win32" ? `"${classPathStr}"` : classPathStr, this.mainClass]);
203
+ }
204
+ allArgs.push(...programArgs);
205
+ return allArgs;
206
+ }
207
+
208
+ // Install Java if the found java version is not matching the requirements
209
+ async manageJavaInstall() {
210
+ if (await this.getInstallInCache()) {
211
+ return;
212
+ }
213
+ const javaVersion = await this.getJavaVersion();
214
+ if (javaVersion === false || javaVersion < this.minimumJavaVersion || (this.maximumJavaVersion && javaVersion > this.maximumJavaVersion)) {
215
+ // Check if the appropriate version has already been installed
216
+ const { javaVersionHome = null, javaVersionBin = null } = await this.findJavaVersionHome();
217
+ if (javaVersionHome) {
218
+ // Matching java version has been found: use it
219
+ this.javaHome = javaVersionHome;
220
+ this.javaBin = javaVersionBin;
221
+ await this.addJavaInPath();
222
+ return;
223
+ }
224
+
225
+ // Inform user that the installation is pending
226
+ const requiredMsg =
227
+ this.minimumJavaVersion !== this.maximumJavaVersion
228
+ ? `Java ${this.javaType ? this.javaType : "jre or jdk"} between ${this.minimumJavaVersion} and ${
229
+ this.maximumJavaVersion
230
+ } is required `
231
+ : `Java ${this.javaType ? this.javaType : "jre or jdk"} ${this.minimumJavaVersion} is required`;
232
+ console.log(requiredMsg);
233
+ const javaVersionToInstall = this.maximumJavaVersion || this.minimumJavaVersion;
234
+ const javaTypeToInstall = this.javaType || "jre";
235
+ console.log(`Installing Java ${javaTypeToInstall} ${javaVersionToInstall} in ${this.javaCallerSupportDir}...`);
236
+
237
+ // Create a directory for installing Java and ensure it contains a dummy package.json
238
+ await fse.ensureDir(this.javaCallerSupportDir, { mode: "0777" });
239
+ const packageJson = `${this.javaCallerSupportDir + path.sep}package.json`;
240
+ if (!fse.existsSync(packageJson)) {
241
+ const packageJsonContent = {
242
+ name: "java-caller-support",
243
+ version: "1.0.0",
244
+ description: "Java installations by java-caller (https://github.com/nvuillam/node-java-caller)",
245
+ };
246
+ await fse.writeFile(packageJson, JSON.stringify(packageJsonContent), "utf8");
247
+ }
248
+
249
+ // Install appropriate java version using njre
250
+ const njre = require("njre");
251
+ const njreOptions = { type: javaTypeToInstall };
252
+ const prevRequireMainFilename = require.main.filename; // hack require.main.filename to njre installs java where we want
253
+ require.main.filename = packageJson;
254
+ const installDir = await njre.install(this.maximumJavaVersion || this.minimumJavaVersion, njreOptions);
255
+ require.main.filename = prevRequireMainFilename; // unhack require.main.filename
256
+ console.log(`Installed Java ${javaTypeToInstall} ${javaVersionToInstall} in ${installDir}...`);
257
+
258
+ // Call again this method: now matching java version will be found :)
259
+ return await this.manageJavaInstall();
260
+ }
261
+ }
262
+
263
+ // Get matching version in java-caller cache
264
+ async getInstallInCache() {
265
+ if (globalThis.JAVA_CALLER_VERSIONS_CACHE != null) {
266
+ for (const { version, file, java_home, java_bin } of globalThis.JAVA_CALLER_VERSIONS_CACHE) {
267
+ if (this.checkMatchingJavaVersion(version, file)) {
268
+ this.javaHome = java_home;
269
+ this.javaBin = java_bin;
270
+ await this.addJavaInPath();
271
+ return true;
272
+ }
273
+ }
274
+ }
275
+ return false;
276
+ }
277
+
278
+ // Returns system default java version
279
+ async getJavaVersion() {
280
+ try {
281
+ const { stderr } = await execPromise("java -version");
282
+ const match = /version "(.*?)"/.exec(stderr);
283
+ const parts = match[1].split(".");
284
+ let join = ".";
285
+ let versionStr = "";
286
+ for (const v of parts) {
287
+ versionStr += v;
288
+ if (join !== null) {
289
+ versionStr += join;
290
+ join = null;
291
+ }
292
+ }
293
+ versionStr = versionStr.replace("_", "");
294
+ let versionNb = parseFloat(versionStr);
295
+ if (versionNb < 2) {
296
+ versionStr = versionStr.substring(2);
297
+ versionNb = parseFloat(versionStr[0] + "." + versionStr.substring(2));
298
+ }
299
+ debug(`Found default java version ${versionNb}`);
300
+ return versionNb;
301
+ } catch (e) {
302
+ debug(`Java not found: ${e.message}`);
303
+ return false;
304
+ }
305
+ }
306
+
307
+ // Browse locally installed java versions
308
+ // check if one matches with javaType , minimumJavaVersion and maximumJavaVersion
309
+ async findJavaVersionHome() {
310
+ let javaInstallsTopDir = this.javaCallerSupportDir + path.sep + "jre";
311
+ if (fse.existsSync(javaInstallsTopDir)) {
312
+ const dirContent = await fse.readdir(javaInstallsTopDir);
313
+ let cacheFutureItem;
314
+ let isFirst = true;
315
+ const dirContentFiltered = dirContent.filter((file) => {
316
+ if (!fse.statSync(path.join(javaInstallsTopDir, file)).isDirectory()) {
317
+ return false;
318
+ }
319
+ const versionMatches = file.includes("u")
320
+ ? file.substring(0, file.indexOf("u")).match(/(\d+)/)
321
+ : file.substring(0, file.indexOf(".")).match(/(\d+)/);
322
+ const versionFound = parseInt(versionMatches[0], 10);
323
+ const isMatching = this.checkMatchingJavaVersion(versionFound, file);
324
+ if (isMatching && isFirst) {
325
+ cacheFutureItem = { versionFound, file };
326
+ isFirst = false;
327
+ }
328
+ return isMatching;
329
+ });
330
+ if (dirContentFiltered[0]) {
331
+ const javaVersionHome = javaInstallsTopDir + path.sep + dirContentFiltered[0];
332
+ const javaVersionBin = javaVersionHome + path.sep + this.getPlatformBinPath();
333
+ if (fse.existsSync(javaVersionBin)) {
334
+ debug(
335
+ `Found matching java bin: ${javaVersionBin} for ${this.javaType ? this.javaType : "jre or jdk"} ${this.minimumJavaVersion}${
336
+ this.maximumJavaVersion && this.maximumJavaVersion !== this.minimumJavaVersion ? " -> " + this.maximumJavaVersion : "+"
337
+ }`
338
+ );
339
+ this.addInCache(cacheFutureItem.versionFound, cacheFutureItem.file, javaVersionHome, javaVersionBin);
340
+ return { javaVersionHome, javaVersionBin };
341
+ }
342
+ }
343
+ }
344
+ return {};
345
+ }
346
+
347
+ checkMatchingJavaVersion(versionFound, file) {
348
+ if (versionFound < this.minimumJavaVersion) {
349
+ return false;
350
+ }
351
+ if (this.maximumJavaVersion != null && versionFound > this.maximumJavaVersion) {
352
+ return false;
353
+ }
354
+ if (this.javaType === "jre" && !file.includes("jre")) {
355
+ return false;
356
+ } else if (this.javaType === "jdk" && file.includes("jre")) {
357
+ return false;
358
+ }
359
+ return true;
360
+ }
361
+
362
+ // Add java bin dir in PATH or JAVA_HOME
363
+ async addJavaInPath() {
364
+ this.prevPath = process.env["PATH"];
365
+ this.prevJavaHome = process.env["JAVA_HOME"];
366
+ // Add java-caller installed java version in PATH
367
+ if (this.javaBin) {
368
+ process.env["PATH"] = process.env["PATH"].includes(this.javaBin)
369
+ ? process.env["PATH"]
370
+ : this.javaBin + path.delimiter + process.env["PATH"];
371
+ process.env["JAVA_HOME"] = this.javaHome;
372
+ }
373
+ // If JAVA_HOME is set, but not jdk or jre, add it in PATH
374
+ else if (process.env["JAVA_HOME"] && !process.env["PATH"].includes("jdk") && !process.env["PATH"].includes("jre")) {
375
+ process.env["PATH"] = process.env["PATH"].includes(process.env["JAVA_HOME"])
376
+ ? process.env["PATH"]
377
+ : process.env["JAVA_HOME"] + path.sep + this.getPlatformBinPath() + path.delimiter + process.env["PATH"];
378
+ }
379
+ if (process.env["PATH"] !== this.prevPath) {
380
+ debug("Updated PATH with value: " + process.env["PATH"]);
381
+ }
382
+ }
383
+
384
+ getPlatformBinPath() {
385
+ const platform = os.platform();
386
+ let binPath;
387
+ switch (platform) {
388
+ case "darwin":
389
+ binPath = `Contents${path.sep}Home${path.sep}bin`;
390
+ break;
391
+ case "win32":
392
+ binPath = "bin";
393
+ break;
394
+ case "linux":
395
+ binPath = "bin";
396
+ break;
397
+ default:
398
+ this.fail(`unsupported platform: ${platform}`);
399
+ }
400
+ return binPath;
401
+ }
402
+
403
+ fail(reason) {
404
+ console.error(reason);
405
+ this.status = 666;
406
+ }
407
+
408
+ addInCache(version, file, java_home, java_bin) {
409
+ if (globalThis.JAVA_CALLER_VERSIONS_CACHE == null) {
410
+ globalThis.JAVA_CALLER_VERSIONS_CACHE = [];
411
+ }
412
+ globalThis.JAVA_CALLER_VERSIONS_CACHE.push({ version, file, java_home, java_bin });
413
+ }
414
+ }
415
+
416
+ module.exports = { JavaCaller };