js4j 0.1.1 → 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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js4j",
3
- "version": "0.1.1",
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(() => {