quarchpy 2.2.17.dev1__py3-none-any.whl → 2.2.17.dev3__py3-none-any.whl

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.
quarchpy/qis/qisFuncs.py CHANGED
@@ -5,12 +5,19 @@ Contains general functions for starting and stopping QIS processes
5
5
  """
6
6
 
7
7
  import os, sys
8
+ import socket
8
9
  import time, platform
10
+ from subprocess import CompletedProcess, Popen
9
11
  from threading import Thread, Lock, Event, active_count
10
12
  from queue import Queue, Empty
13
+ from typing import Optional, List, Any, Tuple, Union
14
+
15
+ import quarchpy_binaries
16
+
11
17
  from quarchpy.connection_specific.connection_QIS import QisInterface
12
18
  from quarchpy.connection_specific.jdk_jres.fix_permissions import main as fix_permissions, find_java_permissions
13
19
  from quarchpy.install_qps import find_qps
20
+ from quarchpy.user_interface import requestDialog
14
21
  from quarchpy.user_interface.user_interface import printText, logDebug
15
22
  import subprocess
16
23
  import logging
@@ -22,7 +29,7 @@ def isQisRunning():
22
29
  Checks if a local instance of QIS is running and responding
23
30
  Returns
24
31
  -------
25
- is_running : bool
32
+ is_running : bool\
26
33
  True if QIS is running and responding
27
34
  """
28
35
 
@@ -76,141 +83,76 @@ def isQisRunningAndResponding(timeout=2):
76
83
  return True
77
84
 
78
85
 
79
- def startLocalQis(terminal=False, headless=False, args=None, timeout=20):
86
+ def startLocalQis(
87
+ terminal: bool = False,
88
+ headless: bool = False,
89
+ args: List[str] = [],
90
+ timeout: int = 20,
91
+ host: str = '127.0.0.1',
92
+ port: int = 9722,
93
+ rest_port: int = 9780
94
+ ) -> Optional['QisInterface']:
80
95
  """
81
- Executes QIS on the local system, using the version contained within quarchpy
82
-
83
- Parameters
84
- ----------
85
- terminal : bool, optional
86
- True if QIS terminal should be shown on startup
87
- headless : bool, optional
88
- True if app should be run in headless mode for non graphical environments
89
- args : list[str], optional
90
- List of additional parameters to be supplied to QIS on the command line
91
-
96
+ Executes QIS on the local system and returns a connected interface.
97
+
98
+ Args:
99
+ terminal: True if QIS terminal should be shown on startup.
100
+ headless: True if app should be run in headless mode.
101
+ args: List of additional parameters.
102
+ timeout: Time in seconds to wait for launch.
103
+ host: Host address (default localhost).
104
+ port: The Telnet port to use (Default: 9722).
105
+ rest_port: The REST port to use (Default: 9780).
92
106
  """
107
+ # 0. Prepare Arguments
108
+ # We copy the list to avoid modifying the input in place
109
+ launch_args = args.copy()
110
+
111
+ # Sync 'port' arg to CLI flags if non-default
112
+ if port != 9722:
113
+ # Only add if not already manually present in args
114
+ if not any("-port=" in arg for arg in launch_args):
115
+ launch_args.append(f"-port={port}")
116
+
117
+ # Sync 'restport' arg to CLI flags if non-default
118
+ if rest_port != 9780:
119
+ if not any("-restport=" in arg for arg in launch_args):
120
+ launch_args.append(f"-restport={rest_port}")
121
+
122
+ # 1. Check if already running (Use the explicit 'port' integer)
123
+ if _check_port_open(host, port):
124
+ logger.debug(f"QIS instance on port {port} is already running. Connecting...")
125
+ return QisInterface(host=host, port=port)
126
+
127
+ # 2. Check for installation
93
128
  if not find_qps():
94
129
  logger.error("Unable to find or install QPS... Aborting...")
95
- return
130
+ return None
96
131
 
97
- # java path
98
- java_path = os.path.dirname(os.path.abspath(__file__))
99
- java_path, junk = os.path.split(java_path)
100
- java_path = os.path.join(java_path, "connection_specific", "jdk_jres")
101
- java_path = "\"" + java_path
132
+ # 3. Prepare Command and Environment
133
+ # Note: We pass 'launch_args' which now includes our port flags
134
+ command, qis_dir = _prepare_qis_launch_env(terminal, headless, launch_args)
135
+ if not command:
136
+ return None
102
137
 
103
- # change directory to /QPS/QIS
104
- qis_path = os.path.dirname(os.path.abspath(__file__))
105
- qis_path, junk = os.path.split(qis_path)
106
-
107
- # OS
108
- current_os = platform.system()
109
- current_arch = platform.machine()
110
- current_arch = current_arch.lower() # ensure comparing same case
111
-
112
- # Currently officially unsupported
113
- if (current_os in "Linux" and current_arch == "aarch64") or (current_os in "Darwin" and current_arch == "arm64"):
114
- logger.warning("The system [" + current_os + ", " + current_arch + "] is not officially supported.")
115
- logger.warning("Please contact Quarch support for running QuarchPy on this system.")
116
- return
117
-
118
- # ensure the jres folder has the required permissions
119
- permissions, message = find_java_permissions()
120
- if permissions is False:
121
- logger.warning(message)
122
- logger.warning("Not having correct permissions will prevent Quarch Java Programs to launch")
123
- logger.warning("Run \"python -m quarchpy.run permission_fix\" to fix this.")
124
- user_input = input("Would you like to fix permissions now? (Y/N)")
125
- if user_input.lower() == "y":
126
- fix_permissions()
127
- permissions, message = find_java_permissions()
128
- time.sleep(0.5)
129
- if permissions is False:
130
- logger.warning("Attempt to fix permissions was unsuccessful. Please fix these manually.")
131
- else:
132
- logger.warning("Attempt to fix permissions was successful. Now continuing.")
133
-
134
- qis_path = os.path.join(qis_path, "connection_specific", "QPS", "qis", "qis.jar")
135
-
136
- # record current working directory
138
+ # 4. Launch QIS Process
137
139
  current_dir = os.getcwd()
138
- os.chdir(os.path.dirname(qis_path))
139
-
140
- # Building the command
141
-
142
- # prefer IPV4 to IPV6
143
- ipv4v6_vm_args = "-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv6Addresses=false"
144
-
145
- # Process command prefix. Needed for headless mode, to support OSs with no system tray.
146
- # Added the flag to suppress the Java restricted method warning
147
- cmd_prefix = ipv4v6_vm_args + " --enable-native-access=ALL-UNNAMED"
148
- if headless is True or (args is not None and "-headless" in args):
149
- cmd_prefix += " -Djava.awt.headless=true"
150
-
151
- # Ignore netty unsafe warning
152
- cmd_prefix += " -Dio.netty.noUnsafe=true"
153
-
154
- # Process command suffix (additional standard options for QIS).
155
- if terminal is True:
156
- cmd_suffix = " -terminal"
157
- else:
158
- cmd_suffix = ""
159
- if args is not None:
160
- for option in args:
161
- # Avoid doubling the terminal option
162
- if option == "-terminal" and terminal is True:
163
- continue
164
- # Headless option is processed seperately as a java command
165
- if option != "-headless":
166
- cmd_suffix = cmd_suffix + " " + option
167
-
168
-
169
- command = "java\" " + cmd_prefix + " -jar qis.jar" + cmd_suffix
170
-
171
- # different start for different OS
172
- if current_os == "Windows":
173
- command = java_path + "\\win_amd64_jdk_jre\\bin\\" + command
174
- elif current_os == "Linux" and current_arch == "x86_64":
175
- command = java_path + "/lin_amd64_jdk_jre/bin/" + command
176
- elif current_os == "Linux" and current_arch == "aarch64":
177
- command = java_path + "/lin_arm64_jdk_jre/bin/" + command
178
- elif current_os == "Darwin" and current_arch == "x86_64":
179
- command = java_path + "/mac_amd64_jdk_jre/bin/" + command
180
- elif current_os == "Darwin" and current_arch == "arm64":
181
- command = java_path + "/mac_arm64_jdk_jre/bin/" + command
182
- else: # default to windows
183
- command = java_path + "\\win_amd64_jdk_jre\\bin\\" + command
184
-
185
- # Use the command and check QIS has launched
186
- # If logging to a terminal window is on then os.system should be used to view logging.
187
- if "-logging=ON" in str(args):
188
- process = subprocess.Popen(command, shell=True)
189
- startTime = time.time() # Checks for Popen launch only
190
- while not isQisRunning():
191
- if time.time() - startTime > timeout:
192
- raise TimeoutError("QIS failed to launch within timelimit of " + str(timeout) + " sec.")
193
- pass
194
- else:
195
- if sys.version_info[0] < 3:
196
- process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
197
- else:
198
- process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True)
199
-
200
- startTime = time.time() #Checks for Popen launch only
201
- while not isQisRunning():
202
- _get_std_msg_and_err_from_QIS_process(process)
203
- if time.time() - startTime > timeout:
204
- raise TimeoutError("QIS failed to launch within timelimit of " + str(timeout) + " sec.")
205
- pass
140
+ try:
141
+ os.chdir(qis_dir)
142
+ process = _launch_qis_process(command, launch_args)
143
+ finally:
144
+ os.chdir(current_dir)
206
145
 
207
- if isQisRunningAndResponding(timeout=timeout):
208
- logDebug("QIS running and responding")
209
- else:
210
- logDebug("QIS running but not responding")
146
+ # 5. Wait for QIS to be ready
147
+ if not _wait_for_qis_service(host, port, timeout, process, launch_args):
148
+ return None
211
149
 
212
- # change directory back to start directory
213
- os.chdir(current_dir)
150
+ # 6. Return Connected Interface
151
+ try:
152
+ return QisInterface(host=host, port=port)
153
+ except Exception as e:
154
+ logger.error(f"QIS started, but failed to create interface object: {e}")
155
+ return None
214
156
 
215
157
 
216
158
  def reader(stream, q, source, lock, stop_flag):
@@ -368,3 +310,170 @@ def GetQisModuleSelection(QisConnection):
368
310
  myDeviceID = None
369
311
 
370
312
  return myDeviceID
313
+
314
+
315
+ # ==========================================
316
+ # HELPER FUNCTIONS
317
+ # ==========================================
318
+
319
+ def _parse_qis_port(args: List[str]) -> int:
320
+ """Extracts the QIS port from arguments, defaulting to 9722."""
321
+ port = 9722
322
+ if args:
323
+ for arg in args:
324
+ if "-port=" in arg.lower():
325
+ try:
326
+ port = int(arg.split('=')[1])
327
+ except (IndexError, ValueError):
328
+ pass
329
+ return port
330
+
331
+
332
+ def _prepare_qis_launch_env(terminal: bool, headless: bool, args: List[str]) -> Tuple[Optional[str], Optional[str]]:
333
+ """Resolves paths, JVM flags, and builds the QIS command string."""
334
+
335
+ # 1. OS Checks
336
+ current_os = platform.system()
337
+ current_arch = platform.machine().lower()
338
+
339
+ # 2. Permissions
340
+ _handle_java_permissions()
341
+
342
+ # 3. Path Resolution
343
+ if 'quarchpy_binaries' not in globals() and 'quarchpy_binaries' not in locals():
344
+ logger.error("quarchpy_binaries module not found.")
345
+ return None, None
346
+
347
+ try:
348
+ java_home = quarchpy_binaries.get_jre_home()
349
+ except Exception as e:
350
+ logger.error(f"Failed to get JRE home: {e}")
351
+ return None, None
352
+
353
+ # Locate QIS Jar
354
+ # Assuming standard structure relative to this file
355
+ base_path = os.path.dirname(os.path.abspath(__file__))
356
+ # Go up one level from 'connection_specific' typically
357
+ root_path = os.path.dirname(base_path)
358
+
359
+ # Construct path: .../connection_specific/QPS/qis/qis.jar
360
+ qis_jar_path = os.path.join(root_path, "connection_specific", "QPS", "qis", "qis.jar")
361
+ qis_dir = os.path.dirname(qis_jar_path)
362
+
363
+ # 4. Java Binary Selection
364
+ java_bin_rel = "bin/java"
365
+ if current_os == "Windows":
366
+ java_bin_rel = r"bin\java"
367
+
368
+ java_exe = os.path.join(java_home, java_bin_rel)
369
+ java_exe_quoted = f'"{java_exe}"'
370
+
371
+ # 5. Build JVM Arguments
372
+ # Prefer IPv4
373
+ cmd_prefix = "-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv6Addresses=false"
374
+ # Enable native access (required for newer Java versions)
375
+ cmd_prefix += " --enable-native-access=ALL-UNNAMED"
376
+ # Netty config
377
+ cmd_prefix += " -Dio.netty.noUnsafe=true"
378
+
379
+ # Headless logic
380
+ is_headless = headless
381
+ if args and "-headless" in args:
382
+ is_headless = True
383
+
384
+ if is_headless:
385
+ cmd_prefix += " -Djava.awt.headless=true"
386
+
387
+ # 6. Build Application Arguments
388
+ cmd_suffix = ""
389
+ if terminal:
390
+ cmd_suffix += " -terminal"
391
+
392
+ if args:
393
+ for option in args:
394
+ # Prevent double flags
395
+ if option == "-terminal" and terminal:
396
+ continue
397
+ if option != "-headless":
398
+ cmd_suffix += f" {option}"
399
+
400
+ # 7. Final Command
401
+ command = f'{java_exe_quoted} {cmd_prefix} -jar qis.jar{cmd_suffix}'
402
+
403
+ return command, qis_dir
404
+
405
+
406
+ def _launch_qis_process(command: str, args: List[str]) -> Union[Popen, CompletedProcess]:
407
+ """Launches QIS, checking for logging flags."""
408
+ args_str = " ".join(args) if args else ""
409
+
410
+ if "-logconsole=ON" in args_str:
411
+ if platform.system() == "Windows":
412
+ return subprocess.Popen(command, shell=True)
413
+ else:
414
+ return subprocess.run(command + "; exec bash", shell=True)
415
+ else:
416
+ text_mode = True if sys.version_info >= (3, 7) else False
417
+ return subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=text_mode, shell=True)
418
+
419
+
420
+ def _wait_for_qis_service(host: str, port: int, timeout: int, process: Optional[subprocess.Popen], args: List[str]) -> bool:
421
+ """Polls the specific QIS port until it opens."""
422
+ start_time = time.time()
423
+ args_str = " ".join(args) if args else ""
424
+ logging_on = "-logconsole=ON" in args_str
425
+
426
+ while True:
427
+ # 1. Check TCP Connectivity
428
+ if _check_port_open(host, port):
429
+ # QIS accepts the connection
430
+ logger.debug(f"QIS detected on port {port} after {time.time() - start_time:.2f}s")
431
+
432
+ # Optional: Extra check to ensure it's actually responding to commands
433
+ # (Replaces old 'isQisRunningAndResponding' logic efficiently)
434
+ # You could do a quick handshake here if strict validation is required,
435
+ # but usually TCP open is sufficient for startup success.
436
+ return True
437
+
438
+ # 2. Monitor Process Health
439
+ if not logging_on and process:
440
+ # Drain pipes and check if process died
441
+ if process.poll() is not None:
442
+ logger.error("QIS process terminated unexpectedly.")
443
+ return False
444
+ try:
445
+ _get_std_msg_and_err_from_QIS_process(process)
446
+ except NameError:
447
+ pass # Function might not be available in this scope
448
+
449
+ # 3. Timeout Check
450
+ if time.time() - start_time > timeout:
451
+ logger.error(f"QIS failed to launch on port {port} within {timeout}s.")
452
+ return False
453
+
454
+ time.sleep(0.2)
455
+
456
+ def _handle_java_permissions() -> None:
457
+ """Checks and attempts to fix Java execution permissions."""
458
+ permissions, message = find_java_permissions()
459
+ if not permissions:
460
+ logger.warning(message)
461
+ printText("Not having correct permissions will prevent Quarch Java Programs from launching.")
462
+ printText("Would you like quarchpy to attempt to fix the permissions now? (Y/N)")
463
+ try:
464
+ user_input = requestDialog(">>> ")
465
+ except EOFError:
466
+ user_input = "N"
467
+ if user_input.strip().lower() in ['y', 'yes']:
468
+ fix_permissions()
469
+
470
+ def _check_port_open(host: str, port: int) -> bool:
471
+ """Simple TCP connect check."""
472
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
473
+ s.settimeout(1)
474
+ try:
475
+ s.connect((host, int(port)))
476
+ s.close()
477
+ return True
478
+ except (socket.timeout, ConnectionRefusedError, OSError):
479
+ return False