quarchpy 2.2.17.dev2__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,15 +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
11
14
 
12
15
  import quarchpy_binaries
13
16
 
14
17
  from quarchpy.connection_specific.connection_QIS import QisInterface
15
18
  from quarchpy.connection_specific.jdk_jres.fix_permissions import main as fix_permissions, find_java_permissions
16
19
  from quarchpy.install_qps import find_qps
20
+ from quarchpy.user_interface import requestDialog
17
21
  from quarchpy.user_interface.user_interface import printText, logDebug
18
22
  import subprocess
19
23
  import logging
@@ -25,7 +29,7 @@ def isQisRunning():
25
29
  Checks if a local instance of QIS is running and responding
26
30
  Returns
27
31
  -------
28
- is_running : bool
32
+ is_running : bool\
29
33
  True if QIS is running and responding
30
34
  """
31
35
 
@@ -79,139 +83,76 @@ def isQisRunningAndResponding(timeout=2):
79
83
  return True
80
84
 
81
85
 
82
- 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']:
83
95
  """
84
- Executes QIS on the local system, using the version contained within quarchpy
85
-
86
- Parameters
87
- ----------
88
- terminal : bool, optional
89
- True if QIS terminal should be shown on startup
90
- headless : bool, optional
91
- True if app should be run in headless mode for non graphical environments
92
- args : list[str], optional
93
- List of additional parameters to be supplied to QIS on the command line
94
-
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).
95
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
96
128
  if not find_qps():
97
129
  logger.error("Unable to find or install QPS... Aborting...")
98
- return
99
-
100
- # java path
101
- java_path = quarchpy_binaries.get_jre_home()
102
- java_path = "\"" + java_path
103
-
104
- # change directory to /QPS/QIS
105
- qis_path = os.path.dirname(os.path.abspath(__file__))
106
- qis_path, junk = os.path.split(qis_path)
107
-
108
- # OS
109
- current_os = platform.system()
110
- current_arch = platform.machine()
111
- current_arch = current_arch.lower() # ensure comparing same case
112
-
113
- # Currently officially unsupported
114
- if (current_os in "Linux" and current_arch == "aarch64") or (current_os in "Darwin" and current_arch == "arm64"):
115
- logger.warning("The system [" + current_os + ", " + current_arch + "] is not officially supported.")
116
- logger.warning("Please contact Quarch support for running QuarchPy on this system.")
117
- return
118
-
119
- # ensure the jres folder has the required permissions
120
- permissions, message = find_java_permissions()
121
- if permissions is False:
122
- logger.warning(message)
123
- logger.warning("Not having correct permissions will prevent Quarch Java Programs to launch")
124
- logger.warning("Run \"python -m quarchpy.run permission_fix\" to fix this.")
125
- user_input = input("Would you like to fix permissions now? (Y/N)")
126
- if user_input.lower() == "y":
127
- fix_permissions()
128
- permissions, message = find_java_permissions()
129
- time.sleep(0.5)
130
- if permissions is False:
131
- logger.warning("Attempt to fix permissions was unsuccessful. Please fix these manually.")
132
- else:
133
- logger.warning("Attempt to fix permissions was successful. Now continuing.")
130
+ return None
134
131
 
135
- qis_path = os.path.join(qis_path, "connection_specific", "QPS", "qis", "qis.jar")
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
136
137
 
137
- # record current working directory
138
+ # 4. Launch QIS Process
138
139
  current_dir = os.getcwd()
139
- os.chdir(os.path.dirname(qis_path))
140
-
141
- # Building the command
142
-
143
- # prefer IPV4 to IPV6
144
- ipv4v6_vm_args = "-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv6Addresses=false"
145
-
146
- # Process command prefix. Needed for headless mode, to support OSs with no system tray.
147
- # Added the flag to suppress the Java restricted method warning
148
- cmd_prefix = ipv4v6_vm_args + " --enable-native-access=ALL-UNNAMED"
149
- if headless is True or (args is not None and "-headless" in args):
150
- cmd_prefix += " -Djava.awt.headless=true"
151
-
152
- # Ignore netty unsafe warning
153
- cmd_prefix += " -Dio.netty.noUnsafe=true"
154
-
155
- # Process command suffix (additional standard options for QIS).
156
- if terminal is True:
157
- cmd_suffix = " -terminal"
158
- else:
159
- cmd_suffix = ""
160
- if args is not None:
161
- for option in args:
162
- # Avoid doubling the terminal option
163
- if option == "-terminal" and terminal is True:
164
- continue
165
- # Headless option is processed seperately as a java command
166
- if option != "-headless":
167
- cmd_suffix = cmd_suffix + " " + option
168
-
169
-
170
- command = "java\" " + cmd_prefix + " -jar qis.jar" + cmd_suffix
171
-
172
- # different start for different OS
173
- if current_os == "Windows":
174
- command = java_path + "\\bin\\" + command
175
- elif current_os == "Linux" and current_arch == "x86_64":
176
- command = java_path + "/bin/" + command
177
- elif current_os == "Linux" and current_arch == "aarch64":
178
- command = java_path + "/bin/" + command
179
- elif current_os == "Darwin" and current_arch == "x86_64":
180
- command = java_path + "/bin/" + command
181
- elif current_os == "Darwin" and current_arch == "arm64":
182
- command = java_path + "/bin/" + command
183
- else: # default to windows
184
- command = java_path + "\\bin\\" + command
185
-
186
- # Use the command and check QIS has launched
187
- # If logging to a terminal window is on then os.system should be used to view logging.
188
- if "-logging=ON" in str(args):
189
- process = subprocess.Popen(command, shell=True)
190
- startTime = time.time() # Checks for Popen launch only
191
- while not isQisRunning():
192
- if time.time() - startTime > timeout:
193
- raise TimeoutError("QIS failed to launch within timelimit of " + str(timeout) + " sec.")
194
- pass
195
- else:
196
- if sys.version_info[0] < 3:
197
- process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
198
- else:
199
- process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True)
200
-
201
- startTime = time.time() #Checks for Popen launch only
202
- while not isQisRunning():
203
- _get_std_msg_and_err_from_QIS_process(process)
204
- if time.time() - startTime > timeout:
205
- raise TimeoutError("QIS failed to launch within timelimit of " + str(timeout) + " sec.")
206
- pass
140
+ try:
141
+ os.chdir(qis_dir)
142
+ process = _launch_qis_process(command, launch_args)
143
+ finally:
144
+ os.chdir(current_dir)
207
145
 
208
- if isQisRunningAndResponding(timeout=timeout):
209
- logDebug("QIS running and responding")
210
- else:
211
- 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
212
149
 
213
- # change directory back to start directory
214
- 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
215
156
 
216
157
 
217
158
  def reader(stream, q, source, lock, stop_flag):
@@ -369,3 +310,170 @@ def GetQisModuleSelection(QisConnection):
369
310
  myDeviceID = None
370
311
 
371
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