Skip to content

Commit fb8a390

Browse files
authored
v2.5.3: 紧急修复域名切换重试机制,优化异常机制和GitHub Actions的异常处理 (#206)
1 parent 684754a commit fb8a390

File tree

11 files changed

+126
-46
lines changed

11 files changed

+126
-46
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ $ jmcomic 422866
6969
- **可扩展性强**
7070

7171
- 支持自定义本子/章节/图片下载前后的回调函数
72-
- 支持自定义日志
7372
- 支持自定义类:`Downloader(负责调度)` `Option(负责配置)` `Client(负责请求)` `实体类`
73+
- 支持自定义日志、异常监听器
7474
- **支持Plugin插件,可以方便地扩展功能,以及使用别人的插件,目前内置插件有**
7575
- `登录插件`
7676
- `硬件占用监控插件`

assets/docs/sources/tutorial/4_module_custom.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,32 @@ def custom_jm_log():
163163

164164
# 2. 让my_log生效
165165
JmModuleConfig.log_executor = my_log
166+
```
167+
168+
169+
170+
## 自定义异常监听器/回调
171+
172+
```python
173+
def custom_exception_listener():
174+
"""
175+
该函数演示jmcomic的异常监听器机制
176+
"""
177+
178+
# 1. 选一个可能会发生的、你感兴趣的异常
179+
etype = ResponseUnexpectedException
180+
181+
182+
def listener(e):
183+
"""
184+
你的监听器方法
185+
该方法无需返回值
186+
:param e: 异常实例
187+
"""
188+
print(f'my exception listener invoke !!! exception happened: {e}')
189+
190+
191+
# 注册监听器/回调
192+
# 这个异常类(或者这个异常的子类)的实例将要被raise前,你的listener方法会被调用
193+
JmModuleConfig.register_exception_listener(etype, listener)
166194
```

assets/docs/sources/tutorial/8_pick_domain.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ print(f'获取到{len(domain_set)}个域名,开始测试')
4545

4646

4747
def test_domain(domain: str):
48-
client = option.new_jm_client(domain_list=[domain], **meta_data)
48+
client = option.new_jm_client(impl='html', domain_list=[domain], **meta_data)
4949
status = 'ok'
5050

5151
try:

assets/option/option_test_html.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ client:
1111
timeout: 7
1212
domain:
1313
html:
14+
- 18comic.org
1415
- jmcomic1.me
1516
- jmcomic.me
1617

src/jmcomic/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# 被依赖方 <--- 使用方
33
# config <--- entity <--- toolkit <--- client <--- option <--- downloader
44

5-
__version__ = '2.5.2'
5+
__version__ = '2.5.3'
66

77
from .api import *
88
from .jm_plugin import *

src/jmcomic/jm_client_impl.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,10 @@ def request_with_retry(self,
7878
:param kwargs: 请求方法的kwargs
7979
"""
8080
if domain_index >= len(self.domain_list):
81-
self.fallback(request, url, domain_index, retry_count, **kwargs)
82-
81+
return self.fallback(request, url, domain_index, retry_count, **kwargs)
82+
83+
url_backup = url
84+
8385
if url.startswith('/'):
8486
# path → url
8587
domain = self.domain_list[domain_index]
@@ -120,9 +122,9 @@ def request_with_retry(self,
120122
self.before_retry(e, kwargs, retry_count, url)
121123

122124
if retry_count < self.retry_times:
123-
return self.request_with_retry(request, url, domain_index, retry_count + 1, callback, **kwargs)
125+
return self.request_with_retry(request, url_backup, domain_index, retry_count + 1, callback, **kwargs)
124126
else:
125-
return self.request_with_retry(request, url, domain_index + 1, 0, callback, **kwargs)
127+
return self.request_with_retry(request, url_backup, domain_index + 1, 0, callback, **kwargs)
126128

127129
# noinspection PyMethodMayBeStatic
128130
def raise_if_resp_should_retry(self, resp):
@@ -209,7 +211,7 @@ def set_domain_list(self, domain_list: List[str]):
209211
def fallback(self, request, url, domain_index, retry_count, **kwargs):
210212
msg = f"请求重试全部失败: [{url}], {self.domain_list}"
211213
jm_log('req.fallback', msg)
212-
ExceptionTool.raises(msg)
214+
ExceptionTool.raises(msg, {}, RequestRetryAllFailException)
213215

214216
# noinspection PyMethodMayBeStatic
215217
def append_params_to_url(self, url, params):

src/jmcomic/jm_config.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ class JmModuleConfig:
117117

118118
# 移动端API域名
119119
DOMAIN_API_LIST = str_to_list('''
120+
www.jmapinode.biz
120121
www.jmapinode1.top
121122
www.jmapinode2.top
122123
www.jmapinode3.top
123-
www.jmapinode.biz
124124
www.jmapinode.top
125125
126126
''')
@@ -144,8 +144,11 @@ class JmModuleConfig:
144144
REGISTRY_CLIENT = {}
145145
# 插件注册表
146146
REGISTRY_PLUGIN = {}
147-
# 异常处理器
148-
REGISTRY_EXCEPTION_ADVICE = {}
147+
# 异常监听器
148+
# key: 异常类
149+
# value: 函数,参数只有异常对象,无需返回值
150+
# 这个异常类(或者这个异常的子类)的实例将要被raise前,你的listener方法会被调用
151+
REGISTRY_EXCEPTION_LISTENER = {}
149152

150153
# 执行log的函数
151154
executor_log = default_jm_logging
@@ -311,7 +314,7 @@ def new_postman(cls, session=False, **kwargs):
311314
# 而如果只想修改几个简单常用的配置,也可以下方的DEFAULT_XXX属性
312315
JM_OPTION_VER = '2.1'
313316
DEFAULT_CLIENT_IMPL = 'api' # 默认Client实现类型为网页端
314-
DEFAULT_CLIENT_CACHE = True # 默认开启Client缓存,缓存级别是level_option,详见CacheRegistry
317+
DEFAULT_CLIENT_CACHE = None # 默认关闭Client缓存。缓存的配置详见 CacheRegistry
315318
DEFAULT_PROXIES = ProxyBuilder.system_proxy() # 默认使用系统代理
316319

317320
default_option_dict: dict = {
@@ -404,8 +407,8 @@ def register_client(cls, client_class):
404407
cls.REGISTRY_CLIENT[client_class.client_key] = client_class
405408

406409
@classmethod
407-
def register_exception_advice(cls, etype, eadvice):
408-
cls.REGISTRY_EXCEPTION_ADVICE[etype] = eadvice
410+
def register_exception_listener(cls, etype, listener):
411+
cls.REGISTRY_EXCEPTION_LISTENER[etype] = listener
409412

410413

411414
jm_log = JmModuleConfig.jm_log

src/jmcomic/jm_exception.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33

44

55
class JmcomicException(Exception):
6-
"""
7-
jmcomic 模块异常
8-
"""
6+
description = 'jmcomic 模块异常'
97

108
def __init__(self, msg: str, context: dict):
119
self.msg = msg
@@ -16,19 +14,22 @@ def from_context(self, key):
1614

1715

1816
class ResponseUnexpectedException(JmcomicException):
19-
"""
20-
响应不符合预期异常
21-
"""
17+
description = '响应不符合预期异常'
2218

2319
@property
2420
def resp(self):
2521
return self.from_context(ExceptionTool.CONTEXT_KEY_RESP)
2622

2723

28-
class RegularNotMatchException(ResponseUnexpectedException):
29-
"""
30-
正则表达式不匹配异常
31-
"""
24+
class RegularNotMatchException(JmcomicException):
25+
description = '正则表达式不匹配异常'
26+
27+
@property
28+
def resp(self):
29+
"""
30+
可能为None
31+
"""
32+
return self.context.get(ExceptionTool.CONTEXT_KEY_RESP, None)
3233

3334
@property
3435
def error_text(self):
@@ -40,19 +41,23 @@ def pattern(self):
4041

4142

4243
class JsonResolveFailException(ResponseUnexpectedException):
44+
description = 'Json解析异常'
4345
pass
4446

4547

4648
class MissingAlbumPhotoException(ResponseUnexpectedException):
47-
"""
48-
缺少本子/章节异常
49-
"""
49+
description = '不存在本子或章节异常'
5050

5151
@property
5252
def error_jmid(self) -> str:
5353
return self.from_context(ExceptionTool.CONTEXT_KEY_MISSING_JM_ID)
5454

5555

56+
class RequestRetryAllFailException(JmcomicException):
57+
description = '请求重试全部失败异常'
58+
pass
59+
60+
5661
class ExceptionTool:
5762
"""
5863
抛异常的工具
@@ -95,10 +100,7 @@ def raises(cls,
95100
e = etype(msg, context)
96101

97102
# 异常处理建议
98-
advice = JmModuleConfig.REGISTRY_EXCEPTION_ADVICE.get(etype, None)
99-
100-
if advice is not None:
101-
advice(e)
103+
cls.notify_all_listeners(e)
102104

103105
raise e
104106

@@ -174,3 +176,13 @@ def new(msg, context=None, _etype=None):
174176
raises(old, msg, context)
175177

176178
cls.raises = new
179+
180+
@classmethod
181+
def notify_all_listeners(cls, e):
182+
registry: Dict[Type, Callable[Type]] = JmModuleConfig.REGISTRY_EXCEPTION_LISTENER
183+
if not registry:
184+
return None
185+
186+
for accept_type, listener in registry.items():
187+
if isinstance(e, accept_type):
188+
listener(e)

src/jmcomic/jm_option.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ def level_client(cls, _option, client):
1717
return registry[client]
1818

1919
@classmethod
20-
def enable_client_cache_on_condition(cls, option: 'JmOption', client: JmcomicClient,
21-
cache: Union[None, bool, str, Callable]):
20+
def enable_client_cache_on_condition(cls,
21+
option: 'JmOption',
22+
client: JmcomicClient,
23+
cache: Union[None, bool, str, Callable],
24+
):
2225
"""
2326
cache parameter
2427
@@ -539,7 +542,7 @@ def invoke_plugin(self, pclass, kwargs: Optional[Dict], extra: dict, pinfo: dict
539542

540543
pclass: Type[JmOptionPlugin]
541544
plugin: Optional[JmOptionPlugin] = None
542-
545+
543546
try:
544547
# 构建插件对象
545548
plugin: JmOptionPlugin = pclass.build(self)

tests/test_jmcomic/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ def setUpClass(cls):
4848
# 设置 JmOption,JmcomicClient
4949
option = cls.new_option()
5050
cls.option = option
51-
cls.client = option.build_jm_client()
51+
# 设置缓存级别为option,可以减少请求次数
52+
cls.client = option.build_jm_client(cache='level_option')
5253

5354
# 跨平台设置
5455
cls.adapt_os()

usage/workflow_download.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,20 +86,50 @@ def log_before_raise():
8686
jm_download_dir = env('JM_DOWNLOAD_DIR', workspace())
8787
mkdir_if_not_exists(jm_download_dir)
8888

89-
# 自定义异常抛出函数,在抛出前把HTML响应数据写到下载文件夹(日志留痕)
90-
def raises(old, msg, extra: dict):
91-
if ExceptionTool.EXTRA_KEY_RESP not in extra:
92-
return old(msg, extra)
89+
def decide_filepath(e):
90+
resp = e.context.get(ExceptionTool.CONTEXT_KEY_RESP, None)
91+
92+
if resp is None:
93+
suffix = str(time_stamp())
94+
else:
95+
suffix = resp.url
96+
97+
name = '-'.join(
98+
fix_windir_name(it)
99+
for it in [
100+
e.description,
101+
current_thread().name,
102+
suffix
103+
]
104+
)
105+
106+
path = f'{jm_download_dir}/【出错了】{name}.log'
107+
return path
108+
109+
def exception_listener(e: JmcomicException):
110+
"""
111+
异常监听器,实现了在 GitHub Actions 下,把请求错误的信息下载到文件,方便调试和通知使用者
112+
"""
113+
# 决定要写入的文件路径
114+
path = decide_filepath(e)
115+
116+
# 准备内容
117+
content = [
118+
str(type(e)),
119+
e.msg,
120+
]
121+
for k, v in e.context.items():
122+
content.append(f'{k}: {v}')
123+
124+
# resp.text
125+
resp = e.context.get(ExceptionTool.CONTEXT_KEY_RESP, None)
126+
if resp:
127+
content.append(f'响应文本: {resp.text}')
93128

94-
resp = extra[ExceptionTool.EXTRA_KEY_RESP]
95129
# 写文件
96-
from common import write_text, fix_windir_name
97-
write_text(f'{jm_download_dir}/{fix_windir_name(resp.url)}', resp.text)
130+
write_text(path, '\n'.join(content))
98131

99-
return old(msg, extra)
100-
101-
# 应用函数
102-
ExceptionTool.replace_old_exception_executor(raises)
132+
JmModuleConfig.register_exception_listener(JmcomicException, exception_listener)
103133

104134

105135
if __name__ == '__main__':

0 commit comments

Comments
 (0)