teleprox 2.1.3__tar.gz
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.
- teleprox-2.1.3/LICENSE +27 -0
- teleprox-2.1.3/PKG-INFO +88 -0
- teleprox-2.1.3/README.md +46 -0
- teleprox-2.1.3/examples/advanced_logging.py +488 -0
- teleprox-2.1.3/examples/conda_env.py +10 -0
- teleprox-2.1.3/examples/custom_logviewer_with_docs.py +333 -0
- teleprox-2.1.3/examples/daemon.py +45 -0
- teleprox-2.1.3/examples/remote_logging.py +61 -0
- teleprox-2.1.3/examples/shared_memory.py +42 -0
- teleprox-2.1.3/pyproject.toml +31 -0
- teleprox-2.1.3/setup.cfg +4 -0
- teleprox-2.1.3/teleprox/__init__.py +8 -0
- teleprox-2.1.3/teleprox/bootstrap.py +135 -0
- teleprox-2.1.3/teleprox/client.py +789 -0
- teleprox-2.1.3/teleprox/log/__init__.py +36 -0
- teleprox-2.1.3/teleprox/log/handler.py +156 -0
- teleprox-2.1.3/teleprox/log/logviewer/__init__.py +6 -0
- teleprox-2.1.3/teleprox/log/logviewer/constants.py +74 -0
- teleprox-2.1.3/teleprox/log/logviewer/export.py +402 -0
- teleprox-2.1.3/teleprox/log/logviewer/filtering.py +310 -0
- teleprox-2.1.3/teleprox/log/logviewer/log_model.py +770 -0
- teleprox-2.1.3/teleprox/log/logviewer/proxies.py +59 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_child_filtering.py +246 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_child_inheritance.py +94 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_child_ui_behavior.py +228 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_code_line_clicking.py +77 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_column_data_mapping.py +75 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_column_filtering.py +229 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_column_filtering_integration.py +117 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_context_menu_copy.py +97 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_export.py +598 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_filter_data_roles.py +56 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_filter_proxies.py +155 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_filter_utilities.py +87 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_filtering_integration.py +161 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_lazy_loading.py +216 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_level_filtering.py +179 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_search.py +323 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_set_records.py +248 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_sorting.py +185 -0
- teleprox-2.1.3/teleprox/log/logviewer/tests/test_thread_safety.py +121 -0
- teleprox-2.1.3/teleprox/log/logviewer/utils.py +121 -0
- teleprox-2.1.3/teleprox/log/logviewer/viewer.py +653 -0
- teleprox-2.1.3/teleprox/log/logviewer/widgets.py +437 -0
- teleprox-2.1.3/teleprox/log/remote.py +341 -0
- teleprox-2.1.3/teleprox/log/stdio.py +49 -0
- teleprox-2.1.3/teleprox/process.py +384 -0
- teleprox-2.1.3/teleprox/processspawner.py +4 -0
- teleprox-2.1.3/teleprox/proxy.py +577 -0
- teleprox-2.1.3/teleprox/qt.py +5 -0
- teleprox-2.1.3/teleprox/qt_poll_thread.py +70 -0
- teleprox-2.1.3/teleprox/qt_server.py +85 -0
- teleprox-2.1.3/teleprox/qt_util.py +100 -0
- teleprox-2.1.3/teleprox/serializer.py +324 -0
- teleprox-2.1.3/teleprox/server.py +469 -0
- teleprox-2.1.3/teleprox/shmem.py +84 -0
- teleprox-2.1.3/teleprox/timer.py +85 -0
- teleprox-2.1.3/teleprox/util.py +196 -0
- teleprox-2.1.3/teleprox.egg-info/PKG-INFO +88 -0
- teleprox-2.1.3/teleprox.egg-info/SOURCES.txt +61 -0
- teleprox-2.1.3/teleprox.egg-info/dependency_links.txt +1 -0
- teleprox-2.1.3/teleprox.egg-info/requires.txt +2 -0
- teleprox-2.1.3/teleprox.egg-info/top_level.txt +3 -0
teleprox-2.1.3/LICENSE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2016, French National Center for Scientific Research (CNRS)
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
3. Neither the name of the CNRS nor the names of its contributors may be used
|
|
15
|
+
to endorse or promote products derived from this software without specific
|
|
16
|
+
prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY
|
|
22
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
23
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
24
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
25
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
26
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
27
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
teleprox-2.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: teleprox
|
|
3
|
+
Version: 2.1.3
|
|
4
|
+
Summary: Object proxies over TCP
|
|
5
|
+
Author: Luke Campagnola, Samuel Garcia, Martin Chase
|
|
6
|
+
License: Copyright (c) 2016, French National Center for Scientific Research (CNRS)
|
|
7
|
+
All rights reserved.
|
|
8
|
+
|
|
9
|
+
Redistribution and use in source and binary forms, with or without
|
|
10
|
+
modification, are permitted provided that the following conditions are met:
|
|
11
|
+
|
|
12
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
13
|
+
list of conditions and the following disclaimer.
|
|
14
|
+
|
|
15
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
16
|
+
this list of conditions and the following disclaimer in the documentation
|
|
17
|
+
and/or other materials provided with the distribution.
|
|
18
|
+
|
|
19
|
+
3. Neither the name of the CNRS nor the names of its contributors may be used
|
|
20
|
+
to endorse or promote products derived from this software without specific
|
|
21
|
+
prior written permission.
|
|
22
|
+
|
|
23
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
24
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
25
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
26
|
+
DISCLAIMED. IN NO EVENT SHALL THE HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY
|
|
27
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
28
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
29
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
30
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
31
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
32
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
33
|
+
|
|
34
|
+
Project-URL: Homepage, http://github.com/campagnola/teleprox
|
|
35
|
+
Project-URL: Repository, http://github.com/campagnola/teleprox
|
|
36
|
+
Requires-Python: >=3.6
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
License-File: LICENSE
|
|
39
|
+
Requires-Dist: pyzmq
|
|
40
|
+
Requires-Dist: msgpack
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# Teleprox: simple python object proxies over TCP
|
|
44
|
+
|
|
45
|
+
[](https://github.com/campagnola/teleprox/actions)
|
|
46
|
+
[](https://badge.fury.io/py/teleprox)
|
|
47
|
+
|
|
48
|
+
No declarations required; just access remote objects as if they are local.
|
|
49
|
+
|
|
50
|
+
Requires
|
|
51
|
+
--------
|
|
52
|
+
|
|
53
|
+
- python 3
|
|
54
|
+
- pyzmq
|
|
55
|
+
- msgpack
|
|
56
|
+
- numpy (optional; required only for SharedNDArray)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
Examples
|
|
60
|
+
--------
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from teleprox import start_process
|
|
64
|
+
import time
|
|
65
|
+
|
|
66
|
+
# start a new process
|
|
67
|
+
proc = start_process()
|
|
68
|
+
|
|
69
|
+
# import os in the remote process
|
|
70
|
+
remote_os = proc.client._import('os')
|
|
71
|
+
|
|
72
|
+
# call os.getpid() in the remote process
|
|
73
|
+
pid = remote_os.getpid()
|
|
74
|
+
|
|
75
|
+
# or, call getpid asynchronously and wait for the result:
|
|
76
|
+
request = remote_os.getpid(_sync='async')
|
|
77
|
+
while not request.hasResult():
|
|
78
|
+
time.sleep(0.01)
|
|
79
|
+
pid = request.result()
|
|
80
|
+
|
|
81
|
+
# write to sys.stdout in the remote process, and ignore the return value
|
|
82
|
+
remote_sys = proc.client._import('sys')
|
|
83
|
+
remote_sys.stdout.write('hello', _sync='off')
|
|
84
|
+
|
|
85
|
+
proc.stop()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Teleprox was originally developed as pyacq.core.rpc by the French National Center for Scientific Research (CNRS).
|
teleprox-2.1.3/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Teleprox: simple python object proxies over TCP
|
|
2
|
+
|
|
3
|
+
[](https://github.com/campagnola/teleprox/actions)
|
|
4
|
+
[](https://badge.fury.io/py/teleprox)
|
|
5
|
+
|
|
6
|
+
No declarations required; just access remote objects as if they are local.
|
|
7
|
+
|
|
8
|
+
Requires
|
|
9
|
+
--------
|
|
10
|
+
|
|
11
|
+
- python 3
|
|
12
|
+
- pyzmq
|
|
13
|
+
- msgpack
|
|
14
|
+
- numpy (optional; required only for SharedNDArray)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
Examples
|
|
18
|
+
--------
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from teleprox import start_process
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
# start a new process
|
|
25
|
+
proc = start_process()
|
|
26
|
+
|
|
27
|
+
# import os in the remote process
|
|
28
|
+
remote_os = proc.client._import('os')
|
|
29
|
+
|
|
30
|
+
# call os.getpid() in the remote process
|
|
31
|
+
pid = remote_os.getpid()
|
|
32
|
+
|
|
33
|
+
# or, call getpid asynchronously and wait for the result:
|
|
34
|
+
request = remote_os.getpid(_sync='async')
|
|
35
|
+
while not request.hasResult():
|
|
36
|
+
time.sleep(0.01)
|
|
37
|
+
pid = request.result()
|
|
38
|
+
|
|
39
|
+
# write to sys.stdout in the remote process, and ignore the return value
|
|
40
|
+
remote_sys = proc.client._import('sys')
|
|
41
|
+
remote_sys.stdout.write('hello', _sync='off')
|
|
42
|
+
|
|
43
|
+
proc.stop()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Teleprox was originally developed as pyacq.core.rpc by the French National Center for Scientific Research (CNRS).
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Advanced logging example with daemon process, GUI controls, and reconnection capability
|
|
3
|
+
Demonstrates interactive log generation and remote log collection in a GUI environment
|
|
4
|
+
"""
|
|
5
|
+
import atexit
|
|
6
|
+
import logging
|
|
7
|
+
import signal
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
from PyQt5 import QtWidgets, QtCore
|
|
12
|
+
|
|
13
|
+
import teleprox
|
|
14
|
+
import teleprox.log
|
|
15
|
+
from teleprox.log.remote import LogServer
|
|
16
|
+
from teleprox.log.logviewer import LogViewer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_daemon_gui():
|
|
20
|
+
"""Function to be imported by daemon process to create its own GUI"""
|
|
21
|
+
|
|
22
|
+
class DaemonGUI(QtWidgets.QWidget):
|
|
23
|
+
def __init__(self):
|
|
24
|
+
super().__init__()
|
|
25
|
+
self.message_count = 0
|
|
26
|
+
self.setup_ui()
|
|
27
|
+
|
|
28
|
+
def setup_ui(self):
|
|
29
|
+
self.setWindowTitle("Daemon Process - Independent GUI")
|
|
30
|
+
self.setGeometry(600, 100, 350, 250)
|
|
31
|
+
|
|
32
|
+
layout = QtWidgets.QVBoxLayout()
|
|
33
|
+
|
|
34
|
+
# Status info
|
|
35
|
+
import os
|
|
36
|
+
pid_label = QtWidgets.QLabel(f"Daemon PID: {os.getpid()}")
|
|
37
|
+
layout.addWidget(pid_label)
|
|
38
|
+
|
|
39
|
+
self.status_label = QtWidgets.QLabel("Daemon process is running independently")
|
|
40
|
+
layout.addWidget(self.status_label)
|
|
41
|
+
|
|
42
|
+
# Log generation controls
|
|
43
|
+
self.log_btn = QtWidgets.QPushButton("Generate Log Message")
|
|
44
|
+
self.log_btn.clicked.connect(self.generate_log)
|
|
45
|
+
layout.addWidget(self.log_btn)
|
|
46
|
+
|
|
47
|
+
self.auto_log_btn = QtWidgets.QPushButton("Start Auto-Logging (5s)")
|
|
48
|
+
self.auto_log_btn.clicked.connect(self.toggle_auto_log)
|
|
49
|
+
layout.addWidget(self.auto_log_btn)
|
|
50
|
+
|
|
51
|
+
self.exception_btn = QtWidgets.QPushButton("Create Exception")
|
|
52
|
+
self.exception_btn.clicked.connect(self.create_exception)
|
|
53
|
+
layout.addWidget(self.exception_btn)
|
|
54
|
+
|
|
55
|
+
self.message_count_label = QtWidgets.QLabel("Messages sent: 0")
|
|
56
|
+
layout.addWidget(self.message_count_label)
|
|
57
|
+
|
|
58
|
+
# Log level selector
|
|
59
|
+
level_layout = QtWidgets.QHBoxLayout()
|
|
60
|
+
level_layout.addWidget(QtWidgets.QLabel("Log Level:"))
|
|
61
|
+
self.level_combo = QtWidgets.QComboBox()
|
|
62
|
+
self.level_combo.addItems(['DEBUG', 'INFO', 'WARNING', 'ERROR'])
|
|
63
|
+
self.level_combo.setCurrentText('INFO')
|
|
64
|
+
level_layout.addWidget(self.level_combo)
|
|
65
|
+
layout.addLayout(level_layout)
|
|
66
|
+
|
|
67
|
+
self.setLayout(layout)
|
|
68
|
+
|
|
69
|
+
# Timer for auto-logging
|
|
70
|
+
self.auto_timer = QtCore.QTimer()
|
|
71
|
+
self.auto_timer.timeout.connect(self.generate_log)
|
|
72
|
+
self.auto_logging = False
|
|
73
|
+
|
|
74
|
+
def generate_log(self):
|
|
75
|
+
self.message_count += 1
|
|
76
|
+
level = self.level_combo.currentText()
|
|
77
|
+
message = f"Daemon-generated message #{self.message_count} (level: {level})"
|
|
78
|
+
|
|
79
|
+
# Log at the selected level
|
|
80
|
+
if level == 'DEBUG':
|
|
81
|
+
logging.debug(message)
|
|
82
|
+
elif level == 'INFO':
|
|
83
|
+
logging.info(message)
|
|
84
|
+
elif level == 'WARNING':
|
|
85
|
+
logging.warning(message)
|
|
86
|
+
elif level == 'ERROR':
|
|
87
|
+
logging.error(message)
|
|
88
|
+
|
|
89
|
+
self.message_count_label.setText(f"Messages sent: {self.message_count}")
|
|
90
|
+
|
|
91
|
+
def toggle_auto_log(self):
|
|
92
|
+
if self.auto_logging:
|
|
93
|
+
self.auto_timer.stop()
|
|
94
|
+
self.auto_log_btn.setText("Start Auto-Logging (5s)")
|
|
95
|
+
self.auto_logging = False
|
|
96
|
+
else:
|
|
97
|
+
self.auto_timer.start(5000) # 5 seconds
|
|
98
|
+
self.auto_log_btn.setText("Stop Auto-Logging")
|
|
99
|
+
self.auto_logging = True
|
|
100
|
+
|
|
101
|
+
def create_exception(self):
|
|
102
|
+
"""Create an exception and log it"""
|
|
103
|
+
try:
|
|
104
|
+
# Create a realistic exception scenario
|
|
105
|
+
data = {"key": "value"}
|
|
106
|
+
missing_key = data["nonexistent_key"] # This will raise KeyError
|
|
107
|
+
except KeyError as e:
|
|
108
|
+
logging.exception("Exception occurred while accessing data")
|
|
109
|
+
self.message_count += 1
|
|
110
|
+
self.message_count_label.setText(f"Messages sent: {self.message_count}")
|
|
111
|
+
|
|
112
|
+
def show(self):
|
|
113
|
+
super().show()
|
|
114
|
+
self.raise_()
|
|
115
|
+
self.activateWindow()
|
|
116
|
+
|
|
117
|
+
# Create and show the GUI
|
|
118
|
+
gui = DaemonGUI()
|
|
119
|
+
gui.show()
|
|
120
|
+
return gui
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class DaemonController(QtWidgets.QWidget):
|
|
124
|
+
"""Main controller window that manages the daemon process and log viewing"""
|
|
125
|
+
|
|
126
|
+
def __init__(self):
|
|
127
|
+
super().__init__()
|
|
128
|
+
self.daemon = None
|
|
129
|
+
self.daemon_address = None
|
|
130
|
+
self.log_server = None
|
|
131
|
+
self.setup_ui()
|
|
132
|
+
self.setup_logging()
|
|
133
|
+
self.setup_signal_handlers()
|
|
134
|
+
|
|
135
|
+
def setup_ui(self):
|
|
136
|
+
"""Create the UI controls"""
|
|
137
|
+
self.setWindowTitle("Advanced Logging Example - Controller")
|
|
138
|
+
self.setGeometry(100, 100, 1000, 700)
|
|
139
|
+
|
|
140
|
+
layout = QtWidgets.QVBoxLayout()
|
|
141
|
+
|
|
142
|
+
# Daemon controls
|
|
143
|
+
daemon_group = QtWidgets.QGroupBox("Daemon Process Control")
|
|
144
|
+
daemon_layout = QtWidgets.QVBoxLayout()
|
|
145
|
+
|
|
146
|
+
self.start_daemon_btn = QtWidgets.QPushButton("Start Daemon Process")
|
|
147
|
+
self.start_daemon_btn.clicked.connect(self.start_daemon)
|
|
148
|
+
daemon_layout.addWidget(self.start_daemon_btn)
|
|
149
|
+
|
|
150
|
+
self.daemon_status_label = QtWidgets.QLabel("Status: No daemon running")
|
|
151
|
+
daemon_layout.addWidget(self.daemon_status_label)
|
|
152
|
+
|
|
153
|
+
self.reconnect_btn = QtWidgets.QPushButton("Reconnect to Daemon")
|
|
154
|
+
self.reconnect_btn.clicked.connect(self.reconnect_daemon)
|
|
155
|
+
self.reconnect_btn.setEnabled(False)
|
|
156
|
+
daemon_layout.addWidget(self.reconnect_btn)
|
|
157
|
+
|
|
158
|
+
daemon_group.setLayout(daemon_layout)
|
|
159
|
+
layout.addWidget(daemon_group)
|
|
160
|
+
|
|
161
|
+
# Test connection button
|
|
162
|
+
self.test_connection_btn = QtWidgets.QPushButton("Test Connection to Daemon")
|
|
163
|
+
self.test_connection_btn.clicked.connect(self.test_connection)
|
|
164
|
+
self.test_connection_btn.setEnabled(False)
|
|
165
|
+
layout.addWidget(self.test_connection_btn)
|
|
166
|
+
|
|
167
|
+
# Embedded log viewer
|
|
168
|
+
log_group = QtWidgets.QGroupBox("Log Viewer")
|
|
169
|
+
log_layout = QtWidgets.QVBoxLayout()
|
|
170
|
+
|
|
171
|
+
# Log viewer controls
|
|
172
|
+
viewer_controls = QtWidgets.QHBoxLayout()
|
|
173
|
+
|
|
174
|
+
self.clear_logs_btn = QtWidgets.QPushButton("Clear Logs")
|
|
175
|
+
self.clear_logs_btn.clicked.connect(self.clear_logs)
|
|
176
|
+
viewer_controls.addWidget(self.clear_logs_btn)
|
|
177
|
+
|
|
178
|
+
self.load_sample_logs_btn = QtWidgets.QPushButton("Load Sample Historical Logs")
|
|
179
|
+
self.load_sample_logs_btn.clicked.connect(self.load_sample_historical_logs)
|
|
180
|
+
viewer_controls.addWidget(self.load_sample_logs_btn)
|
|
181
|
+
|
|
182
|
+
viewer_controls.addStretch() # Push buttons to the left
|
|
183
|
+
log_layout.addLayout(viewer_controls)
|
|
184
|
+
|
|
185
|
+
self.log_viewer = LogViewer()
|
|
186
|
+
log_layout.addWidget(self.log_viewer)
|
|
187
|
+
|
|
188
|
+
log_group.setLayout(log_layout)
|
|
189
|
+
layout.addWidget(log_group)
|
|
190
|
+
|
|
191
|
+
self.setLayout(layout)
|
|
192
|
+
|
|
193
|
+
def setup_logging(self):
|
|
194
|
+
"""Set up logging for this process"""
|
|
195
|
+
teleprox.log.basic_config(log_level='DEBUG', exceptions=False)
|
|
196
|
+
self.log("Controller logging set up")
|
|
197
|
+
|
|
198
|
+
def setup_signal_handlers(self):
|
|
199
|
+
"""Set up signal handlers for proper daemon cleanup"""
|
|
200
|
+
|
|
201
|
+
def cleanup_and_exit(signum, frame):
|
|
202
|
+
self.log(f"Received signal {signum}, cleaning up...")
|
|
203
|
+
self.cleanup_daemon()
|
|
204
|
+
sys.exit(0)
|
|
205
|
+
|
|
206
|
+
# Register handlers for common termination signals
|
|
207
|
+
signal.signal(signal.SIGINT, cleanup_and_exit)
|
|
208
|
+
signal.signal(signal.SIGTERM, cleanup_and_exit)
|
|
209
|
+
|
|
210
|
+
# Also register atexit handler as fallback
|
|
211
|
+
atexit.register(self.cleanup_daemon)
|
|
212
|
+
|
|
213
|
+
def cleanup_daemon(self):
|
|
214
|
+
"""Clean up daemon process if it exists"""
|
|
215
|
+
if self.daemon is not None:
|
|
216
|
+
try:
|
|
217
|
+
self.log(f"Cleaning up daemon process {self.daemon.pid}")
|
|
218
|
+
self.daemon.kill()
|
|
219
|
+
self.daemon = None
|
|
220
|
+
except Exception as e:
|
|
221
|
+
self.log(f"Error cleaning up daemon: {e}")
|
|
222
|
+
|
|
223
|
+
# Also clean up log server
|
|
224
|
+
self._cleanup_log_server()
|
|
225
|
+
|
|
226
|
+
def log(self, message):
|
|
227
|
+
"""Log message"""
|
|
228
|
+
logging.info(message)
|
|
229
|
+
|
|
230
|
+
def _create_new_log_server(self):
|
|
231
|
+
"""Create a new LogServer instance for collecting logs from daemon
|
|
232
|
+
|
|
233
|
+
NOTE: This creates a new log server each time for testing purposes to demonstrate
|
|
234
|
+
that the daemon can be reconfigured to use different log servers. In normal use,
|
|
235
|
+
you would typically use the global log server throughout the application lifecycle:
|
|
236
|
+
|
|
237
|
+
# Normal usage (simpler):
|
|
238
|
+
teleprox.log.start_log_server() # Create global log server once
|
|
239
|
+
log_addr = teleprox.log.get_logger_address() # Get its address
|
|
240
|
+
# Use log_addr for all daemon processes
|
|
241
|
+
"""
|
|
242
|
+
# Clean up any existing log server
|
|
243
|
+
self._cleanup_log_server()
|
|
244
|
+
|
|
245
|
+
# Create new log server attached to the root logger
|
|
246
|
+
self.log_server = LogServer(logging.getLogger())
|
|
247
|
+
self.log(f"Created new log server at {self.log_server.address}")
|
|
248
|
+
|
|
249
|
+
def _cleanup_log_server(self):
|
|
250
|
+
"""Clean up existing log server if it exists"""
|
|
251
|
+
if self.log_server is not None:
|
|
252
|
+
try:
|
|
253
|
+
self.log_server.stop()
|
|
254
|
+
self.log_server = None
|
|
255
|
+
self.log("Cleaned up old log server")
|
|
256
|
+
except Exception as e:
|
|
257
|
+
self.log(f"Error cleaning up log server: {e}")
|
|
258
|
+
|
|
259
|
+
def start_daemon(self):
|
|
260
|
+
"""Start the daemon process with GUI capabilities"""
|
|
261
|
+
try:
|
|
262
|
+
self.log("Starting daemon process...")
|
|
263
|
+
|
|
264
|
+
# Create a new log server for this connection
|
|
265
|
+
self._create_new_log_server()
|
|
266
|
+
|
|
267
|
+
# Start daemon with Qt support and logging directed to this process
|
|
268
|
+
self.daemon = teleprox.start_process(
|
|
269
|
+
'advanced-logging-daemon',
|
|
270
|
+
daemon=True,
|
|
271
|
+
qt=True, # Enable Qt event loop in daemon
|
|
272
|
+
log_addr=self.log_server.address,
|
|
273
|
+
log_level=logging.DEBUG
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
self.daemon_address = self.daemon.client.address
|
|
277
|
+
|
|
278
|
+
# Set up the daemon with its own independent GUI
|
|
279
|
+
try:
|
|
280
|
+
# Get the examples directory path from this process
|
|
281
|
+
import os
|
|
282
|
+
examples_dir = os.path.dirname(os.path.abspath(__file__))
|
|
283
|
+
self.log(f"Examples directory: {examples_dir}")
|
|
284
|
+
|
|
285
|
+
# Add examples directory to daemon's Python path
|
|
286
|
+
r_sys = self.daemon.client._import('sys')
|
|
287
|
+
r_sys.path.append(examples_dir)
|
|
288
|
+
self.log("Added examples dir to daemon's Python path")
|
|
289
|
+
|
|
290
|
+
# Create QApplication in daemon if it doesn't exist
|
|
291
|
+
r_qtwidgets = self.daemon.client._import('PyQt5.QtWidgets')
|
|
292
|
+
daemon_app = r_qtwidgets.QApplication.instance()
|
|
293
|
+
if daemon_app is None:
|
|
294
|
+
daemon_app = r_qtwidgets.QApplication([])
|
|
295
|
+
self.log("Created Qt application in daemon")
|
|
296
|
+
|
|
297
|
+
# Import daemon GUI module and create the GUI
|
|
298
|
+
daemon_gui_module = self.daemon.client._import('advanced_logging')
|
|
299
|
+
daemon_gui_module.create_daemon_gui()
|
|
300
|
+
self.log("Created independent GUI window in daemon process")
|
|
301
|
+
|
|
302
|
+
except Exception as gui_error:
|
|
303
|
+
self.log(f"GUI setup error: {gui_error}")
|
|
304
|
+
# Continue anyway - daemon can still work without GUI
|
|
305
|
+
|
|
306
|
+
self.log(f"Daemon started with PID {self.daemon.pid} at {self.daemon_address}")
|
|
307
|
+
|
|
308
|
+
# Update UI
|
|
309
|
+
self.start_daemon_btn.setEnabled(False)
|
|
310
|
+
self.reconnect_btn.setEnabled(True)
|
|
311
|
+
self.test_connection_btn.setEnabled(True)
|
|
312
|
+
self.daemon_status_label.setText(f"Status: Daemon running (PID {self.daemon.pid})")
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
self.log(f"Failed to start daemon: {e}")
|
|
316
|
+
|
|
317
|
+
def reconnect_daemon(self):
|
|
318
|
+
"""Demonstrate reconnecting to the daemon with a new log server"""
|
|
319
|
+
if not self.daemon_address:
|
|
320
|
+
self.log("No daemon address available for reconnection")
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
self.log("Simulating reconnection with new log server...")
|
|
325
|
+
|
|
326
|
+
# Create a new log server for this reconnection
|
|
327
|
+
old_log_address = self.log_server.address if self.log_server else "none"
|
|
328
|
+
self._create_new_log_server()
|
|
329
|
+
self.log(f"Switched from log server {old_log_address} to {self.log_server.address}")
|
|
330
|
+
|
|
331
|
+
# Close existing connection
|
|
332
|
+
if self.daemon and self.daemon.client:
|
|
333
|
+
self.daemon.client.close()
|
|
334
|
+
self.log("Closed existing connection")
|
|
335
|
+
|
|
336
|
+
# Create new client connection
|
|
337
|
+
new_client = teleprox.RPCClient.get_client(address=self.daemon_address)
|
|
338
|
+
|
|
339
|
+
# Verify connection by getting PID
|
|
340
|
+
r_os = new_client._import('os')
|
|
341
|
+
pid = r_os.getpid()
|
|
342
|
+
|
|
343
|
+
self.log(f"Reconnected to daemon at {self.daemon_address} (PID {pid})")
|
|
344
|
+
|
|
345
|
+
# Configure daemon to use the new log server
|
|
346
|
+
new_client._import('teleprox.log').set_logger_address(self.log_server.address)
|
|
347
|
+
self.log(f"Configured daemon to use new log server at {self.log_server.address}")
|
|
348
|
+
|
|
349
|
+
# Update our reference
|
|
350
|
+
if self.daemon:
|
|
351
|
+
self.daemon.client = new_client
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
self.log(f"Reconnection failed: {e}")
|
|
355
|
+
|
|
356
|
+
def test_connection(self):
|
|
357
|
+
"""Test connection to daemon by getting its PID"""
|
|
358
|
+
if not self.daemon:
|
|
359
|
+
self.log("No daemon available")
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
# Get daemon PID to verify connection
|
|
364
|
+
r_os = self.daemon.client._import('os')
|
|
365
|
+
pid = r_os.getpid()
|
|
366
|
+
self.log(f"Connection test successful - daemon PID: {pid}")
|
|
367
|
+
except Exception as e:
|
|
368
|
+
self.log(f"Connection test failed: {e}")
|
|
369
|
+
|
|
370
|
+
def clear_logs(self):
|
|
371
|
+
"""Clear all logs from the viewer using set_records()"""
|
|
372
|
+
self.log_viewer.set_records()
|
|
373
|
+
self.log("Cleared all logs from viewer")
|
|
374
|
+
|
|
375
|
+
def load_sample_historical_logs(self):
|
|
376
|
+
"""Load sample historical logs to demonstrate set_records() functionality"""
|
|
377
|
+
import datetime
|
|
378
|
+
|
|
379
|
+
self.log("Loading sample historical logs...")
|
|
380
|
+
|
|
381
|
+
# Create sample historical log records from a simulated "previous day"
|
|
382
|
+
base_time = time.time() - (24 * 60 * 60) # 24 hours ago
|
|
383
|
+
historical_records = []
|
|
384
|
+
|
|
385
|
+
# Create various types of historical log records
|
|
386
|
+
for i in range(10):
|
|
387
|
+
record_time = base_time + (i * 300) # 5 minutes apart
|
|
388
|
+
|
|
389
|
+
# Create different types of records
|
|
390
|
+
if i == 0:
|
|
391
|
+
# System startup record
|
|
392
|
+
rec = logging.LogRecord(
|
|
393
|
+
name='system.startup',
|
|
394
|
+
level=logging.INFO,
|
|
395
|
+
pathname='/app/startup.py',
|
|
396
|
+
lineno=42,
|
|
397
|
+
msg="System startup initiated",
|
|
398
|
+
args=(),
|
|
399
|
+
exc_info=None
|
|
400
|
+
)
|
|
401
|
+
elif i == 3:
|
|
402
|
+
# Warning with extra attributes
|
|
403
|
+
rec = logging.LogRecord(
|
|
404
|
+
name='app.performance',
|
|
405
|
+
level=logging.WARNING,
|
|
406
|
+
pathname='/app/monitor.py',
|
|
407
|
+
lineno=156,
|
|
408
|
+
msg=f"High memory usage detected: {85.3}%",
|
|
409
|
+
args=(),
|
|
410
|
+
exc_info=None
|
|
411
|
+
)
|
|
412
|
+
rec.memory_percent = 85.3
|
|
413
|
+
rec.process_count = 47
|
|
414
|
+
elif i == 7:
|
|
415
|
+
# Error with simulated exception
|
|
416
|
+
try:
|
|
417
|
+
# Simulate an error that would have occurred
|
|
418
|
+
raise ConnectionError("Database connection timeout after 30s")
|
|
419
|
+
except ConnectionError:
|
|
420
|
+
exc_info = sys.exc_info()
|
|
421
|
+
|
|
422
|
+
rec = logging.LogRecord(
|
|
423
|
+
name='database.connection',
|
|
424
|
+
level=logging.ERROR,
|
|
425
|
+
pathname='/app/db.py',
|
|
426
|
+
lineno=298,
|
|
427
|
+
msg="Failed to connect to database",
|
|
428
|
+
args=(),
|
|
429
|
+
exc_info=exc_info
|
|
430
|
+
)
|
|
431
|
+
else:
|
|
432
|
+
# Regular info messages
|
|
433
|
+
messages = [
|
|
434
|
+
"User authentication successful",
|
|
435
|
+
"Processing batch job #1247",
|
|
436
|
+
"Cache cleanup completed",
|
|
437
|
+
"Scheduled backup started",
|
|
438
|
+
"API endpoint /users accessed",
|
|
439
|
+
"Configuration file reloaded",
|
|
440
|
+
"Network health check passed"
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
rec = logging.LogRecord(
|
|
444
|
+
name=f'app.module{i}',
|
|
445
|
+
level=logging.INFO,
|
|
446
|
+
pathname=f'/app/module{i}.py',
|
|
447
|
+
lineno=100 + i,
|
|
448
|
+
msg=messages[i % len(messages)],
|
|
449
|
+
args=(),
|
|
450
|
+
exc_info=None
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# Set the timestamp to our historical time
|
|
454
|
+
rec.created = record_time
|
|
455
|
+
rec.msecs = (record_time % 1) * 1000
|
|
456
|
+
|
|
457
|
+
# Set realistic process/thread info for historical records
|
|
458
|
+
rec.processName = f"HistoricalProcess-{(i % 3) + 1}"
|
|
459
|
+
rec.threadName = f"Thread-{(i % 2) + 1}"
|
|
460
|
+
|
|
461
|
+
historical_records.append(rec)
|
|
462
|
+
|
|
463
|
+
# Use set_records to replace all current logs with historical ones
|
|
464
|
+
self.log_viewer.set_records(*historical_records)
|
|
465
|
+
|
|
466
|
+
# Log what we just did (this will appear in the viewer since it's a new record)
|
|
467
|
+
yesterday = datetime.datetime.fromtimestamp(base_time).strftime("%Y-%m-%d")
|
|
468
|
+
self.log(f"Loaded {len(historical_records)} historical log records from {yesterday}")
|
|
469
|
+
self.log("Notice how set_records() replaced all existing logs and preserved filters!")
|
|
470
|
+
|
|
471
|
+
def closeEvent(self, event):
|
|
472
|
+
"""Handle window close event - clean up daemon process"""
|
|
473
|
+
self.cleanup_daemon()
|
|
474
|
+
event.accept()
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def main():
|
|
478
|
+
"""Main entry point"""
|
|
479
|
+
app = QtWidgets.QApplication(sys.argv)
|
|
480
|
+
|
|
481
|
+
controller = DaemonController()
|
|
482
|
+
controller.show()
|
|
483
|
+
|
|
484
|
+
sys.exit(app.exec_())
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
if __name__ == '__main__':
|
|
488
|
+
main()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from teleprox import start_process
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
print("Running from conda env:", os.environ['CONDA_DEFAULT_ENV'])
|
|
6
|
+
|
|
7
|
+
proc = start_process(conda_env='teleprox2')
|
|
8
|
+
|
|
9
|
+
remote_os = proc.client._import('os')
|
|
10
|
+
print("Remote conda env:", remote_os.environ['CONDA_DEFAULT_ENV'])
|