10
10
import java .util .List ;
11
11
import java .util .Map ;
12
12
13
- import org .bytedeco .javacpp .Loader ;
14
13
import org .myrobotlab .codec .CodecUtils ;
15
14
import org .myrobotlab .framework .Message ;
16
15
import org .myrobotlab .framework .Platform ;
@@ -156,10 +155,6 @@ public void run() {
156
155
*/
157
156
protected Py4jClient pythonProcess = null ;
158
157
159
- /**
160
- * The base command to launch the Python interpreter without any arguments.
161
- */
162
- protected transient String pythonCommand = "python" ;
163
158
164
159
public Py4j (String n , String id ) {
165
160
super (n , id );
@@ -172,9 +167,9 @@ public Py4j(String n, String id) {
172
167
* data/Py4j/{serviceName}
173
168
*
174
169
* @param scriptName
175
- * - name of the script
170
+ * - name of the script
176
171
* @param code
177
- * - code block
172
+ * - code block
178
173
*/
179
174
public void addScript (String scriptName , String code ) {
180
175
Py4jConfig c = (Py4jConfig ) config ;
@@ -208,7 +203,7 @@ public boolean autostartPython(boolean b) {
208
203
* removes script from memory of openScripts
209
204
*
210
205
* @param scriptName
211
- * The name of the script to close.
206
+ * The name of the script to close.
212
207
*/
213
208
public void closeScript (String scriptName ) {
214
209
openedScripts .remove (scriptName );
@@ -244,7 +239,7 @@ public void connectionStopped(Py4JServerConnection gatewayConnection) {
244
239
* One of 3 methods supported on the MessageHandler() callbacks
245
240
*
246
241
* @param code
247
- * The Python code to execute in the interpreter.
242
+ * The Python code to execute in the interpreter.
248
243
*/
249
244
@ Override
250
245
public boolean exec (String code ) {
@@ -263,7 +258,8 @@ public boolean exec(String code) {
263
258
}
264
259
265
260
private String getClientKey (Py4JServerConnection gatewayConnection ) {
266
- return String .format ("%s:%d" , gatewayConnection .getSocket ().getInetAddress (), gatewayConnection .getSocket ().getPort ());
261
+ return String .format ("%s:%d" , gatewayConnection .getSocket ().getInetAddress (),
262
+ gatewayConnection .getSocket ().getPort ());
267
263
}
268
264
269
265
/**
@@ -291,7 +287,7 @@ public List<String> getScriptList() throws IOException {
291
287
* immediately publishes the output on {@link #publishStdOut(String)}.
292
288
*
293
289
* @param msg
294
- * The output from a py4j related subprocess.
290
+ * The output from a py4j related subprocess.
295
291
*/
296
292
public void handleStdOut (String msg ) {
297
293
if (!"\n " .equals (msg )) {
@@ -322,7 +318,7 @@ public String onPythonMessage(Message msg) {
322
318
* Opens an example "service" script maintained in myrobotlab
323
319
*
324
320
* @param serviceType
325
- * the type of service
321
+ * the type of service
326
322
* @throws IOException
327
323
*/
328
324
public void openExampleScript (String serviceType ) throws IOException {
@@ -342,8 +338,8 @@ public void openExampleScript(String serviceType) throws IOException {
342
338
* data/Py4j/{serviceName} directory.
343
339
*
344
340
* @param scriptName
345
- * - name of the script file relatie to scriptRootDir
346
- * data/Py4j/{serviceName}/
341
+ * - name of the script file relatie to scriptRootDir
342
+ * data/Py4j/{serviceName}/
347
343
* @throws IOException
348
344
*/
349
345
public void openScript (String scriptName ) throws IOException {
@@ -472,81 +468,101 @@ public void start() {
472
468
* MessageHandler and setup for runtime references to work
473
469
*/
474
470
public void startPythonProcess () {
475
- try {
476
-
477
- // Specify the Python script path and arguments
478
- String pythonScript = new File (getResourceDir () + fs + "Py4j.py" ).getAbsolutePath ();
479
-
480
- // Script requires full name as first command line argument
481
- String [] pythonArgs = { getFullName () };
482
-
483
- // Build the command to start the Python process
484
- ProcessBuilder processBuilder ;
485
- if (((Py4jConfig ) config ).useBundledPython ) {
486
- String venv = getDataDir () + fs + "venv" ;
487
- pythonCommand = (Platform .getLocalInstance ().isWindows ()) ? venv + fs + "Scripts" + fs + "python.exe" : venv + fs + "bin" + fs + "python" ;
488
- if (!FileIO .checkDir (venv )) {
489
- // We don't have an initialized virtual environment, so lets make one
490
- // and install our required packages
491
- String python = Loader .load (org .bytedeco .cpython .python .class );
492
- String venvLib = new File (python ).getParent () + fs + "lib" + fs + "venv" + fs + "scripts" + fs + "nt" ;
493
- if (Platform .getLocalInstance ().isWindows ()) {
494
- // Super hacky workaround, venv works differently on Windows and
495
- // requires these two
496
- // files, but they are not distributed in bare-bones Python or in
497
- // any pip packages.
498
- // So we copy them where it expects, and it seems to work now
499
- FileIO .copy (getResourceDir () + fs + "python.exe" , venvLib + fs + "python.exe" );
500
- FileIO .copy (getResourceDir () + fs + "pythonw.exe" , venvLib + fs + "pythonw.exe" );
471
+ try {
472
+ // Determine Python executable
473
+ String pythonCommand = findPythonExecutable ();
474
+ if (pythonCommand == null ) {
475
+ error ("Python is not installed or not found in system PATH." );
476
+ return ;
501
477
}
502
- ProcessBuilder installProcess = new ProcessBuilder (python , "-m" , "venv" , venv );
503
- int ret = installProcess .inheritIO ().start ().waitFor ();
504
- if (ret != 0 ) {
505
- error ("Could not create virtual environment, subprocess returned {}. If on Windows, make sure there is a python.exe file in {}" , ret , venvLib );
506
- return ;
478
+
479
+ // Define paths
480
+ String venv = getDataDir () + fs + "venv" ;
481
+ String pythonScript = new File (getResourceDir () + fs + "Py4j.py" ).getAbsolutePath ();
482
+ String [] pythonArgs = { getFullName () };
483
+
484
+ // Check if virtual environment exists, if not, create and install dependencies
485
+ File venvDir = new File (venv );
486
+ String venvPython = Platform .getLocalInstance ().isWindows ()
487
+ ? venv + fs + "Scripts" + fs + "python.exe"
488
+ : venv + fs + "bin" + fs + "python" ;
489
+
490
+ if (!venvDir .exists ()) {
491
+ ProcessBuilder venvProcess = new ProcessBuilder (pythonCommand , "-m" , "venv" , venv );
492
+ int ret = venvProcess .inheritIO ().start ().waitFor ();
493
+ if (ret != 0 ) {
494
+ error ("Failed to create virtual environment." );
495
+ return ;
496
+ }
497
+
498
+ // Install py4j inside venv
499
+ ProcessBuilder pipProcess = new ProcessBuilder (venvPython , "-m" , "pip" , "install" , "py4j" );
500
+ ret = pipProcess .inheritIO ().start ().waitFor ();
501
+ if (ret != 0 ) {
502
+ error ("Failed to install py4j in the virtual environment." );
503
+ return ;
504
+ }
507
505
}
508
506
509
- installProcess = new ProcessBuilder (pythonCommand , "-m" , "pip" , "install" , "py4j" );
510
- ret = installProcess .inheritIO ().start ().waitFor ();
511
- if (ret != 0 ) {
512
- error ("Could not install package, subprocess returned " + ret );
513
- return ;
507
+ // Validate py4j is installed
508
+ ProcessBuilder checkPy4j = new ProcessBuilder (venvPython , "-c" , "import py4j" );
509
+ int checkRet = checkPy4j .inheritIO ().start ().waitFor ();
510
+ if (checkRet != 0 ) {
511
+ error ("py4j is not installed properly in the virtual environment." );
512
+ return ;
514
513
}
515
514
516
- }
515
+ // Ensure the virtual environment's Python is used
516
+ ProcessBuilder processBuilder = new ProcessBuilder (venvPython , pythonScript );
517
+ processBuilder .redirectErrorStream (true );
518
+ processBuilder .command ().addAll (List .of (pythonArgs ));
517
519
518
- // Virtual environment should exist, so lets use that python
519
- } else {
520
- // Just use the system python
521
- pythonCommand = "python" ;
520
+ pythonProcess = new Py4jClient ( this , processBuilder . start ());
521
+
522
+ } catch ( Exception e ) {
523
+ error ( e ) ;
522
524
}
523
- processBuilder = new ProcessBuilder (pythonCommand , pythonScript );
524
- processBuilder .redirectErrorStream (true );
525
- processBuilder .command ().addAll (List .of (pythonArgs ));
525
+ }
526
526
527
- // Start the Python process
528
- pythonProcess = new Py4jClient (this , processBuilder .start ());
527
+ /**
528
+ * Finds the Python executable and returns its path.
529
+ * Checks both "python3" and "python" commands.
530
+ */
531
+ private String findPythonExecutable () {
532
+ try {
533
+ ProcessBuilder processBuilder = new ProcessBuilder ("python3" , "--version" );
534
+ Process process = processBuilder .start ();
535
+ int exitCode = process .waitFor ();
536
+ if (exitCode == 0 ) return "python3" ;
537
+ } catch (Exception ignored ) {}
529
538
530
- } catch (Exception e ) {
531
- error (e );
532
- }
539
+ try {
540
+ ProcessBuilder processBuilder = new ProcessBuilder ("python" , "--version" );
541
+ Process process = processBuilder .start ();
542
+ int exitCode = process .waitFor ();
543
+ if (exitCode == 0 ) return "python" ;
544
+ } catch (Exception ignored ) {}
545
+
546
+ return null ; // Python not found
533
547
}
534
548
549
+
550
+
535
551
/**
536
552
* Install a list of packages into the environment Py4j is running in. Py4j
537
553
* does not need to be running/connected to call this method as it spawns a
538
554
* new subprocess to invoke Pip. Output from pip is echoed via
539
555
* {@link #handleStdOut(String)}.
540
556
*
541
557
* @param packages
542
- * The list of packages to install. Must be findable by Pip
558
+ * The list of packages to install. Must be findable by Pip
543
559
* @throws IOException
544
- * If an I/O error occurs running Pip.
560
+ * If an I/O error occurs running Pip.
545
561
*/
546
562
public void installPipPackages (List <String > packages ) throws IOException {
547
563
List <String > commandArgs = new ArrayList <>(List .of ("-m" , "pip" , "install" ));
548
564
commandArgs .addAll (packages );
549
- ProcessBuilder pipProcess = new ProcessBuilder (pythonCommand );
565
+ ProcessBuilder pipProcess = new ProcessBuilder (findPythonExecutable () );
550
566
pipProcess .command ().addAll (commandArgs );
551
567
Process proc = pipProcess .redirectErrorStream (true ).start ();
552
568
new Thread (() -> {
0 commit comments