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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js4j",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A Node.js implementation of py4j — bridge between JavaScript and Java via the Py4J gateway protocol",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
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(() => {