Alaea 2.6.0__tar.gz → 2.6.2__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 (62) hide show
  1. alaea-2.6.2/.VERSION +1 -0
  2. {alaea-2.6.0 → alaea-2.6.2}/Alaea/Blogging.py +86 -54
  3. {alaea-2.6.0 → alaea-2.6.2}/Alaea/storage.py +1 -1
  4. {alaea-2.6.0 → alaea-2.6.2}/Alaea.egg-info/PKG-INFO +1 -1
  5. {alaea-2.6.0 → alaea-2.6.2}/ChangeLog.md +8 -0
  6. {alaea-2.6.0 → alaea-2.6.2}/PKG-INFO +1 -1
  7. {alaea-2.6.0 → alaea-2.6.2}/setup.py +1 -1
  8. {alaea-2.6.0 → alaea-2.6.2}/tests/test_core.py +4 -1
  9. {alaea-2.6.0 → alaea-2.6.2}/tests/test_logging_multiprocess.py +32 -13
  10. alaea-2.6.0/.VERSION +0 -1
  11. {alaea-2.6.0 → alaea-2.6.2}/Alaea/Pyshell/RS_File.py +0 -0
  12. {alaea-2.6.0 → alaea-2.6.2}/Alaea/Pyshell/__init__.py +0 -0
  13. {alaea-2.6.0 → alaea-2.6.2}/Alaea/S_DateTime.py +0 -0
  14. {alaea-2.6.0 → alaea-2.6.2}/Alaea/SameAsMATLAB/CoorTrans.py +0 -0
  15. {alaea-2.6.0 → alaea-2.6.2}/Alaea/SameAsMATLAB/__init__.py +0 -0
  16. {alaea-2.6.0 → alaea-2.6.2}/Alaea/SameAsMATLAB/calculatefuns.py +0 -0
  17. {alaea-2.6.0 → alaea-2.6.2}/Alaea/Standard/__init__.py +0 -0
  18. {alaea-2.6.0 → alaea-2.6.2}/Alaea/Standard/vars_name.py +0 -0
  19. {alaea-2.6.0 → alaea-2.6.2}/Alaea/Standard/xr_atmos.py +0 -0
  20. {alaea-2.6.0 → alaea-2.6.2}/Alaea/Standard/xr_attrs.py +0 -0
  21. {alaea-2.6.0 → alaea-2.6.2}/Alaea/Standard/xr_ice.py +0 -0
  22. {alaea-2.6.0 → alaea-2.6.2}/Alaea/Standard/xr_netcdf.py +0 -0
  23. {alaea-2.6.0 → alaea-2.6.2}/Alaea/Standard/xr_vars.py +0 -0
  24. {alaea-2.6.0 → alaea-2.6.2}/Alaea/Standard/xr_wave.py +0 -0
  25. {alaea-2.6.0 → alaea-2.6.2}/Alaea/Standard/xr_wind.py +0 -0
  26. {alaea-2.6.0 → alaea-2.6.2}/Alaea/__init__.py +0 -0
  27. {alaea-2.6.0 → alaea-2.6.2}/Alaea/baysalt/Forage/__init__.py +0 -0
  28. {alaea-2.6.0 → alaea-2.6.2}/Alaea/baysalt/Forage/erosion_data/__init__.py +0 -0
  29. {alaea-2.6.0 → alaea-2.6.2}/Alaea/baysalt/__init__.py +0 -0
  30. {alaea-2.6.0 → alaea-2.6.2}/Alaea/baysalt/erosion_coastline.py +0 -0
  31. {alaea-2.6.0 → alaea-2.6.2}/Alaea/commonCode.py +0 -0
  32. {alaea-2.6.0 → alaea-2.6.2}/Alaea/cprintfs.py +0 -0
  33. {alaea-2.6.0 → alaea-2.6.2}/Alaea/exutils/Interp/Interp_g2s.py +0 -0
  34. {alaea-2.6.0 → alaea-2.6.2}/Alaea/exutils/Interp/Interp_s2g.py +0 -0
  35. {alaea-2.6.0 → alaea-2.6.2}/Alaea/exutils/Interp/__init__.py +0 -0
  36. {alaea-2.6.0 → alaea-2.6.2}/Alaea/exutils/Interp/interpolate.py +0 -0
  37. {alaea-2.6.0 → alaea-2.6.2}/Alaea/exutils/__init__.py +0 -0
  38. {alaea-2.6.0 → alaea-2.6.2}/Alaea/pFVCOM/__init__.py +0 -0
  39. {alaea-2.6.0 → alaea-2.6.2}/Alaea/pFVCOM/fvcom_class.py +0 -0
  40. {alaea-2.6.0 → alaea-2.6.2}/Alaea/pFVCOM/fvcom_image.py +0 -0
  41. {alaea-2.6.0 → alaea-2.6.2}/Alaea/pFVCOM/fvcom_load.py +0 -0
  42. {alaea-2.6.0 → alaea-2.6.2}/Alaea/pFVCOM/fvcom_tools.py +0 -0
  43. {alaea-2.6.0 → alaea-2.6.2}/Alaea/pFVCOM/writeFile.py +0 -0
  44. {alaea-2.6.0 → alaea-2.6.2}/Alaea/processBar.py +0 -0
  45. {alaea-2.6.0 → alaea-2.6.2}/Alaea/read_conf.py +0 -0
  46. {alaea-2.6.0 → alaea-2.6.2}/Alaea/server_info.py +0 -0
  47. {alaea-2.6.0 → alaea-2.6.2}/Alaea/tools/P_params.py +0 -0
  48. {alaea-2.6.0 → alaea-2.6.2}/Alaea/tools/Read.py +0 -0
  49. {alaea-2.6.0 → alaea-2.6.2}/Alaea/tools/__init__.py +0 -0
  50. {alaea-2.6.0 → alaea-2.6.2}/Alaea/tools/download_check.py +0 -0
  51. {alaea-2.6.0 → alaea-2.6.2}/Alaea/tools/geo_distance.py +0 -0
  52. {alaea-2.6.0 → alaea-2.6.2}/Alaea/tools/kml2shp.py +0 -0
  53. {alaea-2.6.0 → alaea-2.6.2}/Alaea/warningTools/__init__.py +0 -0
  54. {alaea-2.6.0 → alaea-2.6.2}/Alaea.egg-info/SOURCES.txt +0 -0
  55. {alaea-2.6.0 → alaea-2.6.2}/Alaea.egg-info/dependency_links.txt +0 -0
  56. {alaea-2.6.0 → alaea-2.6.2}/Alaea.egg-info/requires.txt +0 -0
  57. {alaea-2.6.0 → alaea-2.6.2}/Alaea.egg-info/top_level.txt +0 -0
  58. {alaea-2.6.0 → alaea-2.6.2}/LICENSE +0 -0
  59. {alaea-2.6.0 → alaea-2.6.2}/MANIFEST.in +0 -0
  60. {alaea-2.6.0 → alaea-2.6.2}/README.md +0 -0
  61. {alaea-2.6.0 → alaea-2.6.2}/pyproject.toml +0 -0
  62. {alaea-2.6.0 → alaea-2.6.2}/setup.cfg +0 -0
alaea-2.6.2/.VERSION ADDED
@@ -0,0 +1 @@
1
+ v2.6.2
@@ -12,8 +12,8 @@
12
12
  from __future__ import annotations
13
13
 
14
14
  import datetime
15
- import errno
16
15
  import gzip
16
+ import hashlib
17
17
  import logging
18
18
  import os
19
19
  import shutil
@@ -29,10 +29,21 @@ try:
29
29
  except ImportError:
30
30
  fcntl = None
31
31
 
32
- try:
33
- import msvcrt
34
- except ImportError:
35
- msvcrt = None
32
+ if os.name == 'nt':
33
+ import ctypes
34
+ from ctypes import wintypes
35
+
36
+ _windows_kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
37
+ _windows_kernel32.CreateMutexW.argtypes = [wintypes.LPVOID, wintypes.BOOL, wintypes.LPCWSTR]
38
+ _windows_kernel32.CreateMutexW.restype = wintypes.HANDLE
39
+ _windows_kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
40
+ _windows_kernel32.WaitForSingleObject.restype = wintypes.DWORD
41
+ _windows_kernel32.ReleaseMutex.argtypes = [wintypes.HANDLE]
42
+ _windows_kernel32.ReleaseMutex.restype = wintypes.BOOL
43
+ _windows_kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
44
+ _windows_kernel32.CloseHandle.restype = wintypes.BOOL
45
+ else:
46
+ _windows_kernel32 = None
36
47
 
37
48
  try:
38
49
  import zstandard
@@ -70,7 +81,11 @@ def _local_converter(timestamp):
70
81
 
71
82
 
72
83
  class _ProcessSafeRotatingMixin:
73
- """为每个日志路径使用一个协作锁,在多进程之间串行执行轮转。"""
84
+ """通过日志目录锁在多进程之间串行执行写入和轮转。"""
85
+
86
+ _WAIT_OBJECT_0 = 0
87
+ _WAIT_ABANDONED = 0x80
88
+ _INFINITE = 0xFFFFFFFF
74
89
 
75
90
  def __init__(self, *args, **kwargs):
76
91
  """初始化 handler,并避免 Windows 长期占用当前日志文件。"""
@@ -79,40 +94,65 @@ class _ProcessSafeRotatingMixin:
79
94
 
80
95
  def _close_windows_stream(self):
81
96
  """在 Windows 上关闭日志流,使其他进程能够重命名当前日志文件。"""
82
- if msvcrt is not None and self.stream is not None:
97
+ if _windows_kernel32 is not None and self.stream is not None:
83
98
  self.stream.close()
84
99
  self.stream = None
85
100
 
86
- @staticmethod
87
- def _acquire_process_lock(lock_stream):
88
- """使用当前平台标准库取得跨进程排他锁。"""
101
+ def _acquire_process_lock(self):
102
+ """锁定日志目录,返回当前平台用于释放锁的标记和句柄。
103
+
104
+ Returns:
105
+ ``('fcntl', fd)``、``('windows', handle)``,不支持时返回 ``None``。
106
+
107
+ Raises:
108
+ OSError: 无法打开日志目录或取得系统锁时抛出。
109
+ """
89
110
  if fcntl is not None:
90
- fcntl.flock(lock_stream.fileno(), fcntl.LOCK_EX)
91
- return True
92
- if msvcrt is None:
93
- return False
94
- lock_stream.seek(0, os.SEEK_END)
95
- if lock_stream.tell() == 0:
96
- lock_stream.write(b'\0')
97
- lock_stream.flush()
98
- while True:
99
- lock_stream.seek(0)
111
+ directory_fd = os.open(os.path.dirname(self.baseFilename), os.O_RDONLY | getattr(os, 'O_DIRECTORY', 0))
100
112
  try:
101
- msvcrt.locking(lock_stream.fileno(), msvcrt.LK_NBLCK, 1)
102
- return True
103
- except OSError as error:
104
- if error.errno not in {errno.EACCES, errno.EDEADLK}:
105
- raise
106
- time.sleep(0.05)
113
+ fcntl.flock(directory_fd, fcntl.LOCK_EX)
114
+ except OSError:
115
+ os.close(directory_fd)
116
+ raise
117
+ return 'fcntl', directory_fd
118
+ if _windows_kernel32 is None:
119
+ return None
120
+ directory = os.path.normcase(os.path.realpath(os.path.dirname(self.baseFilename)))
121
+ digest = hashlib.sha256(directory.encode('utf-8', errors='surrogatepass')).hexdigest()
122
+ handle = _windows_kernel32.CreateMutexW(None, False, f'Global\\AlaeaLogRotation-{digest}')
123
+ if not handle:
124
+ raise OSError('无法创建 Windows 日志目录互斥量')
125
+ result = _windows_kernel32.WaitForSingleObject(handle, self._INFINITE)
126
+ if result not in {self._WAIT_OBJECT_0, self._WAIT_ABANDONED}:
127
+ _windows_kernel32.CloseHandle(handle)
128
+ raise OSError(f'无法取得 Windows 日志目录互斥量: {result}')
129
+ return 'windows', handle
107
130
 
108
131
  @staticmethod
109
- def _release_process_lock(lock_stream):
110
- """使用当前平台标准库释放跨进程排他锁。"""
111
- if fcntl is not None:
112
- fcntl.flock(lock_stream.fileno(), fcntl.LOCK_UN)
113
- elif msvcrt is not None:
114
- lock_stream.seek(0)
115
- msvcrt.locking(lock_stream.fileno(), msvcrt.LK_UNLCK, 1)
132
+ def _release_process_lock(lock_token):
133
+ """释放并关闭当前平台的日志目录锁。
134
+
135
+ Args:
136
+ lock_token: ``_acquire_process_lock`` 返回的平台标记和句柄。
137
+
138
+ Returns:
139
+ None。
140
+
141
+ Raises:
142
+ OSError: 系统拒绝释放锁时抛出。
143
+ """
144
+ lock_kind, handle = lock_token
145
+ if lock_kind == 'fcntl':
146
+ try:
147
+ fcntl.flock(handle, fcntl.LOCK_UN)
148
+ finally:
149
+ os.close(handle)
150
+ return
151
+ try:
152
+ if not _windows_kernel32.ReleaseMutex(handle):
153
+ raise OSError('无法释放 Windows 日志目录互斥量')
154
+ finally:
155
+ _windows_kernel32.CloseHandle(handle)
116
156
 
117
157
  def _reopen_replaced_stream(self):
118
158
  """其他进程替换当前日志文件后,重新打开日志流。
@@ -171,37 +211,29 @@ class _ProcessSafeRotatingMixin:
171
211
  Raises:
172
212
  不额外抛出异常;当前平台没有文件锁或锁不可用时回退到标准 handler 行为。
173
213
  """
174
- if fcntl is None and msvcrt is None:
214
+ if fcntl is None and _windows_kernel32 is None:
175
215
  super().emit(record)
176
216
  return
177
217
  self._close_windows_stream()
178
218
  try:
179
- lock_stream = open(f'{self.baseFilename}.rotate.lock', 'a+b')
219
+ lock_token = self._acquire_process_lock()
180
220
  except OSError:
181
221
  super().emit(record)
182
222
  self._close_windows_stream()
183
223
  return
184
- with lock_stream:
224
+ if lock_token is None:
225
+ super().emit(record)
226
+ return
227
+ try:
228
+ self._reopen_replaced_stream()
229
+ self._synchronize_timed_rollover()
230
+ super().emit(record)
231
+ finally:
232
+ self._close_windows_stream()
185
233
  try:
186
- acquired = self._acquire_process_lock(lock_stream)
234
+ self._release_process_lock(lock_token)
187
235
  except OSError:
188
- super().emit(record)
189
- self._close_windows_stream()
190
- return
191
- if not acquired:
192
- super().emit(record)
193
- self._close_windows_stream()
194
- return
195
- try:
196
- self._reopen_replaced_stream()
197
- self._synchronize_timed_rollover()
198
- super().emit(record)
199
- finally:
200
- self._close_windows_stream()
201
- try:
202
- self._release_process_lock(lock_stream)
203
- except OSError:
204
- pass
236
+ pass
205
237
 
206
238
 
207
239
  class _ProcessSafeRotatingFileHandler(_ProcessSafeRotatingMixin, RotatingFileHandler):
@@ -43,7 +43,7 @@ def run_storage_fallback(storage_mode, auto_methods, operations, fallback_errors
43
43
  try:
44
44
  result = normalized_operations[method]()
45
45
  if logger is not None:
46
- logger.info('Storage method succeeded: context=%s, method=%s, wallSeconds=%.2f', context, method, perf_counter() - started_at)
46
+ logger.debug('Storage method succeeded: context=%s, method=%s, wallSeconds=%.2f', context, method, perf_counter() - started_at)
47
47
  return result
48
48
  except fallback_errors as error:
49
49
  if active_mode != 'AUTO':
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Alaea
3
- Version: 2.6.0
3
+ Version: 2.6.2
4
4
  Summary: Some simple tools by Christmas
5
5
  Author: Christmas
6
6
  Author-email: 273519355@qq.com
@@ -1,5 +1,13 @@
1
1
  # Change Log
2
2
 
3
+ ## v2.6.2
4
+
5
+ - `run_storage_fallback` 的成功线路明细由 INFO 调整为 DEBUG,减少 `Storage method succeeded` 高频常规日志;实际失败并降级仍保持 WARNING。
6
+
7
+ ## v2.6.1
8
+
9
+ - macOS/Linux 使用 `fcntl` 锁定日志目录,Windows 使用由日志目录确定的系统命名互斥量,让不同程序的按时间、按大小及组合日志轮转无额外锁文件地跨进程串行;多个进程在同一时刻压缩共享日志时只轮转一次,Windows 写入后会关闭日志句柄以允许其他进程重命名文件,其他 handler 会同步当前日志和下次轮转时间,避免 `.gz.tmp`/`.zst.tmp` 竞争、重复归档和轮转后日志丢失。
10
+
3
11
  ## v2.6.0
4
12
 
5
13
  - macOS/Linux 使用 `fcntl`、Windows 使用 `msvcrt`,让按时间、按大小及组合日志轮转通过同路径协作式文件锁跨进程串行;多个进程在同一时刻压缩共享日志时只轮转一次,Windows 写入后会关闭日志句柄以允许其他进程重命名文件,其他 handler 会同步当前日志和下次轮转时间,避免 `.gz.tmp`/`.zst.tmp` 竞争、重复归档和轮转后日志丢失。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Alaea
3
- Version: 2.6.0
3
+ Version: 2.6.2
4
4
  Summary: Some simple tools by Christmas
5
5
  Author: Christmas
6
6
  Author-email: 273519355@qq.com
@@ -8,7 +8,7 @@ with open("README.md", "r") as fh:
8
8
 
9
9
  setuptools.setup(
10
10
  name="Alaea",
11
- version="2.6.0",
11
+ version="2.6.2",
12
12
  author="Christmas",
13
13
  author_email="273519355@qq.com",
14
14
  description="Some simple tools by Christmas",
@@ -345,8 +345,11 @@ class UtilityRegressionTest(unittest.TestCase):
345
345
  return 'nfs-result'
346
346
 
347
347
  operations = {'CACHE': unavailable, 'NFS': direct}
348
- self.assertEqual(run_storage_fallback('AUTO', ('CACHE', 'NFS'), operations), 'nfs-result')
348
+ logger = mock.Mock()
349
+ self.assertEqual(run_storage_fallback('AUTO', ('CACHE', 'NFS'), operations, logger=logger, context='down-gfs-cache'), 'nfs-result')
349
350
  self.assertEqual(calls, ['CACHE', 'NFS'])
351
+ logger.debug.assert_called_once_with('Storage method succeeded: context=%s, method=%s, wallSeconds=%.2f', 'down-gfs-cache', 'NFS', mock.ANY)
352
+ logger.info.assert_not_called()
350
353
  with self.assertRaisesRegex(OSError, 'cache unavailable'):
351
354
  run_storage_fallback('CACHE', ('CACHE', 'NFS'), operations)
352
355
 
@@ -13,17 +13,34 @@ from Alaea.Blogging import Blog
13
13
  blogging = importlib.import_module('Alaea.Blogging')
14
14
 
15
15
 
16
- class FakeMsvcrt:
17
- """在非 Windows 测试环境中记录 msvcrt 锁调用。"""
16
+ class FakeKernel32:
17
+ """在非 Windows 测试环境中记录命名互斥量调用。"""
18
18
 
19
- LK_NBLCK = 1
20
- LK_UNLCK = 2
21
19
  calls = []
22
20
 
23
21
  @classmethod
24
- def locking(cls, _file_descriptor, mode, byte_count):
25
- """记录锁模式和锁定字节数。"""
26
- cls.calls.append((mode, byte_count))
22
+ def CreateMutexW(cls, _attributes, _initial_owner, name):
23
+ """记录互斥量名称并返回测试句柄。"""
24
+ cls.calls.append(('create', name))
25
+ return 42
26
+
27
+ @classmethod
28
+ def WaitForSingleObject(cls, handle, timeout):
29
+ """记录等待调用并模拟成功取得互斥量。"""
30
+ cls.calls.append(('wait', handle, timeout))
31
+ return 0
32
+
33
+ @classmethod
34
+ def ReleaseMutex(cls, handle):
35
+ """记录释放调用并模拟成功。"""
36
+ cls.calls.append(('release', handle))
37
+ return True
38
+
39
+ @classmethod
40
+ def CloseHandle(cls, handle):
41
+ """记录句柄关闭调用并模拟成功。"""
42
+ cls.calls.append(('close', handle))
43
+ return True
27
44
 
28
45
 
29
46
  def emit_after_barrier(log_path: str, rollover_at: int, barrier, message: str) -> None:
@@ -89,11 +106,12 @@ def main() -> None:
89
106
  with gzip.open(archives[0], 'rt', encoding='utf-8') as archive:
90
107
  archived_log = archive.read()
91
108
  assert 'before' in archived_log and 'warmup-first' in archived_log and 'warmup-second' in archived_log
109
+ assert not list(log_path.parent.glob('*.rotate.lock'))
92
110
 
93
- original_fcntl, original_msvcrt = blogging.fcntl, blogging.msvcrt
111
+ original_fcntl, original_kernel32 = blogging.fcntl, blogging._windows_kernel32
94
112
  try:
95
- blogging.fcntl, blogging.msvcrt = None, FakeMsvcrt
96
- FakeMsvcrt.calls = []
113
+ blogging.fcntl, blogging._windows_kernel32 = None, FakeKernel32
114
+ FakeKernel32.calls = []
97
115
  with tempfile.TemporaryDirectory() as directory:
98
116
  log_path = Path(directory) / 'WINDOWS.log'
99
117
  blog = Blog(_loger_name='windows-lock-test', _switch_write_bug_log=True, _switch_print_log=False)
@@ -101,11 +119,12 @@ def main() -> None:
101
119
  handler = next(item for item in blog.logger.handlers if isinstance(item, RotatingFileHandler))
102
120
  blog.logger.error('windows-lock')
103
121
  assert handler.stream is None
104
- assert FakeMsvcrt.calls == [(FakeMsvcrt.LK_NBLCK, 1), (FakeMsvcrt.LK_UNLCK, 1)]
105
- assert log_path.with_name(f'{log_path.name}.rotate.lock').read_bytes()
122
+ assert [call[0] for call in FakeKernel32.calls] == ['create', 'wait', 'release', 'close']
123
+ assert FakeKernel32.calls[0][1].startswith('Global\\AlaeaLogRotation-')
124
+ assert not list(log_path.parent.glob('*.rotate.lock'))
106
125
  blog.setup_Blog(switch_write_error_plus_log=False)
107
126
  finally:
108
- blogging.fcntl, blogging.msvcrt = original_fcntl, original_msvcrt
127
+ blogging.fcntl, blogging._windows_kernel32 = original_fcntl, original_kernel32
109
128
 
110
129
 
111
130
  if __name__ == '__main__':
alaea-2.6.0/.VERSION DELETED
@@ -1 +0,0 @@
1
- v2.6.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes