ErisPulse 2.10.2__tar.gz → 2.11.0.dev0__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.
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/PKG-INFO +1 -1
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/pyproject.toml +1 -1
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/init.py +11 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/run.py +41 -22
- erispulse-2.11.0.dev0/src/ErisPulse/CLI/commands/runtime.py +153 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/i18n/locales/en.py +11 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/i18n/locales/ja.py +11 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/i18n/locales/ru.py +11 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +11 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +11 -0
- erispulse-2.11.0.dev0/src/ErisPulse/CLI/utils/runtime_state.py +208 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/__init__.py +2 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/base.py +8 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/command.py +17 -1
- erispulse-2.11.0.dev0/src/ErisPulse/Core/Event/command_flow.py +348 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/wrapper.py +5 -1
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/adapter.py +49 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/config.py +44 -6
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/constants.py +41 -4
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/i18n/locales/en.py +25 -11
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/i18n/locales/ja.py +25 -11
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/i18n/locales/ru.py +25 -11
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/i18n/locales/zh_cn.py +25 -11
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/i18n/locales/zh_tw.py +25 -11
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/lifecycle.py +2 -1
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/module.py +444 -23
- erispulse-2.11.0.dev0/src/ErisPulse/loaders/adapter.py +907 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/loaders/module.py +378 -37
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/sdk_initializer.py +4 -2
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/sdk.py +102 -213
- erispulse-2.10.2/src/ErisPulse/loaders/adapter.py +0 -434
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/.gitignore +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/LICENSE +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/README.pypi.md +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/base.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/cli.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/config.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/create.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/doctor.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/install.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/language.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/list.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/self_update.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/types.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/commands/version.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/console.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/constants.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/hints.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/registry.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/utils/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/utils/config_wizard.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/utils/display.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/utils/interactive.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/utils/package_manager.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/CLI/utils/scaffold_text.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/adapter.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/api_dsl.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/client.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/config_schema.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/converter.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/errors.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/i18n_schema.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/kv_builder.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/manager.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/model.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/module.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/router.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/send_rules.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/sql_base.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/storage.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Bases/websocket.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/command_args.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/conversation.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/governance.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/interaction.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/message.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/message_builder.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/meta.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/notice.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/overrides.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/request.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/session_type.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/throttle.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/Event/trace.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/assets/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/assets/error.css +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/assets/error.html +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/assets/root.css +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/assets/root.html +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/client.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/connections.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/di.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/i18n/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/i18n/constants.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/logger.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/master.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/ownership.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/router.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/scope.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/shadow.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/storage/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/storage/mysql.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/storage/postgres.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/storage/sqlite.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/text_match.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/Core/transcript.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/__main__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/finders/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/finders/adapter.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/finders/bases/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/finders/bases/finder.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/finders/module.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/loaders/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/loaders/bases/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/loaders/bases/loader.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/loaders/plugin_folder.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/loaders/strategy.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/loaders/strict.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/py.typed +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/__init__.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/config_schema.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/context.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/diagnostics.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/example_config.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/exceptions.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/file_watcher.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/frame_config.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/hints.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/memory.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/owner_cleanup.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/plugin_reload.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/proactive_gc.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/tasks.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/troubleshoot.py +0 -0
- {erispulse-2.10.2 → erispulse-2.11.0.dev0}/src/ErisPulse/runtime/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ErisPulse
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.11.0-dev.0
|
|
4
4
|
Summary: Event-driven modular async bot framework — write once, deploy to any platform | 事件驱动的模块化异步机器人框架,一次编写,多平台部署
|
|
5
5
|
Project-URL: Homepage, https://github.com/ErisPulse/ErisPulse
|
|
6
6
|
Project-URL: Documentation, https://www.erisdev.com#docs
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ErisPulse"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.11.0-dev.0"
|
|
8
8
|
description = "Event-driven modular async bot framework — write once, deploy to any platform | 事件驱动的模块化异步机器人框架,一次编写,多平台部署"
|
|
9
9
|
readme = "README.pypi.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -231,6 +231,16 @@ class InitCommand(Command):
|
|
|
231
231
|
with config_file.open("w", encoding="utf-8") as f:
|
|
232
232
|
f.write(st.initial_config_toml())
|
|
233
233
|
|
|
234
|
+
# 配置目录自带 .gitignore:配置含令牌等敏感信息,全部文件不入库
|
|
235
|
+
# (规则仅一行 *——忽略文件自身也在排除范围内)
|
|
236
|
+
config_gitignore = project_path / "config" / ".gitignore"
|
|
237
|
+
if not config_gitignore.exists():
|
|
238
|
+
with config_gitignore.open("w", encoding="utf-8") as f:
|
|
239
|
+
f.write(
|
|
240
|
+
"# ErisPulse: sensitive config — keep out of VCS / 配置含敏感信息,全部不入库\n"
|
|
241
|
+
"*\n"
|
|
242
|
+
)
|
|
243
|
+
|
|
234
244
|
example_file = project_path / "config" / "config.full.example"
|
|
235
245
|
if not example_file.exists():
|
|
236
246
|
with example_file.open("w", encoding="utf-8") as f:
|
|
@@ -295,6 +305,7 @@ class InitCommand(Command):
|
|
|
295
305
|
f"{st.t('init.gitignore_section_runtime')}\n"
|
|
296
306
|
"config/\n"
|
|
297
307
|
"logs/\n"
|
|
308
|
+
".erispulse/\n"
|
|
298
309
|
"\n"
|
|
299
310
|
f"{st.t('init.gitignore_section_editor')}\n"
|
|
300
311
|
".idea/\n"
|
|
@@ -32,6 +32,7 @@ from ..constants import (
|
|
|
32
32
|
RUN_RESTART_PAUSE_SECS,
|
|
33
33
|
)
|
|
34
34
|
from ..i18n import i18n
|
|
35
|
+
from ..utils import runtime_state
|
|
35
36
|
from ..utils.package_manager import (
|
|
36
37
|
resolve_target_python,
|
|
37
38
|
warn_if_uv_isolated,
|
|
@@ -310,29 +311,35 @@ class RunCommand(Command):
|
|
|
310
311
|
"""
|
|
311
312
|
|
|
312
313
|
if reload_mode:
|
|
314
|
+
# 运行状态登记(epsdk stop / restart 的定位依据);内联模式监督者 = 当前进程
|
|
315
|
+
runtime_state.write(os.getpid(), argv=sys.argv)
|
|
316
|
+
try:
|
|
313
317
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
318
|
+
async def _run():
|
|
319
|
+
"""
|
|
320
|
+
设置文件监控并运行 SDK
|
|
317
321
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
322
|
+
:return: [None] 无返回值
|
|
323
|
+
"""
|
|
324
|
+
from ... import sdk
|
|
321
325
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
326
|
+
loop = asyncio.get_running_loop()
|
|
327
|
+
self._setup_watchdog(".", loop)
|
|
328
|
+
await sdk.run(keep_running=True)
|
|
325
329
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
+
try:
|
|
331
|
+
asyncio.run(_run())
|
|
332
|
+
except KeyboardInterrupt:
|
|
333
|
+
pass
|
|
330
334
|
finally:
|
|
331
335
|
if hasattr(self, "_observer"):
|
|
332
336
|
self._observer.stop()
|
|
333
337
|
self._observer.join()
|
|
338
|
+
runtime_state.clear()
|
|
334
339
|
return
|
|
335
340
|
|
|
341
|
+
runtime_state.write(os.getpid(), argv=sys.argv)
|
|
342
|
+
|
|
336
343
|
cmd = [
|
|
337
344
|
getattr(self, "python_exe", sys.executable),
|
|
338
345
|
"-c",
|
|
@@ -354,6 +361,8 @@ class RunCommand(Command):
|
|
|
354
361
|
child_env = dict(os.environ)
|
|
355
362
|
child_env[ENV_SUPERVISED] = "cli"
|
|
356
363
|
process = subprocess.Popen(cmd, env=child_env)
|
|
364
|
+
# 每次拉起新子进程都更新状态文件(stop 先断子进程再停监督者)
|
|
365
|
+
runtime_state.update_child(process.pid)
|
|
357
366
|
process.wait()
|
|
358
367
|
|
|
359
368
|
if process.returncode == self._RESTART_EXIT_CODE:
|
|
@@ -406,6 +415,7 @@ class RunCommand(Command):
|
|
|
406
415
|
except subprocess.TimeoutExpired:
|
|
407
416
|
process.kill()
|
|
408
417
|
process.wait()
|
|
418
|
+
runtime_state.clear()
|
|
409
419
|
|
|
410
420
|
def _run_script(self, script_path: str, reload_mode: bool):
|
|
411
421
|
"""
|
|
@@ -417,17 +427,25 @@ class RunCommand(Command):
|
|
|
417
427
|
script_path_abs = str(Path(script_path).resolve())
|
|
418
428
|
|
|
419
429
|
if reload_mode:
|
|
420
|
-
|
|
430
|
+
runtime_state.write(os.getpid(), argv=sys.argv)
|
|
431
|
+
try:
|
|
432
|
+
self._run_script_with_reload(script_path_abs)
|
|
433
|
+
finally:
|
|
434
|
+
runtime_state.clear()
|
|
421
435
|
else:
|
|
422
|
-
|
|
423
|
-
if script_dir not in sys.path:
|
|
424
|
-
sys.path.insert(0, script_dir)
|
|
436
|
+
runtime_state.write(os.getpid(), argv=sys.argv)
|
|
425
437
|
try:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
438
|
+
script_dir = str(Path(script_path_abs).parent)
|
|
439
|
+
if script_dir not in sys.path:
|
|
440
|
+
sys.path.insert(0, script_dir)
|
|
441
|
+
try:
|
|
442
|
+
runpy.run_path(script_path_abs, run_name="__main__")
|
|
443
|
+
except SystemExit:
|
|
444
|
+
pass
|
|
445
|
+
except KeyboardInterrupt:
|
|
446
|
+
pass
|
|
447
|
+
finally:
|
|
448
|
+
runtime_state.clear()
|
|
431
449
|
|
|
432
450
|
def _run_script_with_reload(self, script_path_abs: str):
|
|
433
451
|
"""
|
|
@@ -453,6 +471,7 @@ class RunCommand(Command):
|
|
|
453
471
|
reload_state["process"] = subprocess.Popen(
|
|
454
472
|
[getattr(self, "python_exe", sys.executable), script_path_abs]
|
|
455
473
|
)
|
|
474
|
+
runtime_state.update_child(reload_state["process"].pid)
|
|
456
475
|
|
|
457
476
|
class _ScriptReloadHandler(FileSystemEventHandler):
|
|
458
477
|
"""
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stop / Restart 命令实现
|
|
3
|
+
|
|
4
|
+
控制由 ``epsdk run`` 启动的运行实例:``.erispulse/runtime.json`` 状态文件
|
|
5
|
+
记录监督进程 PID、当前子进程 PID 与原始启动命令,``stop`` 优雅终止实例,
|
|
6
|
+
``restart`` 停止后按原参数重新启动。
|
|
7
|
+
|
|
8
|
+
POSIX 停止走 SIGINT(监督进程按 Ctrl+C 同款路径收尾,finally 链终止子进程),
|
|
9
|
+
超时逐级升级 SIGTERM / SIGKILL;Windows 无优雅信号语义,先断子进程再终止
|
|
10
|
+
监督进程(避免监督者把强杀误判为崩溃重新拉起)。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from argparse import ArgumentParser, Namespace
|
|
16
|
+
|
|
17
|
+
from ..base import Command
|
|
18
|
+
from ..console import console
|
|
19
|
+
from ..i18n import i18n
|
|
20
|
+
from ..utils import runtime_state
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _stop_instance(state: dict, force: bool = False) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
停止一个运行实例(子进程先断,监督进程优雅收尾后逐级升级)
|
|
26
|
+
|
|
27
|
+
:param state: 状态字典(pid / child_pid)
|
|
28
|
+
:param force: 跳过优雅信号直接强制终止
|
|
29
|
+
:return: 是否成功停止
|
|
30
|
+
"""
|
|
31
|
+
pid = state.get("pid")
|
|
32
|
+
child_pid = state.get("child_pid")
|
|
33
|
+
if not isinstance(pid, int) or not runtime_state.pid_alive(pid):
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
# 1. 先断框架子进程:Windows 强停监督进程前必须先断子,
|
|
37
|
+
# 否则监督者会把强杀误判为崩溃而按退避策略重新拉起
|
|
38
|
+
if isinstance(child_pid, int) and runtime_state.pid_alive(child_pid):
|
|
39
|
+
runtime_state.terminate(child_pid, force=force or os_needs_force())
|
|
40
|
+
if not runtime_state.wait_gone(child_pid, runtime_state.FORCE_TIMEOUT_SECS):
|
|
41
|
+
runtime_state.kill_tree_hard(child_pid)
|
|
42
|
+
|
|
43
|
+
# 2. 停监督进程:优雅信号 → 等待 → 强制
|
|
44
|
+
if not force:
|
|
45
|
+
runtime_state.terminate(pid, force=False)
|
|
46
|
+
if runtime_state.wait_gone(pid, runtime_state.GRACEFUL_TIMEOUT_SECS):
|
|
47
|
+
return True
|
|
48
|
+
console.print(f"[warning]{i18n.t('cli.stop.graceful_timeout')}[/]")
|
|
49
|
+
runtime_state.terminate(pid, force=True)
|
|
50
|
+
if runtime_state.wait_gone(pid, runtime_state.FORCE_TIMEOUT_SECS):
|
|
51
|
+
return True
|
|
52
|
+
runtime_state.kill_tree_hard(pid)
|
|
53
|
+
return not runtime_state.pid_alive(pid)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def os_needs_force() -> bool:
|
|
57
|
+
"""Windows 无优雅信号语义:子进程直接强停"""
|
|
58
|
+
return sys.platform.startswith("win")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class StopCommand(Command):
|
|
62
|
+
"""
|
|
63
|
+
stop 命令
|
|
64
|
+
|
|
65
|
+
停止正在运行的 ErisPulse 实例(由 ``epsdk run`` 启动)
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
name = "stop"
|
|
69
|
+
description = i18n.t("cli.stop.description")
|
|
70
|
+
aliases: list = []
|
|
71
|
+
category = "project"
|
|
72
|
+
|
|
73
|
+
def add_arguments(self, parser: ArgumentParser):
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--force",
|
|
76
|
+
action="store_true",
|
|
77
|
+
default=False,
|
|
78
|
+
help=i18n.t("cli.stop.force_help"),
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--pid",
|
|
82
|
+
type=int,
|
|
83
|
+
default=None,
|
|
84
|
+
help=i18n.t("cli.stop.pid_help"),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def execute(self, args: Namespace):
|
|
88
|
+
state = runtime_state.read()
|
|
89
|
+
if args.pid is not None:
|
|
90
|
+
state = {"pid": args.pid, "child_pid": None}
|
|
91
|
+
if state is None or not runtime_state.pid_alive(state.get("pid", -1)):
|
|
92
|
+
console.print(f"[warning]{i18n.t('cli.stop.not_running')}[/]")
|
|
93
|
+
runtime_state.clear()
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
pid = state.get("pid")
|
|
97
|
+
console.print(f"[info]{i18n.t('cli.stop.stopping', pid=pid)}[/]")
|
|
98
|
+
if _stop_instance(state, force=bool(args.force)):
|
|
99
|
+
runtime_state.clear()
|
|
100
|
+
console.print(f"[success]{i18n.t('cli.stop.stopped')}[/]")
|
|
101
|
+
else:
|
|
102
|
+
console.print(f"[error]{i18n.t('cli.stop.failed', pid=pid)}[/]")
|
|
103
|
+
raise SystemExit(1)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class RestartCommand(Command):
|
|
107
|
+
"""
|
|
108
|
+
restart 命令
|
|
109
|
+
|
|
110
|
+
重启正在运行的 ErisPulse 实例(停止后按原参数重新启动,前台接管控制台)
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
name = "restart"
|
|
114
|
+
description = i18n.t("cli.restart.description")
|
|
115
|
+
aliases: list = []
|
|
116
|
+
category = "project"
|
|
117
|
+
|
|
118
|
+
def add_arguments(self, parser: ArgumentParser):
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--force",
|
|
121
|
+
action="store_true",
|
|
122
|
+
default=False,
|
|
123
|
+
help=i18n.t("cli.stop.force_help"),
|
|
124
|
+
)
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"--pid",
|
|
127
|
+
type=int,
|
|
128
|
+
default=None,
|
|
129
|
+
help=i18n.t("cli.stop.pid_help"),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def execute(self, args: Namespace):
|
|
133
|
+
state = runtime_state.read()
|
|
134
|
+
if args.pid is not None:
|
|
135
|
+
state = {"pid": args.pid, "child_pid": None}
|
|
136
|
+
|
|
137
|
+
if state is None or not runtime_state.pid_alive(state.get("pid", -1)):
|
|
138
|
+
console.print(f"[warning]{i18n.t('cli.restart.not_running_hint')}[/]")
|
|
139
|
+
runtime_state.clear()
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
argv = state.get("argv") or [sys.argv[0], "run"]
|
|
143
|
+
console.print(f"[info]{i18n.t('cli.stop.stopping', pid=state.get('pid'))}[/]")
|
|
144
|
+
if not _stop_instance(state, force=bool(args.force)):
|
|
145
|
+
console.print(f"[error]{i18n.t('cli.stop.failed', pid=state.get('pid'))}[/]")
|
|
146
|
+
raise SystemExit(1)
|
|
147
|
+
runtime_state.clear()
|
|
148
|
+
console.print(f"[info]{i18n.t('cli.restart.starting_new', cmd=' '.join(argv))}[/]")
|
|
149
|
+
# 前台接管:新实例写自己的状态文件,退出码透传
|
|
150
|
+
raise SystemExit(subprocess.call(argv))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
__all__ = ["StopCommand", "RestartCommand"]
|
|
@@ -373,6 +373,17 @@ TRANSLATIONS = {
|
|
|
373
373
|
"cli.self_update.invalid_selection": "Invalid version number or version string",
|
|
374
374
|
# ==================== run command ====================
|
|
375
375
|
"cli.run.description": "Run the main program",
|
|
376
|
+
"cli.stop.description": "Stop a running ErisPulse instance (started via epsdk run)",
|
|
377
|
+
"cli.stop.force_help": "Skip graceful shutdown and terminate forcefully",
|
|
378
|
+
"cli.stop.pid_help": "Target process PID (overrides the runtime state file)",
|
|
379
|
+
"cli.stop.not_running": "No running instance found — execute from the project directory with an instance started via epsdk run (processes started via python main.py must be managed with system tools)",
|
|
380
|
+
"cli.stop.stopping": "Stopping instance (PID {pid})...",
|
|
381
|
+
"cli.stop.graceful_timeout": "Graceful shutdown timed out, escalating to force kill...",
|
|
382
|
+
"cli.stop.stopped": "Instance stopped",
|
|
383
|
+
"cli.stop.failed": "Failed to stop instance (PID {pid}); please check the process manually",
|
|
384
|
+
"cli.restart.description": "Restart a running ErisPulse instance (stop, then relaunch with the original arguments)",
|
|
385
|
+
"cli.restart.not_running_hint": "No running instance found — just run epsdk run to start one",
|
|
386
|
+
"cli.restart.starting_new": "Stopped. Relaunching with the original arguments: {cmd}",
|
|
376
387
|
"cli.run.script_help": "Script path to run (default: run SDK directly)",
|
|
377
388
|
"cli.run.reload_help": "Enable hot reload mode",
|
|
378
389
|
"cli.run.strict_help": "Temporarily enable strict mode: exit on preflight failure, framework strict level raised to at least 1 (skip non-compliant components) for this run; defaults to ErisPulse.framework.strict_mode, --no-strict disables temporarily",
|
|
@@ -373,6 +373,17 @@ TRANSLATIONS = {
|
|
|
373
373
|
"cli.self_update.invalid_selection": "無効なバージョン番号またはバージョン文字列です",
|
|
374
374
|
# ==================== run コマンド ====================
|
|
375
375
|
"cli.run.description": "メインプログラムを実行",
|
|
376
|
+
"cli.stop.description": "稼働中の ErisPulse インスタンスを停止します(epsdk run で起動したもの)",
|
|
377
|
+
"cli.stop.force_help": "グレースフル停止をスキップし、強制終了します",
|
|
378
|
+
"cli.stop.pid_help": "対象プロセスの PID を直接指定(ランタイム状態ファイルを上書き)",
|
|
379
|
+
"cli.stop.not_running": "稼働中のインスタンスが見つかりません——プロジェクトディレクトリで実行し、epsdk run で起動したインスタンスを対象にしてください(python main.py 直起動のプロセスは OS のツールで管理してください)",
|
|
380
|
+
"cli.stop.stopping": "インスタンスを停止中 (PID {pid})...",
|
|
381
|
+
"cli.stop.graceful_timeout": "グレースフル停止がタイムアウト、強制終了に移行します...",
|
|
382
|
+
"cli.stop.stopped": "インスタンスを停止しました",
|
|
383
|
+
"cli.stop.failed": "インスタンスの停止に失敗しました (PID {pid})。手動でプロセス状態を確認してください",
|
|
384
|
+
"cli.restart.description": "稼働中の ErisPulse インスタンスを再起動します(停止後、元の引数で再起動)",
|
|
385
|
+
"cli.restart.not_running_hint": "稼働中のインスタンスが見つかりません——epsdk run で起動してください",
|
|
386
|
+
"cli.restart.starting_new": "停止しました。元の引数で再起動します: {cmd}",
|
|
376
387
|
"cli.run.script_help": "実行するスクリプトパス(省略時は SDK を直接実行)",
|
|
377
388
|
"cli.run.reload_help": "ホットリロードモードを有効化",
|
|
378
389
|
"cli.run.strict_help": "厳格モードを一時有効化:起動前チェック失敗時に終了し、フレームワークの厳格レベルを今回のみ 1 以上に引き上げます。デフォルトは ErisPulse.framework.strict_mode に従い、--no-strict で一時無効化",
|
|
@@ -373,6 +373,17 @@ TRANSLATIONS = {
|
|
|
373
373
|
"cli.self_update.invalid_selection": "Неверный номер версии или строка версии",
|
|
374
374
|
# ==================== команда run ====================
|
|
375
375
|
"cli.run.description": "Запустить основную программу",
|
|
376
|
+
"cli.stop.description": "Остановить запущенный экземпляр ErisPulse (запущенный через epsdk run)",
|
|
377
|
+
"cli.stop.force_help": "Пропустить плавную остановку и завершить принудительно",
|
|
378
|
+
"cli.stop.pid_help": "Указать PID целевого процесса напрямую (вместо файла состояния)",
|
|
379
|
+
"cli.stop.not_running": "Работающий экземпляр не найден — выполняйте из каталога проекта для экземпляра, запущенного через epsdk run (процессы, запущенные через python main.py, управляйте системными средствами)",
|
|
380
|
+
"cli.stop.stopping": "Остановка экземпляра (PID {pid})...",
|
|
381
|
+
"cli.stop.graceful_timeout": "Истекло время плавной остановки, переход к принудительному завершению...",
|
|
382
|
+
"cli.stop.stopped": "Экземпляр остановлен",
|
|
383
|
+
"cli.stop.failed": "Не удалось остановить экземпляр (PID {pid}); проверьте процесс вручную",
|
|
384
|
+
"cli.restart.description": "Перезапустить работающий экземпляр ErisPulse (остановка и повторный запуск с исходными аргументами)",
|
|
385
|
+
"cli.restart.not_running_hint": "Работающий экземпляр не найден — просто выполните epsdk run",
|
|
386
|
+
"cli.restart.starting_new": "Остановлено. Перезапуск с исходными аргументами: {cmd}",
|
|
376
387
|
"cli.run.script_help": "Путь к скрипту для запуска (по умолчанию: запуск SDK напрямую)",
|
|
377
388
|
"cli.run.reload_help": "Включить режим горячей перезагрузки",
|
|
378
389
|
"cli.run.strict_help": "Временное включение строгого режима: выход при провале проверки, уровень строгости фреймворка не ниже 1 (пропуск несоответствующих компонентов) для этого запуска; по умолчанию следует ErisPulse.framework.strict_mode, --no-strict временно отключает",
|
|
@@ -373,6 +373,17 @@ TRANSLATIONS = {
|
|
|
373
373
|
"cli.self_update.invalid_selection": "无效的版本序号或版本号",
|
|
374
374
|
# ==================== run 命令 ====================
|
|
375
375
|
"cli.run.description": "运行主程序",
|
|
376
|
+
"cli.stop.description": "停止正在运行的 ErisPulse 实例(由 epsdk run 启动)",
|
|
377
|
+
"cli.stop.force_help": "跳过优雅停止,直接强制终止",
|
|
378
|
+
"cli.stop.pid_help": "直接指定目标进程 PID(覆盖运行状态文件)",
|
|
379
|
+
"cli.stop.not_running": "未找到运行中的实例——请在项目目录下执行,且实例由 epsdk run 启动(python main.py 直启的进程请用系统工具管理)",
|
|
380
|
+
"cli.stop.stopping": "正在停止实例 (PID {pid})...",
|
|
381
|
+
"cli.stop.graceful_timeout": "优雅停止超时,升级为强制终止...",
|
|
382
|
+
"cli.stop.stopped": "实例已停止",
|
|
383
|
+
"cli.stop.failed": "停止实例失败 (PID {pid}),请手动检查进程状态",
|
|
384
|
+
"cli.restart.description": "重启正在运行的 ErisPulse 实例(停止后按原参数重新启动)",
|
|
385
|
+
"cli.restart.not_running_hint": "未找到运行中的实例——直接执行 epsdk run 启动即可",
|
|
386
|
+
"cli.restart.starting_new": "已停止,正在按原参数重新启动: {cmd}",
|
|
376
387
|
"cli.run.script_help": "要运行的主程序路径 (不指定则直接运行 SDK)",
|
|
377
388
|
"cli.run.reload_help": "启用热重载模式",
|
|
378
389
|
"cli.run.strict_help": "临时开启严格模式:自检失败即退出,框架严格级别本次至少为 1(跳过不合规组件);缺省跟随 ErisPulse.framework.strict_mode,--no-strict 临时关闭",
|
|
@@ -373,6 +373,17 @@ TRANSLATIONS = {
|
|
|
373
373
|
"cli.self_update.invalid_selection": "無效的版本序號或版本號",
|
|
374
374
|
# ==================== run 命令 ====================
|
|
375
375
|
"cli.run.description": "執行主程式",
|
|
376
|
+
"cli.stop.description": "停止正在執行的 ErisPulse 實例(由 epsdk run 啟動)",
|
|
377
|
+
"cli.stop.force_help": "跳過優雅停止,直接強制終止",
|
|
378
|
+
"cli.stop.pid_help": "直接指定目標程序 PID(覆蓋執行狀態檔)",
|
|
379
|
+
"cli.stop.not_running": "未找到執行中的實例——請在專案目錄下執行,且實例由 epsdk run 啟動(python main.py 直啟的程序請用系統工具管理)",
|
|
380
|
+
"cli.stop.stopping": "正在停止實例 (PID {pid})...",
|
|
381
|
+
"cli.stop.graceful_timeout": "優雅停止逾時,升級為強制終止...",
|
|
382
|
+
"cli.stop.stopped": "實例已停止",
|
|
383
|
+
"cli.stop.failed": "停止實例失敗 (PID {pid}),請手動檢查程序狀態",
|
|
384
|
+
"cli.restart.description": "重啟正在執行的 ErisPulse 實例(停止後按原參數重新啟動)",
|
|
385
|
+
"cli.restart.not_running_hint": "未找到執行中的實例——直接執行 epsdk run 啟動即可",
|
|
386
|
+
"cli.restart.starting_new": "已停止,正在按原參數重新啟動: {cmd}",
|
|
376
387
|
"cli.run.script_help": "要執行的主程式路徑 (不指定則直接執行 SDK)",
|
|
377
388
|
"cli.run.reload_help": "啟用熱重載模式",
|
|
378
389
|
"cli.run.strict_help": "臨時開啟嚴格模式:自檢失敗即退出,框架嚴格級別本次至少為 1(跳過不合規元件);缺省跟隨 ErisPulse.framework.strict_mode,--no-strict 臨時關閉",
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
运行实例状态文件(pidfile)工具
|
|
3
|
+
|
|
4
|
+
``epsdk run`` 启动时把监督进程 PID、当前子进程 PID 与原始 argv 写入
|
|
5
|
+
``.erispulse/runtime.json``(工作目录维度,多项目互不干扰);
|
|
6
|
+
``epsdk stop`` / ``epsdk restart`` 据此定位并控制运行中的实例。
|
|
7
|
+
|
|
8
|
+
跨平台语义:
|
|
9
|
+
- POSIX:优先 SIGINT(监督进程按 Ctrl+C 同款路径优雅收尾——finally 链
|
|
10
|
+
终止子进程),超时升级 SIGTERM → SIGKILL
|
|
11
|
+
- Windows:无进程信号语义,``os.kill(pid, SIGTERM)`` 即 TerminateProcess
|
|
12
|
+
(强制终止);stop 命令先断子进程再停监督进程,避免监督者把强杀误判
|
|
13
|
+
为崩溃而重新拉起
|
|
14
|
+
|
|
15
|
+
{!--< internal-use >!--}
|
|
16
|
+
供 CLI run / stop / restart 命令使用;状态文件损坏时按不存在处理。
|
|
17
|
+
{!--< /internal-use >!--}
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import signal
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
# 状态文件位置(相对当前工作目录;epsdk run / stop / restart 须在同一项目下执行)
|
|
29
|
+
STATE_DIR = Path(".erispulse")
|
|
30
|
+
STATE_FILE = STATE_DIR / "runtime.json"
|
|
31
|
+
|
|
32
|
+
# 优雅停止的等待预算(秒):SIGINT 后等待监督进程自行收尾,超时逐级升级
|
|
33
|
+
GRACEFUL_TIMEOUT_SECS = 8.0
|
|
34
|
+
FORCE_TIMEOUT_SECS = 3.0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def state_path() -> Path:
|
|
38
|
+
"""
|
|
39
|
+
获取状态文件绝对路径(当前工作目录维度)
|
|
40
|
+
|
|
41
|
+
:return: Path 状态文件路径
|
|
42
|
+
"""
|
|
43
|
+
return Path.cwd() / STATE_DIR / STATE_FILE
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def read() -> "dict | None":
|
|
47
|
+
"""
|
|
48
|
+
读取运行实例状态
|
|
49
|
+
|
|
50
|
+
:return: 状态字典 ``{"pid", "child_pid", "argv", "cwd", "started_at"}``;
|
|
51
|
+
不存在或损坏时返回 None
|
|
52
|
+
"""
|
|
53
|
+
path = state_path()
|
|
54
|
+
try:
|
|
55
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
56
|
+
if isinstance(data, dict) and isinstance(data.get("pid"), int):
|
|
57
|
+
return data
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def write(pid: int, argv: "list[str] | None" = None, child_pid: "int | None" = None) -> None:
|
|
64
|
+
"""
|
|
65
|
+
写入/覆盖运行实例状态
|
|
66
|
+
|
|
67
|
+
:param pid: 监督进程 PID(``epsdk run`` 自身;``--reload`` 内联模式为当前进程)
|
|
68
|
+
:param argv: 启动命令(restart 按此重放)
|
|
69
|
+
:param child_pid: 当前框架子进程 PID(无则省略)
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
state_path().parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
state_path().write_text(
|
|
74
|
+
json.dumps(
|
|
75
|
+
{
|
|
76
|
+
"pid": pid,
|
|
77
|
+
"child_pid": child_pid,
|
|
78
|
+
"argv": list(argv or sys.argv),
|
|
79
|
+
"cwd": str(Path.cwd()),
|
|
80
|
+
"started_at": time.time(),
|
|
81
|
+
},
|
|
82
|
+
ensure_ascii=False,
|
|
83
|
+
),
|
|
84
|
+
encoding="utf-8",
|
|
85
|
+
)
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def update_child(child_pid: int) -> None:
|
|
91
|
+
"""
|
|
92
|
+
更新当前框架子进程 PID(监督进程每次拉起新子进程时调用)
|
|
93
|
+
|
|
94
|
+
:param child_pid: 子进程 PID
|
|
95
|
+
"""
|
|
96
|
+
data = read()
|
|
97
|
+
if data is None:
|
|
98
|
+
return
|
|
99
|
+
data["child_pid"] = child_pid
|
|
100
|
+
try:
|
|
101
|
+
state_path().write_text(
|
|
102
|
+
json.dumps(data, ensure_ascii=False), encoding="utf-8"
|
|
103
|
+
)
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def clear() -> None:
|
|
109
|
+
"""清除状态文件(不存在时静默)"""
|
|
110
|
+
try:
|
|
111
|
+
state_path().unlink(missing_ok=True)
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def pid_alive(pid: int) -> bool:
|
|
117
|
+
"""
|
|
118
|
+
判断进程是否存活
|
|
119
|
+
|
|
120
|
+
:param pid: 进程 PID
|
|
121
|
+
:return: 是否存活
|
|
122
|
+
"""
|
|
123
|
+
if pid <= 0:
|
|
124
|
+
return False
|
|
125
|
+
if os.name == "nt":
|
|
126
|
+
import ctypes
|
|
127
|
+
|
|
128
|
+
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
129
|
+
STILL_ACTIVE = 259
|
|
130
|
+
kernel32 = ctypes.windll.kernel32
|
|
131
|
+
handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
|
132
|
+
if not handle:
|
|
133
|
+
return False
|
|
134
|
+
try:
|
|
135
|
+
exit_code = ctypes.c_ulong()
|
|
136
|
+
if kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
|
|
137
|
+
return exit_code.value == STILL_ACTIVE
|
|
138
|
+
return False
|
|
139
|
+
finally:
|
|
140
|
+
kernel32.CloseHandle(handle)
|
|
141
|
+
try:
|
|
142
|
+
os.kill(pid, 0)
|
|
143
|
+
return True
|
|
144
|
+
except ProcessLookupError:
|
|
145
|
+
return False
|
|
146
|
+
except PermissionError:
|
|
147
|
+
return True
|
|
148
|
+
except OSError:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def terminate(pid: int, force: bool = False) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
终止进程
|
|
155
|
+
|
|
156
|
+
POSIX:默认 SIGINT(监督进程按 Ctrl+C 同款路径优雅收尾);``force=True``
|
|
157
|
+
直接 SIGKILL。Windows:无优雅语义,``os.kill`` 即 TerminateProcess。
|
|
158
|
+
|
|
159
|
+
:param pid: 目标进程 PID
|
|
160
|
+
:param force: 是否跳过优雅信号直接强制终止
|
|
161
|
+
:return: 是否已发送终止信号
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
if os.name == "nt":
|
|
165
|
+
os.kill(pid, signal.SIGTERM)
|
|
166
|
+
return True
|
|
167
|
+
if force:
|
|
168
|
+
os.kill(pid, signal.SIGKILL)
|
|
169
|
+
else:
|
|
170
|
+
os.kill(pid, signal.SIGINT)
|
|
171
|
+
return True
|
|
172
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def wait_gone(pid: int, timeout: float) -> bool:
|
|
177
|
+
"""
|
|
178
|
+
等待进程退出
|
|
179
|
+
|
|
180
|
+
:param pid: 进程 PID
|
|
181
|
+
:param timeout: 最长等待秒数
|
|
182
|
+
:return: 是否已在时限内退出
|
|
183
|
+
"""
|
|
184
|
+
deadline = time.monotonic() + timeout
|
|
185
|
+
while time.monotonic() < deadline:
|
|
186
|
+
if not pid_alive(pid):
|
|
187
|
+
return True
|
|
188
|
+
time.sleep(0.15)
|
|
189
|
+
return not pid_alive(pid)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def kill_tree_hard(pid: int) -> None:
|
|
193
|
+
"""
|
|
194
|
+
强制终止进程(Windows 下连带其直接子进程的兜底清理)
|
|
195
|
+
|
|
196
|
+
:param pid: 进程 PID
|
|
197
|
+
"""
|
|
198
|
+
if os.name == "nt":
|
|
199
|
+
try:
|
|
200
|
+
subprocess.run(
|
|
201
|
+
["taskkill", "/PID", str(pid), "/T", "/F"],
|
|
202
|
+
capture_output=True,
|
|
203
|
+
check=False,
|
|
204
|
+
)
|
|
205
|
+
return
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
terminate(pid, force=True)
|
|
@@ -59,6 +59,7 @@ from .session_type import (
|
|
|
59
59
|
unregister_custom_type,
|
|
60
60
|
unregister_custom_types_by_owner,
|
|
61
61
|
)
|
|
62
|
+
from .command_flow import CommandFlow
|
|
62
63
|
from .wrapper import (
|
|
63
64
|
CONFIRM_NO_WORDS,
|
|
64
65
|
CONFIRM_YES_WORDS,
|
|
@@ -118,6 +119,7 @@ __all__ = [
|
|
|
118
119
|
"SendType",
|
|
119
120
|
"SessionOccupiedError",
|
|
120
121
|
"clear_custom_types",
|
|
122
|
+
"CommandFlow",
|
|
121
123
|
"command",
|
|
122
124
|
"convert_to_receive_type",
|
|
123
125
|
"convert_to_send_type",
|
|
@@ -109,6 +109,14 @@ async def _invoke_handler(handler_info: dict, event: Event) -> None:
|
|
|
109
109
|
# 注入 owner 上下文:handler 执行期间 current_owner = 模块 owner,
|
|
110
110
|
# 使其内部发起的出站调用(Send / Api / Request)能被作用域权限识别与按 owner 清理
|
|
111
111
|
_owner_token = current_owner.set(_owner) if _owner else None
|
|
112
|
+
if _owner:
|
|
113
|
+
# 活跃度埋点(懒加载休眠清扫判定):owner 已定位,一次字典写入
|
|
114
|
+
try:
|
|
115
|
+
from ..module import module as _module_manager
|
|
116
|
+
|
|
117
|
+
_module_manager.touch_activity(_owner)
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
112
120
|
try:
|
|
113
121
|
# 依赖注入解析(Depends):上下文为 Event;依赖异常走本函数统一错误路径
|
|
114
122
|
_depends = handler_info.get("depends")
|