meerschaum 2.2.0.dev3__py3-none-any.whl → 2.2.0rc2__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.
- meerschaum/__main__.py +1 -1
- meerschaum/_internal/entry.py +1 -1
- meerschaum/actions/show.py +128 -42
- meerschaum/api/dash/callbacks/dashboard.py +2 -7
- meerschaum/api/dash/pipes.py +33 -9
- meerschaum/api/dash/plugins.py +25 -9
- meerschaum/api/resources/templates/termpage.html +3 -0
- meerschaum/api/routes/_login.py +5 -4
- meerschaum/api/routes/_plugins.py +6 -3
- meerschaum/config/_dash.py +11 -0
- meerschaum/config/_default.py +3 -1
- meerschaum/config/_jobs.py +10 -4
- meerschaum/config/_paths.py +1 -0
- meerschaum/config/_sync.py +2 -3
- meerschaum/config/_version.py +1 -1
- meerschaum/config/stack/__init__.py +6 -6
- meerschaum/config/stack/grafana/__init__.py +1 -1
- meerschaum/config/static/__init__.py +3 -1
- meerschaum/connectors/sql/_plugins.py +0 -2
- meerschaum/core/User/_User.py +156 -16
- meerschaum/core/User/__init__.py +1 -1
- meerschaum/plugins/_Plugin.py +1 -1
- meerschaum/utils/daemon/Daemon.py +63 -34
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +102 -0
- meerschaum/utils/daemon/RotatingFile.py +120 -14
- meerschaum/utils/daemon/__init__.py +1 -0
- meerschaum/utils/packages/__init__.py +9 -2
- meerschaum/utils/packages/_packages.py +3 -3
- meerschaum/utils/schedule.py +41 -47
- meerschaum/utils/threading.py +1 -0
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/METADATA +10 -9
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/RECORD +38 -36
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/WHEEL +1 -1
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/LICENSE +0 -0
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/NOTICE +0 -0
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/top_level.txt +0 -0
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/zip-safe +0 -0
@@ -108,9 +108,7 @@ def get_plugin_version(
|
|
108
108
|
plugins_tbl = get_tables(mrsm_instance=self, debug=debug)['plugins']
|
109
109
|
from meerschaum.utils.packages import attempt_import
|
110
110
|
sqlalchemy = attempt_import('sqlalchemy')
|
111
|
-
|
112
111
|
query = sqlalchemy.select(plugins_tbl.c.version).where(plugins_tbl.c.plugin_name == plugin.name)
|
113
|
-
|
114
112
|
return self.value(query, debug=debug)
|
115
113
|
|
116
114
|
def get_plugin_user_id(
|
meerschaum/core/User/_User.py
CHANGED
@@ -7,22 +7,155 @@ User class definition
|
|
7
7
|
"""
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
10
|
+
import os
|
11
|
+
import hashlib
|
12
|
+
import hmac
|
13
|
+
from binascii import b2a_base64, a2b_base64, Error as _BinAsciiError
|
14
|
+
from meerschaum.utils.typing import Optional, Dict, Any, Tuple
|
15
|
+
from meerschaum.config.static import STATIC_CONFIG
|
16
|
+
from meerschaum.utils.warnings import warn
|
17
|
+
|
18
|
+
|
19
|
+
__all__ = ('hash_password', 'verify_password', 'User')
|
20
|
+
|
21
|
+
def hash_password(
|
22
|
+
password: str,
|
23
|
+
salt: Optional[bytes] = None,
|
24
|
+
rounds: Optional[int] = None,
|
25
|
+
) -> str:
|
26
|
+
"""
|
27
|
+
Return an encoded hash string from the given password.
|
28
|
+
|
29
|
+
Parameters
|
30
|
+
----------
|
31
|
+
password: str
|
32
|
+
The password to be hashed.
|
33
|
+
|
34
|
+
salt: Optional[str], default None
|
35
|
+
If provided, use these bytes for the salt in the hash.
|
36
|
+
Otherwise defaults to 16 random bytes.
|
37
|
+
|
38
|
+
rounds: Optional[int], default None
|
39
|
+
If provided, use this number of rounds to generate the hash.
|
40
|
+
Defaults to 3,000,000.
|
41
|
+
|
42
|
+
Returns
|
43
|
+
-------
|
44
|
+
An encoded hash string to be stored in a database.
|
45
|
+
See the `passlib` documentation on the string format:
|
46
|
+
https://passlib.readthedocs.io/en/stable/lib/passlib.hash.pbkdf2_digest.html#format-algorithm
|
47
|
+
"""
|
48
|
+
hash_config = STATIC_CONFIG['users']['password_hash']
|
49
|
+
if password is None:
|
50
|
+
password = ''
|
51
|
+
if salt is None:
|
52
|
+
salt = os.urandom(hash_config['salt_bytes'])
|
53
|
+
if rounds is None:
|
54
|
+
rounds = hash_config['pbkdf2_sha256__default_rounds']
|
55
|
+
|
56
|
+
pw_hash = hashlib.pbkdf2_hmac(
|
57
|
+
hash_config['algorithm_name'],
|
58
|
+
password.encode('utf-8'),
|
59
|
+
salt,
|
60
|
+
rounds,
|
61
|
+
)
|
62
|
+
return (
|
63
|
+
f"$pbkdf2-{hash_config['algorithm_name']}"
|
64
|
+
+ f"${hash_config['pbkdf2_sha256__default_rounds']}"
|
65
|
+
+ '$' + ab64_encode(salt).decode('utf-8')
|
66
|
+
+ '$' + ab64_encode(pw_hash).decode('utf-8')
|
67
|
+
)
|
68
|
+
|
69
|
+
|
70
|
+
def verify_password(
|
71
|
+
password: str,
|
72
|
+
password_hash: str,
|
73
|
+
) -> bool:
|
74
|
+
"""
|
75
|
+
Return `True` if the password matches the provided hash.
|
76
|
+
|
77
|
+
Parameters
|
78
|
+
----------
|
79
|
+
password: str
|
80
|
+
The password to be checked.
|
81
|
+
|
82
|
+
password_hash: str
|
83
|
+
The encoded hash string as generated from `hash_password()`.
|
84
|
+
|
85
|
+
Returns
|
86
|
+
-------
|
87
|
+
A `bool` indicating whether `password` matches `password_hash`.
|
88
|
+
"""
|
89
|
+
hash_config = STATIC_CONFIG['users']['password_hash']
|
90
|
+
try:
|
91
|
+
digest, rounds_str, encoded_salt, encoded_checksum = password_hash.split('$')[1:]
|
92
|
+
algorithm_name = digest.split('-')[-1]
|
93
|
+
salt = ab64_decode(encoded_salt)
|
94
|
+
checksum = ab64_decode(encoded_checksum)
|
95
|
+
rounds = int(rounds_str)
|
96
|
+
except Exception as e:
|
97
|
+
warn(f"Failed to extract context from password hash '{password_hash}'. Is it corrupted?")
|
98
|
+
return False
|
99
|
+
|
100
|
+
return hmac.compare_digest(
|
101
|
+
checksum,
|
102
|
+
hashlib.pbkdf2_hmac(
|
103
|
+
algorithm_name,
|
104
|
+
password.encode('utf-8'),
|
105
|
+
salt,
|
106
|
+
rounds,
|
24
107
|
)
|
25
|
-
|
108
|
+
)
|
109
|
+
|
110
|
+
_BASE64_STRIP = b"=\n"
|
111
|
+
_BASE64_PAD1 = b"="
|
112
|
+
_BASE64_PAD2 = b"=="
|
113
|
+
|
114
|
+
def ab64_encode(data):
|
115
|
+
return b64s_encode(data).replace(b"+", b".")
|
116
|
+
|
117
|
+
def ab64_decode(data):
|
118
|
+
"""
|
119
|
+
decode from shortened base64 format which omits padding & whitespace.
|
120
|
+
uses custom ``./`` altchars, but supports decoding normal ``+/`` altchars as well.
|
121
|
+
"""
|
122
|
+
if isinstance(data, str):
|
123
|
+
# needs bytes for replace() call, but want to accept ascii-unicode ala a2b_base64()
|
124
|
+
try:
|
125
|
+
data = data.encode("ascii")
|
126
|
+
except UnicodeEncodeError:
|
127
|
+
raise ValueError("string argument should contain only ASCII characters")
|
128
|
+
return b64s_decode(data.replace(b".", b"+"))
|
129
|
+
|
130
|
+
|
131
|
+
def b64s_encode(data):
|
132
|
+
return b2a_base64(data).rstrip(_BASE64_STRIP)
|
133
|
+
|
134
|
+
def b64s_decode(data):
|
135
|
+
"""
|
136
|
+
decode from shortened base64 format which omits padding & whitespace.
|
137
|
+
uses default ``+/`` altchars.
|
138
|
+
"""
|
139
|
+
if isinstance(data, str):
|
140
|
+
# needs bytes for replace() call, but want to accept ascii-unicode ala a2b_base64()
|
141
|
+
try:
|
142
|
+
data = data.encode("ascii")
|
143
|
+
except UnicodeEncodeError as ue:
|
144
|
+
raise ValueError("string argument should contain only ASCII characters") from ue
|
145
|
+
off = len(data) & 3
|
146
|
+
if off == 0:
|
147
|
+
pass
|
148
|
+
elif off == 2:
|
149
|
+
data += _BASE64_PAD2
|
150
|
+
elif off == 3:
|
151
|
+
data += _BASE64_PAD1
|
152
|
+
else: # off == 1
|
153
|
+
raise ValueError("Invalid base64 input")
|
154
|
+
try:
|
155
|
+
return a2b_base64(data)
|
156
|
+
except _BinAsciiError as err:
|
157
|
+
raise TypeError(err) from err
|
158
|
+
|
26
159
|
|
27
160
|
class User:
|
28
161
|
"""
|
@@ -42,7 +175,6 @@ class User:
|
|
42
175
|
if password is None:
|
43
176
|
password = ''
|
44
177
|
self.password = password
|
45
|
-
self.password_hash = get_pwd_context().hash(password)
|
46
178
|
self.username = username
|
47
179
|
self.email = email
|
48
180
|
self.type = type
|
@@ -80,3 +212,11 @@ class User:
|
|
80
212
|
@user_id.setter
|
81
213
|
def user_id(self, user_id):
|
82
214
|
self._user_id = user_id
|
215
|
+
|
216
|
+
@property
|
217
|
+
def password_hash(self):
|
218
|
+
_password_hash = self.__dict__.get('_password_hash', None)
|
219
|
+
if _password_hash is not None:
|
220
|
+
return _password_hash
|
221
|
+
self._password_hash = hash_password(self.password)
|
222
|
+
return self._password_hash
|
meerschaum/core/User/__init__.py
CHANGED
meerschaum/plugins/_Plugin.py
CHANGED
@@ -209,7 +209,7 @@ class Plugin:
|
|
209
209
|
def parse_gitignore() -> 'Set[str]':
|
210
210
|
gitignore_path = pathlib.Path(path) / '.gitignore'
|
211
211
|
if not gitignore_path.exists():
|
212
|
-
return set()
|
212
|
+
return set(default_patterns_to_ignore)
|
213
213
|
with open(gitignore_path, 'r', encoding='utf-8') as f:
|
214
214
|
gitignore_text = f.read()
|
215
215
|
return set(pathspec.PathSpec.from_lines(
|
@@ -15,6 +15,7 @@ import signal
|
|
15
15
|
import sys
|
16
16
|
import time
|
17
17
|
import traceback
|
18
|
+
from functools import partial
|
18
19
|
from datetime import datetime, timezone
|
19
20
|
from meerschaum.utils.typing import Optional, Dict, Any, SuccessTuple, Callable, List, Union
|
20
21
|
from meerschaum.config import get_config
|
@@ -36,7 +37,7 @@ class Daemon:
|
|
36
37
|
def __new__(
|
37
38
|
cls,
|
38
39
|
*args,
|
39
|
-
daemon_id
|
40
|
+
daemon_id: Optional[str] = None,
|
40
41
|
**kw
|
41
42
|
):
|
42
43
|
"""
|
@@ -129,7 +130,7 @@ class Daemon:
|
|
129
130
|
keep_daemon_output: bool, default True
|
130
131
|
If `False`, delete the daemon's output directory upon exiting.
|
131
132
|
|
132
|
-
allow_dirty_run :
|
133
|
+
allow_dirty_run, bool, default False:
|
133
134
|
If `True`, run the daemon, even if the `daemon_id` directory exists.
|
134
135
|
This option is dangerous because if the same `daemon_id` runs twice,
|
135
136
|
the last to finish will overwrite the output of the first.
|
@@ -138,8 +139,13 @@ class Daemon:
|
|
138
139
|
-------
|
139
140
|
Nothing — this will exit the parent process.
|
140
141
|
"""
|
141
|
-
import platform, sys, os
|
142
|
+
import platform, sys, os, traceback
|
143
|
+
from meerschaum.config._paths import DAEMON_ERROR_LOG_PATH
|
144
|
+
from meerschaum.utils.warnings import warn
|
145
|
+
from meerschaum.config import get_config
|
142
146
|
daemon = attempt_import('daemon')
|
147
|
+
lines = get_config('jobs', 'terminal', 'lines')
|
148
|
+
columns = get_config('jobs','terminal', 'columns')
|
143
149
|
|
144
150
|
if platform.system() == 'Windows':
|
145
151
|
return False, "Windows is no longer supported."
|
@@ -160,31 +166,43 @@ class Daemon:
|
|
160
166
|
)
|
161
167
|
|
162
168
|
log_refresh_seconds = get_config('jobs', 'logs', 'refresh_files_seconds')
|
163
|
-
self._log_refresh_timer = RepeatTimer(
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
self.
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
warn(e, stacklevel=3)
|
175
|
-
result = e
|
176
|
-
finally:
|
177
|
-
self._log_refresh_timer.cancel()
|
178
|
-
self.rotating_log.close()
|
179
|
-
if self.pid is None and self.pid_path.exists():
|
180
|
-
self.pid_path.unlink()
|
169
|
+
self._log_refresh_timer = RepeatTimer(
|
170
|
+
log_refresh_seconds,
|
171
|
+
partial(self.rotating_log.refresh_files, start_interception=True),
|
172
|
+
)
|
173
|
+
try:
|
174
|
+
os.environ['LINES'], os.environ['COLUMNS'] = str(int(lines)), str(int(columns))
|
175
|
+
with self._daemon_context:
|
176
|
+
self.rotating_log.refresh_files(start_interception=True)
|
177
|
+
try:
|
178
|
+
with open(self.pid_path, 'w+', encoding='utf-8') as f:
|
179
|
+
f.write(str(os.getpid()))
|
181
180
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
181
|
+
self._log_refresh_timer.start()
|
182
|
+
result = self.target(*self.target_args, **self.target_kw)
|
183
|
+
self.properties['result'] = result
|
184
|
+
except Exception as e:
|
185
|
+
warn(e, stacklevel=3)
|
186
|
+
result = e
|
187
|
+
finally:
|
188
|
+
self._log_refresh_timer.cancel()
|
189
|
+
self.rotating_log.close()
|
190
|
+
if self.pid is None and self.pid_path.exists():
|
191
|
+
self.pid_path.unlink()
|
192
|
+
|
193
|
+
if keep_daemon_output:
|
194
|
+
self._capture_process_timestamp('ended')
|
195
|
+
else:
|
196
|
+
self.cleanup()
|
197
|
+
|
198
|
+
return result
|
199
|
+
except Exception as e:
|
200
|
+
daemon_error = traceback.format_exc()
|
201
|
+
with open(DAEMON_ERROR_LOG_PATH, 'a+', encoding='utf-8') as f:
|
202
|
+
f.write(daemon_error)
|
186
203
|
|
187
|
-
|
204
|
+
if daemon_error:
|
205
|
+
warn(f"Encountered an error while starting the daemon '{self}':\n{daemon_error}")
|
188
206
|
|
189
207
|
|
190
208
|
def _capture_process_timestamp(
|
@@ -453,9 +471,8 @@ class Daemon:
|
|
453
471
|
daemon_context.close()
|
454
472
|
|
455
473
|
_close_pools()
|
456
|
-
|
457
|
-
|
458
|
-
sys.exit(0)
|
474
|
+
self.rotating_log.stop_log_fd_interception()
|
475
|
+
raise KeyboardInterrupt()
|
459
476
|
|
460
477
|
|
461
478
|
def _handle_sigterm(self, signal_number: int, stack_frame: 'frame') -> None:
|
@@ -472,8 +489,7 @@ class Daemon:
|
|
472
489
|
daemon_context.close()
|
473
490
|
|
474
491
|
_close_pools()
|
475
|
-
|
476
|
-
raise SystemExit()
|
492
|
+
raise SystemExit(1)
|
477
493
|
|
478
494
|
|
479
495
|
def _send_signal(
|
@@ -650,7 +666,12 @@ class Daemon:
|
|
650
666
|
if '_rotating_log' in self.__dict__:
|
651
667
|
return self._rotating_log
|
652
668
|
|
653
|
-
self._rotating_log = RotatingFile(
|
669
|
+
self._rotating_log = RotatingFile(
|
670
|
+
self.log_path,
|
671
|
+
redirect_streams = True,
|
672
|
+
write_timestamps = True,
|
673
|
+
timestamp_format = get_config('jobs', 'logs', 'timestamp_format'),
|
674
|
+
)
|
654
675
|
return self._rotating_log
|
655
676
|
|
656
677
|
|
@@ -663,6 +684,7 @@ class Daemon:
|
|
663
684
|
self.rotating_log.file_path,
|
664
685
|
num_files_to_keep = self.rotating_log.num_files_to_keep,
|
665
686
|
max_file_size = self.rotating_log.max_file_size,
|
687
|
+
write_timestamps = True,
|
666
688
|
)
|
667
689
|
return new_rotating_log.read()
|
668
690
|
|
@@ -714,7 +736,7 @@ class Daemon:
|
|
714
736
|
if not self.pid_path.exists():
|
715
737
|
return None
|
716
738
|
try:
|
717
|
-
with open(self.pid_path, 'r') as f:
|
739
|
+
with open(self.pid_path, 'r', encoding='utf-8') as f:
|
718
740
|
text = f.read()
|
719
741
|
pid = int(text.rstrip())
|
720
742
|
except Exception as e:
|
@@ -815,7 +837,7 @@ class Daemon:
|
|
815
837
|
if self.properties is not None:
|
816
838
|
try:
|
817
839
|
self.path.mkdir(parents=True, exist_ok=True)
|
818
|
-
with open(self.properties_path, 'w+') as properties_file:
|
840
|
+
with open(self.properties_path, 'w+', encoding='utf-8') as properties_file:
|
819
841
|
json.dump(self.properties, properties_file)
|
820
842
|
success, msg = True, 'Success'
|
821
843
|
except Exception as e:
|
@@ -887,6 +909,13 @@ class Daemon:
|
|
887
909
|
return False, msg
|
888
910
|
if not keep_logs:
|
889
911
|
self.rotating_log.delete()
|
912
|
+
try:
|
913
|
+
if self.log_offset_path.exists():
|
914
|
+
self.log_offset_path.unlink()
|
915
|
+
except Exception as e:
|
916
|
+
msg = f"Failed to remove offset file for '{self.daemon_id}':\n{e}"
|
917
|
+
warn(msg)
|
918
|
+
return False, msg
|
890
919
|
return True, "Success"
|
891
920
|
|
892
921
|
|
@@ -0,0 +1,102 @@
|
|
1
|
+
#! /usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
# vim:fenc=utf-8
|
4
|
+
|
5
|
+
"""
|
6
|
+
Intercept OS-level file descriptors.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import os
|
10
|
+
import traceback
|
11
|
+
from datetime import datetime
|
12
|
+
from meerschaum.utils.typing import Callable
|
13
|
+
from meerschaum.utils.warnings import warn
|
14
|
+
|
15
|
+
FD_CLOSED: int = 9
|
16
|
+
|
17
|
+
class FileDescriptorInterceptor:
|
18
|
+
"""
|
19
|
+
A management class to intercept data written to a file descriptor.
|
20
|
+
"""
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
file_descriptor: int,
|
24
|
+
injection_hook: Callable[[], str],
|
25
|
+
):
|
26
|
+
"""
|
27
|
+
Parameters
|
28
|
+
----------
|
29
|
+
file_descriptor: int
|
30
|
+
The OS file descriptor from which to read.
|
31
|
+
|
32
|
+
injection_hook: Callable[[], str]
|
33
|
+
A callable which returns a string to be injected into the written data.
|
34
|
+
"""
|
35
|
+
self.injection_hook = injection_hook
|
36
|
+
self.original_file_descriptor = file_descriptor
|
37
|
+
self.new_file_descriptor = os.dup(file_descriptor)
|
38
|
+
self.read_pipe, self.write_pipe = os.pipe()
|
39
|
+
os.dup2(self.write_pipe, file_descriptor)
|
40
|
+
|
41
|
+
def start_interception(self):
|
42
|
+
"""
|
43
|
+
Read from the file descriptor and write the modified data after injection.
|
44
|
+
|
45
|
+
NOTE: This is blocking and is meant to be run in a thread.
|
46
|
+
"""
|
47
|
+
is_first_read = True
|
48
|
+
while True:
|
49
|
+
data = os.read(self.read_pipe, 1024)
|
50
|
+
if not data:
|
51
|
+
break
|
52
|
+
|
53
|
+
first_char_is_newline = data[0] == b'\n'
|
54
|
+
last_char_is_newline = data[-1] == b'\n'
|
55
|
+
|
56
|
+
injected_str = self.injection_hook()
|
57
|
+
injected_bytes = injected_str.encode('utf-8')
|
58
|
+
|
59
|
+
if is_first_read:
|
60
|
+
data = b'\n' + data
|
61
|
+
is_first_read = False
|
62
|
+
|
63
|
+
modified_data = (
|
64
|
+
(data[:-1].replace(b'\n', b'\n' + injected_bytes) + b'\n')
|
65
|
+
if last_char_is_newline
|
66
|
+
else data.replace(b'\n', b'\n' + injected_bytes)
|
67
|
+
)
|
68
|
+
|
69
|
+
os.write(self.new_file_descriptor, modified_data)
|
70
|
+
|
71
|
+
def stop_interception(self):
|
72
|
+
"""
|
73
|
+
Restore the file descriptors and close the new pipes.
|
74
|
+
"""
|
75
|
+
try:
|
76
|
+
os.dup2(self.new_file_descriptor, self.original_file_descriptor)
|
77
|
+
# os.close(self.new_file_descriptor)
|
78
|
+
except OSError as e:
|
79
|
+
if e.errno != FD_CLOSED:
|
80
|
+
warn(
|
81
|
+
f"Error while trying to close the duplicated file descriptor:\n"
|
82
|
+
+ f"{traceback.format_exc()}"
|
83
|
+
)
|
84
|
+
|
85
|
+
try:
|
86
|
+
os.close(self.write_pipe)
|
87
|
+
except OSError as e:
|
88
|
+
if e.errno != FD_CLOSED:
|
89
|
+
warn(
|
90
|
+
f"Error while trying to close the write-pipe "
|
91
|
+
+ "to the intercepted file descriptor:\n"
|
92
|
+
+ f"{traceback.format_exc()}"
|
93
|
+
)
|
94
|
+
try:
|
95
|
+
os.close(self.read_pipe)
|
96
|
+
except OSError as e:
|
97
|
+
if e.errno != FD_CLOSED:
|
98
|
+
warn(
|
99
|
+
f"Error while trying to close the read-pipe "
|
100
|
+
+ "to the intercepted file descriptor:\n"
|
101
|
+
+ f"{traceback.format_exc()}"
|
102
|
+
)
|