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.
Files changed (63) hide show
  1. teleprox-2.1.3/LICENSE +27 -0
  2. teleprox-2.1.3/PKG-INFO +88 -0
  3. teleprox-2.1.3/README.md +46 -0
  4. teleprox-2.1.3/examples/advanced_logging.py +488 -0
  5. teleprox-2.1.3/examples/conda_env.py +10 -0
  6. teleprox-2.1.3/examples/custom_logviewer_with_docs.py +333 -0
  7. teleprox-2.1.3/examples/daemon.py +45 -0
  8. teleprox-2.1.3/examples/remote_logging.py +61 -0
  9. teleprox-2.1.3/examples/shared_memory.py +42 -0
  10. teleprox-2.1.3/pyproject.toml +31 -0
  11. teleprox-2.1.3/setup.cfg +4 -0
  12. teleprox-2.1.3/teleprox/__init__.py +8 -0
  13. teleprox-2.1.3/teleprox/bootstrap.py +135 -0
  14. teleprox-2.1.3/teleprox/client.py +789 -0
  15. teleprox-2.1.3/teleprox/log/__init__.py +36 -0
  16. teleprox-2.1.3/teleprox/log/handler.py +156 -0
  17. teleprox-2.1.3/teleprox/log/logviewer/__init__.py +6 -0
  18. teleprox-2.1.3/teleprox/log/logviewer/constants.py +74 -0
  19. teleprox-2.1.3/teleprox/log/logviewer/export.py +402 -0
  20. teleprox-2.1.3/teleprox/log/logviewer/filtering.py +310 -0
  21. teleprox-2.1.3/teleprox/log/logviewer/log_model.py +770 -0
  22. teleprox-2.1.3/teleprox/log/logviewer/proxies.py +59 -0
  23. teleprox-2.1.3/teleprox/log/logviewer/tests/test_child_filtering.py +246 -0
  24. teleprox-2.1.3/teleprox/log/logviewer/tests/test_child_inheritance.py +94 -0
  25. teleprox-2.1.3/teleprox/log/logviewer/tests/test_child_ui_behavior.py +228 -0
  26. teleprox-2.1.3/teleprox/log/logviewer/tests/test_code_line_clicking.py +77 -0
  27. teleprox-2.1.3/teleprox/log/logviewer/tests/test_column_data_mapping.py +75 -0
  28. teleprox-2.1.3/teleprox/log/logviewer/tests/test_column_filtering.py +229 -0
  29. teleprox-2.1.3/teleprox/log/logviewer/tests/test_column_filtering_integration.py +117 -0
  30. teleprox-2.1.3/teleprox/log/logviewer/tests/test_context_menu_copy.py +97 -0
  31. teleprox-2.1.3/teleprox/log/logviewer/tests/test_export.py +598 -0
  32. teleprox-2.1.3/teleprox/log/logviewer/tests/test_filter_data_roles.py +56 -0
  33. teleprox-2.1.3/teleprox/log/logviewer/tests/test_filter_proxies.py +155 -0
  34. teleprox-2.1.3/teleprox/log/logviewer/tests/test_filter_utilities.py +87 -0
  35. teleprox-2.1.3/teleprox/log/logviewer/tests/test_filtering_integration.py +161 -0
  36. teleprox-2.1.3/teleprox/log/logviewer/tests/test_lazy_loading.py +216 -0
  37. teleprox-2.1.3/teleprox/log/logviewer/tests/test_level_filtering.py +179 -0
  38. teleprox-2.1.3/teleprox/log/logviewer/tests/test_search.py +323 -0
  39. teleprox-2.1.3/teleprox/log/logviewer/tests/test_set_records.py +248 -0
  40. teleprox-2.1.3/teleprox/log/logviewer/tests/test_sorting.py +185 -0
  41. teleprox-2.1.3/teleprox/log/logviewer/tests/test_thread_safety.py +121 -0
  42. teleprox-2.1.3/teleprox/log/logviewer/utils.py +121 -0
  43. teleprox-2.1.3/teleprox/log/logviewer/viewer.py +653 -0
  44. teleprox-2.1.3/teleprox/log/logviewer/widgets.py +437 -0
  45. teleprox-2.1.3/teleprox/log/remote.py +341 -0
  46. teleprox-2.1.3/teleprox/log/stdio.py +49 -0
  47. teleprox-2.1.3/teleprox/process.py +384 -0
  48. teleprox-2.1.3/teleprox/processspawner.py +4 -0
  49. teleprox-2.1.3/teleprox/proxy.py +577 -0
  50. teleprox-2.1.3/teleprox/qt.py +5 -0
  51. teleprox-2.1.3/teleprox/qt_poll_thread.py +70 -0
  52. teleprox-2.1.3/teleprox/qt_server.py +85 -0
  53. teleprox-2.1.3/teleprox/qt_util.py +100 -0
  54. teleprox-2.1.3/teleprox/serializer.py +324 -0
  55. teleprox-2.1.3/teleprox/server.py +469 -0
  56. teleprox-2.1.3/teleprox/shmem.py +84 -0
  57. teleprox-2.1.3/teleprox/timer.py +85 -0
  58. teleprox-2.1.3/teleprox/util.py +196 -0
  59. teleprox-2.1.3/teleprox.egg-info/PKG-INFO +88 -0
  60. teleprox-2.1.3/teleprox.egg-info/SOURCES.txt +61 -0
  61. teleprox-2.1.3/teleprox.egg-info/dependency_links.txt +1 -0
  62. teleprox-2.1.3/teleprox.egg-info/requires.txt +2 -0
  63. 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.
@@ -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
+ [![Tests](https://github.com/campagnola/teleprox/actions/workflows/test.yml/badge.svg)](https://github.com/campagnola/teleprox/actions)
46
+ [![PyPI version](https://badge.fury.io/py/teleprox.svg)](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).
@@ -0,0 +1,46 @@
1
+ # Teleprox: simple python object proxies over TCP
2
+
3
+ [![Tests](https://github.com/campagnola/teleprox/actions/workflows/test.yml/badge.svg)](https://github.com/campagnola/teleprox/actions)
4
+ [![PyPI version](https://badge.fury.io/py/teleprox.svg)](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'])