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/__init__.py +57 -49
- quarchpy/_version.py +1 -1
- quarchpy/connection_specific/connection_QIS.py +3 -8
- quarchpy/connection_specific/connection_QPS.py +116 -8
- quarchpy/connection_specific/connection_Telnet.py +19 -5
- quarchpy/connection_specific/jdk_jres/fix_permissions.py +0 -3
- quarchpy/connection_specific/usb_libs/libusb1.py +21 -0
- quarchpy/debug/SystemTest.py +65 -7
- quarchpy/debug/module_debug.py +11 -3
- quarchpy/device/device.py +93 -45
- quarchpy/device/scanDevices.py +55 -24
- quarchpy/qis/qisFuncs.py +236 -127
- quarchpy/qps/qpsFuncs.py +267 -171
- quarchpy/run.py +1 -1
- {quarchpy-2.2.17.dev1.dist-info → quarchpy-2.2.17.dev3.dist-info}/METADATA +5 -1
- {quarchpy-2.2.17.dev1.dist-info → quarchpy-2.2.17.dev3.dist-info}/RECORD +19 -18
- {quarchpy-2.2.17.dev1.dist-info → quarchpy-2.2.17.dev3.dist-info}/WHEEL +1 -1
- quarchpy-2.2.17.dev3.dist-info/licenses/LICENSE.txt +21 -0
- {quarchpy-2.2.17.dev1.dist-info → quarchpy-2.2.17.dev3.dist-info}/top_level.txt +0 -0
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(
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
#
|
|
213
|
-
|
|
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
|