js4j 0.1.1 → 0.1.3
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/README.md +68 -0
- package/index.d.ts +6 -0
- package/package.json +1 -1
- package/src/exceptions.js +15 -0
- package/src/launcher.js +107 -1
package/README.md
CHANGED
|
@@ -35,6 +35,9 @@ A Node.js implementation of [py4j](https://www.py4j.org/) — a bridge between J
|
|
|
35
35
|
- [ProxyPool](#proxypool)
|
|
36
36
|
- [launchGateway](#launchgateway)
|
|
37
37
|
- [Errors](#errors)
|
|
38
|
+
- [Known Limitations](#known-limitations)
|
|
39
|
+
- [Varargs methods](#varargs-methods)
|
|
40
|
+
- [Fields vs methods](#fields-vs-methods)
|
|
38
41
|
- [TypeScript](#typescript)
|
|
39
42
|
- [Credits](#credits)
|
|
40
43
|
|
|
@@ -643,6 +646,70 @@ try {
|
|
|
643
646
|
|
|
644
647
|
---
|
|
645
648
|
|
|
649
|
+
## Known Limitations
|
|
650
|
+
|
|
651
|
+
### Varargs methods
|
|
652
|
+
|
|
653
|
+
Java compiles `T... args` varargs parameters to `T[]` in bytecode. When js4j forwards multiple bare JavaScript arguments, the Java reflection engine looks for a method with that exact number of parameters and fails with "method not found".
|
|
654
|
+
|
|
655
|
+
**Rule: pack all varargs arguments into a Java array created with `gateway.newArray()`.**
|
|
656
|
+
|
|
657
|
+
```js
|
|
658
|
+
// Java signature: Paths.get(String first, String... more)
|
|
659
|
+
|
|
660
|
+
// WRONG — Java looks for get(String, String, String), which doesn't exist
|
|
661
|
+
const path = await Paths.get('/tmp', 'subdir', 'file.txt');
|
|
662
|
+
|
|
663
|
+
// CORRECT — single-argument call (empty varargs), works fine
|
|
664
|
+
const path = await Paths.get('/tmp/subdir/file.txt');
|
|
665
|
+
|
|
666
|
+
// CORRECT — varargs passed as a String array
|
|
667
|
+
const more = await gateway.newArray(gateway.jvm.java.lang.String, 2);
|
|
668
|
+
await more.set(0, 'subdir');
|
|
669
|
+
await more.set(1, 'file.txt');
|
|
670
|
+
const path = await Paths.get('/tmp', more);
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
The same pattern applies to any varargs method:
|
|
674
|
+
|
|
675
|
+
```js
|
|
676
|
+
// Java signature: String.format(String format, Object... args)
|
|
677
|
+
const fmtArgs = await gateway.newArray(gateway.jvm.java.lang.Object, 2);
|
|
678
|
+
await fmtArgs.set(0, 'World');
|
|
679
|
+
await fmtArgs.set(1, 42);
|
|
680
|
+
const result = await gateway.jvm.java.lang.String.format('Hello %s, you are number %d', fmtArgs);
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
For simple cases it is often easier to avoid varargs entirely by building the value in JavaScript first:
|
|
684
|
+
|
|
685
|
+
```js
|
|
686
|
+
// Build the path string in JS rather than using multi-part Paths.get
|
|
687
|
+
const path = await Paths.get(`${baseDir}/subdir/file.txt`);
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Fields vs methods
|
|
691
|
+
|
|
692
|
+
Property access on a `JavaObject` or `JavaClass` is reserved for **method calls**. Directly reading or writing a public Java field with `obj.fieldName` or `obj.fieldName = value` will only affect the local JavaScript proxy object — it will not communicate with the JVM.
|
|
693
|
+
|
|
694
|
+
Always use `getField` / `setField`:
|
|
695
|
+
|
|
696
|
+
```js
|
|
697
|
+
// WRONG — only sets a JS property, Java never sees it
|
|
698
|
+
obj.count = 10;
|
|
699
|
+
|
|
700
|
+
// CORRECT
|
|
701
|
+
await gateway.setField(obj, 'count', 10);
|
|
702
|
+
const count = await gateway.getField(obj, 'count');
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
This applies to static fields too:
|
|
706
|
+
|
|
707
|
+
```js
|
|
708
|
+
const pi = await gateway.getField(gateway.jvm.java.lang.Math, 'PI');
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
646
713
|
## TypeScript
|
|
647
714
|
|
|
648
715
|
Full type definitions are included. No `@types/` package is needed.
|
|
@@ -698,6 +765,7 @@ await kill();
|
|
|
698
765
|
| `readyPattern` | `RegExp \| string \| null` | `/GATEWAY_STARTED/` | Pattern matched against stdout to detect when the server is ready. Set to `null` to skip stdout checking and rely only on port polling. |
|
|
699
766
|
| `timeout` | `number` | `30000` | Maximum milliseconds to wait for the server to become ready. |
|
|
700
767
|
| `gatewayOptions` | `GatewayParametersOptions` | `{}` | Extra options forwarded to `GatewayParameters` (e.g. `authToken`). |
|
|
768
|
+
| `killConflict` | `boolean` | `false` | If `true`, detect any process already listening on the target port and kill it before launching. Throws if the port cannot be freed within 5 seconds. |
|
|
701
769
|
|
|
702
770
|
#### Return value
|
|
703
771
|
|
package/index.d.ts
CHANGED
|
@@ -401,6 +401,12 @@ export interface LaunchGatewayOptions {
|
|
|
401
401
|
timeout?: number;
|
|
402
402
|
/** Extra options forwarded to GatewayParameters. */
|
|
403
403
|
gatewayOptions?: GatewayParametersOptions;
|
|
404
|
+
/**
|
|
405
|
+
* If true, detect any process already listening on the target port and send
|
|
406
|
+
* it SIGTERM before launching. An error is thrown if the port cannot be
|
|
407
|
+
* freed within 5 seconds. Default: false.
|
|
408
|
+
*/
|
|
409
|
+
killConflict?: boolean;
|
|
404
410
|
}
|
|
405
411
|
|
|
406
412
|
export interface LaunchGatewayResult {
|
package/package.json
CHANGED
package/src/exceptions.js
CHANGED
|
@@ -33,6 +33,21 @@ class Js4JJavaError extends Js4JError {
|
|
|
33
33
|
this.javaException = javaException || null;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Fetch the human-readable Java exception message by calling getMessage()
|
|
38
|
+
* on the Java Throwable object. Falls back to the raw protocol payload
|
|
39
|
+
* (javaExceptionMessage) if the Java call fails or javaException is null.
|
|
40
|
+
* @returns {Promise<string>}
|
|
41
|
+
*/
|
|
42
|
+
async getJavaMessage() {
|
|
43
|
+
if (!this.javaException) return this.javaExceptionMessage || this.message;
|
|
44
|
+
try {
|
|
45
|
+
return await this.javaException.getMessage();
|
|
46
|
+
} catch (_) {
|
|
47
|
+
return this.javaExceptionMessage || this.message;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
toString() {
|
|
37
52
|
return `${this.name}: ${this.message}\n Java exception payload: ${this.javaExceptionMessage}`;
|
|
38
53
|
}
|
package/src/launcher.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { spawn } = require('child_process');
|
|
3
|
+
const { spawn, execSync } = require('child_process');
|
|
4
4
|
const net = require('net');
|
|
5
5
|
const { JavaGateway, GatewayParameters } = require('./gateway');
|
|
6
6
|
|
|
@@ -20,6 +20,8 @@ const { JavaGateway, GatewayParameters } = require('./gateway');
|
|
|
20
20
|
* and rely only on port polling.
|
|
21
21
|
* @param {number} [options.timeout=30000] - Max ms to wait for the server to be ready
|
|
22
22
|
* @param {object} [options.gatewayOptions] - Extra options forwarded to GatewayParameters
|
|
23
|
+
* @param {boolean} [options.killConflict=false] - If true, detect and kill any process already
|
|
24
|
+
* listening on the target port before launching.
|
|
23
25
|
*
|
|
24
26
|
* @returns {Promise<{ process: ChildProcess, gateway: JavaGateway, kill: Function }>}
|
|
25
27
|
*
|
|
@@ -47,11 +49,16 @@ async function launchGateway(options = {}) {
|
|
|
47
49
|
readyPattern = /GATEWAY_STARTED/,
|
|
48
50
|
timeout = 30000,
|
|
49
51
|
gatewayOptions = {},
|
|
52
|
+
killConflict = false,
|
|
50
53
|
} = options;
|
|
51
54
|
|
|
52
55
|
if (!classpath) throw new Error('launchGateway: options.classpath is required');
|
|
53
56
|
if (!mainClass) throw new Error('launchGateway: options.mainClass is required');
|
|
54
57
|
|
|
58
|
+
if (killConflict) {
|
|
59
|
+
await _checkAndKillConflict(host, port);
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
// Build the java command: java [jvmArgs] -cp <classpath> <mainClass> [args]
|
|
56
63
|
const javaArgs = [...jvmArgs, '-cp', classpath, mainClass, ...args];
|
|
57
64
|
|
|
@@ -83,6 +90,105 @@ async function launchGateway(options = {}) {
|
|
|
83
90
|
// Internal helpers
|
|
84
91
|
// ---------------------------------------------------------------------------
|
|
85
92
|
|
|
93
|
+
/**
|
|
94
|
+
* If something is already listening on host:port, find its PID(s) and kill them,
|
|
95
|
+
* then wait for the port to become free.
|
|
96
|
+
*/
|
|
97
|
+
async function _checkAndKillConflict(host, port) {
|
|
98
|
+
const inUse = await _isPortInUse(host, port);
|
|
99
|
+
if (!inUse) return;
|
|
100
|
+
|
|
101
|
+
const pids = _getPidsOnPort(port);
|
|
102
|
+
if (pids.length === 0) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`launchGateway: port ${port} is already in use but no owning process could be found. ` +
|
|
105
|
+
`Free the port manually and retry.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const pid of pids) {
|
|
110
|
+
try {
|
|
111
|
+
process.kill(pid, 'SIGTERM');
|
|
112
|
+
} catch (_) {
|
|
113
|
+
// process may have already exited
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await _waitForPortFree(host, port, 5000);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Return true if something is already accepting connections on host:port.
|
|
122
|
+
*/
|
|
123
|
+
function _isPortInUse(host, port) {
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
const sock = new net.Socket();
|
|
126
|
+
sock.setTimeout(500);
|
|
127
|
+
sock.on('connect', () => { sock.destroy(); resolve(true); });
|
|
128
|
+
sock.on('error', () => { sock.destroy(); resolve(false); });
|
|
129
|
+
sock.on('timeout', () => { sock.destroy(); resolve(false); });
|
|
130
|
+
sock.connect(port, host);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Find the PID(s) of whatever is listening on the given port.
|
|
136
|
+
* Uses lsof on Linux/macOS and netstat on Windows.
|
|
137
|
+
* Returns an array of integer PIDs (may be empty if detection fails).
|
|
138
|
+
*/
|
|
139
|
+
function _getPidsOnPort(port) {
|
|
140
|
+
try {
|
|
141
|
+
if (process.platform === 'win32') {
|
|
142
|
+
// netstat -ano lists active connections; parse LISTENING lines for the port
|
|
143
|
+
const out = execSync(`netstat -ano`, { encoding: 'utf8', stdio: 'pipe' });
|
|
144
|
+
const pids = new Set();
|
|
145
|
+
for (const line of out.split('\n')) {
|
|
146
|
+
// Format: " TCP 0.0.0.0:25333 0.0.0.0:0 LISTENING 1234"
|
|
147
|
+
if (!line.includes('LISTENING')) continue;
|
|
148
|
+
if (!line.includes(`:${port}`)) continue;
|
|
149
|
+
const parts = line.trim().split(/\s+/);
|
|
150
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
151
|
+
if (!isNaN(pid) && pid > 0) pids.add(pid);
|
|
152
|
+
}
|
|
153
|
+
return [...pids];
|
|
154
|
+
} else {
|
|
155
|
+
// lsof -ti :<port> prints one PID per line
|
|
156
|
+
const out = execSync(`lsof -ti :${port}`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
157
|
+
return out
|
|
158
|
+
.split('\n')
|
|
159
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
160
|
+
.filter((n) => !isNaN(n) && n > 0);
|
|
161
|
+
}
|
|
162
|
+
} catch (_) {
|
|
163
|
+
// lsof/netstat not available, or returned non-zero (no matches)
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Poll until nothing is listening on host:port, or reject after timeout ms.
|
|
170
|
+
*/
|
|
171
|
+
function _waitForPortFree(host, port, timeout) {
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const deadline = Date.now() + timeout;
|
|
174
|
+
function check() {
|
|
175
|
+
if (Date.now() > deadline) {
|
|
176
|
+
reject(new Error(
|
|
177
|
+
`launchGateway: port ${port} was still occupied after ${timeout}ms`
|
|
178
|
+
));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const sock = new net.Socket();
|
|
182
|
+
sock.setTimeout(300);
|
|
183
|
+
sock.on('connect', () => { sock.destroy(); setTimeout(check, 200); });
|
|
184
|
+
sock.on('error', () => { sock.destroy(); resolve(); });
|
|
185
|
+
sock.on('timeout', () => { sock.destroy(); resolve(); });
|
|
186
|
+
sock.connect(port, host);
|
|
187
|
+
}
|
|
188
|
+
check();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
86
192
|
function _waitForReady(child, host, port, readyPattern, timeout) {
|
|
87
193
|
return new Promise((resolve, reject) => {
|
|
88
194
|
const deadline = setTimeout(() => {
|