js4j 0.1.0 → 0.1.2
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 +109 -0
- package/index.d.ts +62 -0
- package/index.js +4 -0
- package/package.json +1 -1
- package/src/launcher.js +107 -1
package/README.md
CHANGED
|
@@ -33,7 +33,11 @@ A Node.js implementation of [py4j](https://www.py4j.org/) — a bridge between J
|
|
|
33
33
|
- [createJavaProxy](#createjavaproxy)
|
|
34
34
|
- [CallbackServer](#callbackserver)
|
|
35
35
|
- [ProxyPool](#proxypool)
|
|
36
|
+
- [launchGateway](#launchgateway)
|
|
36
37
|
- [Errors](#errors)
|
|
38
|
+
- [Known Limitations](#known-limitations)
|
|
39
|
+
- [Varargs methods](#varargs-methods)
|
|
40
|
+
- [Fields vs methods](#fields-vs-methods)
|
|
37
41
|
- [TypeScript](#typescript)
|
|
38
42
|
- [Credits](#credits)
|
|
39
43
|
|
|
@@ -642,6 +646,70 @@ try {
|
|
|
642
646
|
|
|
643
647
|
---
|
|
644
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
|
+
|
|
645
713
|
## TypeScript
|
|
646
714
|
|
|
647
715
|
Full type definitions are included. No `@types/` package is needed.
|
|
@@ -668,6 +736,47 @@ const items: any[] = await list.toArray();
|
|
|
668
736
|
|
|
669
737
|
---
|
|
670
738
|
|
|
739
|
+
### launchGateway
|
|
740
|
+
|
|
741
|
+
Spawn a Java process that runs a `GatewayServer` and connect a `JavaGateway` to it automatically. Useful when Node.js is responsible for starting the JVM rather than the other way around.
|
|
742
|
+
|
|
743
|
+
```js
|
|
744
|
+
const { launchGateway } = require('js4j');
|
|
745
|
+
|
|
746
|
+
const { gateway, kill } = await launchGateway({
|
|
747
|
+
classpath: '/usr/share/py4j/py4j.jar:java/build',
|
|
748
|
+
mainClass: 'com.example.MyApp',
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
const result = await gateway.entry_point.doSomething();
|
|
752
|
+
await kill();
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
#### Options
|
|
756
|
+
|
|
757
|
+
| Option | Type | Default | Description |
|
|
758
|
+
|---|---|---|---|
|
|
759
|
+
| `classpath` | `string` | *required* | Java classpath string (e.g. `'py4j.jar:.'`). |
|
|
760
|
+
| `mainClass` | `string` | *required* | Fully-qualified main class to launch. |
|
|
761
|
+
| `host` | `string` | `'127.0.0.1'` | Host to connect to after the process starts. |
|
|
762
|
+
| `port` | `number` | `25333` | Port to connect to. |
|
|
763
|
+
| `jvmArgs` | `string[]` | `[]` | Extra JVM flags (e.g. `['-Xmx512m', '-ea']`). |
|
|
764
|
+
| `args` | `string[]` | `[]` | Arguments passed to the main class. |
|
|
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. |
|
|
766
|
+
| `timeout` | `number` | `30000` | Maximum milliseconds to wait for the server to become ready. |
|
|
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. |
|
|
769
|
+
|
|
770
|
+
#### Return value
|
|
771
|
+
|
|
772
|
+
| Property | Type | Description |
|
|
773
|
+
|---|---|---|
|
|
774
|
+
| `gateway` | `JavaGateway` | A connected `JavaGateway` instance. |
|
|
775
|
+
| `process` | `ChildProcess` | The spawned Java child process. |
|
|
776
|
+
| `kill` | `() => Promise<void>` | Shuts down the gateway and kills the Java process. |
|
|
777
|
+
|
|
778
|
+
---
|
|
779
|
+
|
|
671
780
|
## Running Tests
|
|
672
781
|
|
|
673
782
|
```bash
|
package/index.d.ts
CHANGED
|
@@ -372,6 +372,68 @@ export declare class CallbackServer {
|
|
|
372
372
|
readonly proxyPool: ProxyPool;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// Launcher
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
import { ChildProcess } from 'child_process';
|
|
380
|
+
|
|
381
|
+
export interface LaunchGatewayOptions {
|
|
382
|
+
/** Java classpath (e.g. '/path/to/py4j.jar:.') */
|
|
383
|
+
classpath: string;
|
|
384
|
+
/** Fully-qualified main class to launch (e.g. 'com.example.MyApp') */
|
|
385
|
+
mainClass: string;
|
|
386
|
+
/** Gateway host. Default: '127.0.0.1' */
|
|
387
|
+
host?: string;
|
|
388
|
+
/** Gateway port. Default: 25333 */
|
|
389
|
+
port?: number;
|
|
390
|
+
/** Extra JVM flags (e.g. ['-Xmx512m']). Default: [] */
|
|
391
|
+
jvmArgs?: string[];
|
|
392
|
+
/** Extra arguments passed to the main class. Default: [] */
|
|
393
|
+
args?: string[];
|
|
394
|
+
/**
|
|
395
|
+
* Pattern to match in process stdout that signals the server is ready.
|
|
396
|
+
* Set to null to skip stdout checking and rely only on port polling.
|
|
397
|
+
* Default: /GATEWAY_STARTED/
|
|
398
|
+
*/
|
|
399
|
+
readyPattern?: RegExp | string | null;
|
|
400
|
+
/** Maximum milliseconds to wait for the server to become ready. Default: 30000 */
|
|
401
|
+
timeout?: number;
|
|
402
|
+
/** Extra options forwarded to GatewayParameters. */
|
|
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;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export interface LaunchGatewayResult {
|
|
413
|
+
/** The spawned Java child process. */
|
|
414
|
+
process: ChildProcess;
|
|
415
|
+
/** A connected JavaGateway instance. */
|
|
416
|
+
gateway: JavaGateway;
|
|
417
|
+
/**
|
|
418
|
+
* Shut down the gateway and kill the Java process.
|
|
419
|
+
* Equivalent to calling `gateway.shutdown()` then `process.kill()`.
|
|
420
|
+
*/
|
|
421
|
+
kill: () => Promise<void>;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Spawn a Java GatewayServer process and connect a JavaGateway to it.
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* const { gateway, kill } = await launchGateway({
|
|
429
|
+
* classpath: '/usr/share/py4j/py4j.jar:java/build',
|
|
430
|
+
* mainClass: 'com.example.MyApp',
|
|
431
|
+
* });
|
|
432
|
+
* const result = await gateway.entry_point.doSomething();
|
|
433
|
+
* await kill();
|
|
434
|
+
*/
|
|
435
|
+
export declare function launchGateway(options: LaunchGatewayOptions): Promise<LaunchGatewayResult>;
|
|
436
|
+
|
|
375
437
|
// ---------------------------------------------------------------------------
|
|
376
438
|
// Factory functions
|
|
377
439
|
// ---------------------------------------------------------------------------
|
package/index.js
CHANGED
|
@@ -40,6 +40,7 @@ const {
|
|
|
40
40
|
Js4JAuthenticationError,
|
|
41
41
|
} = require('./src/exceptions');
|
|
42
42
|
const protocol = require('./src/protocol');
|
|
43
|
+
const { launchGateway } = require('./src/launcher');
|
|
43
44
|
|
|
44
45
|
module.exports = {
|
|
45
46
|
// Main gateway classes
|
|
@@ -73,6 +74,9 @@ module.exports = {
|
|
|
73
74
|
Js4JNetworkError,
|
|
74
75
|
Js4JAuthenticationError,
|
|
75
76
|
|
|
77
|
+
// Launcher
|
|
78
|
+
launchGateway,
|
|
79
|
+
|
|
76
80
|
// Low-level protocol (for advanced use)
|
|
77
81
|
protocol,
|
|
78
82
|
};
|
package/package.json
CHANGED
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(() => {
|