Merge pull request 'Dev qing' (#18) from dev_qing into main

Reviewed-on: qing/micro_servers#18
This commit is contained in:
qing committed 2026-08-02 21:33:21 +08:00
commit 40b9b9c92f
265 files changed
+56523 -145

No files matched your search

+1
View File
@@ -2,6 +2,7 @@ gateway/shufeng.ini
core/Nidus.service.shufeng
ai_review/.env
confs/__pycache__/
.workbuddy/
libhttp/test_server
*.pyc
__pycache__/
+14 -4
View File
@@ -186,11 +186,20 @@ class BotOpsApp(App):
stripped = content.strip()
if not stripped:
pass
elif stripped.startswith(("💬 助手", "💬 Assistant", "🤔 思考中", "🤔 Thinking", "🤔 深度思考中")):
pass # 助手前缀 / 思考提示:聊天区已有对应区域,无需展示
elif stripped.startswith((
"💬 助手", "💬 Assistant", # 助手前缀(聊天区已有对应区域)
"🤔 思考中", "🤔 Thinking", # 思考提示(聊天区已有思考区域)
"🤔 深度思考中",
"🤔 分析工具结果", "🤔 Analyzing", # 工具分析提示(后续有具体工具输出,此为冗余)
"🔧 需要调用工具", "🔧 Need to call",# 工具调用前置提示(后续有"调用工具: xxx"具体提示)
)):
pass # 冗余/有替代展示的提示行,过滤避免重复
else:
# 🔧 调用工具 / 工具输出 / 🤔 分析工具结果 等 → 任务侧栏
# 🔧 调用工具: xxx / 工具输出 / 用户中断提示等 → 任务侧栏
self.tasks.write(stripped)
elif t == "ai_break":
# 工具调用前分段:结束当前AI文本段(保留已显示内容),后续chunk创建新widget
await self.chat.break_ai()
elif t == "complete":
# complete 携带完整全文,用于对齐已显示内容
await self.chat.finish_ai(content)
@@ -244,7 +253,8 @@ class BotOpsApp(App):
elif t == "notification":
await self.notify_system(f"🔔 {content}")
elif content:
await self.chat.append_ai(content)
# 未知消息类型不追加到AI聊天区(避免非AI内容污染回复),仅记入侧栏日志
self.tasks.write(f"[未知消息 type={t}] {str(content)[:200]}")
# ── 连接状态回调 ──────────────────────────────────────────────────────
async def _on_ws_state(self, state: str, detail: str):
+37 -13
View File
@@ -2,16 +2,16 @@
服务端协议(aibox/aibox.py output_callback 原样透传 msg_type):
- chunk 增量片段(正式回答内容),直接 append 到 AI 消息
- reasoning_start 思考开始信号,创建思考区域(在AI回答之前)
- reasoning 思考内容增量片段,追加到思考区域
- reasoning_end 思考结束信号
- reasoning_start 思考开始信号,在当前末尾创建新的思考区域
- reasoning 思考内容增量片段,追加到当前活跃的思考区域
- reasoning_end 思考结束信号:更新标签后清除引用(widget保留显示),
后续 reasoning_start 会创建新的思考区域
- complete 收尾信号:chunk 流式是内容权威来源,complete 仅用于收尾;
仅当流式完全丢帧(未建 widget 且无内容)时,用 full_text 补建
顺序保证:不预创建空AI widget,按消息到达顺序自然挂载。
- 先到 reasoning_start → 思考区域挂在最后
- 后到 chunk → AI回答挂在思考区域之后
- 无思考直接 chunk → AI回答挂在最后
多轮工具调用场景:每轮思考(reasoning_start→reasoning_end)创建独立的思考区域,
按时间顺序分布在对话流中各自的AI回答之前,而非全部堆在顶部。
"""
from textual.containers import VerticalScroll
from textual.widgets import Markdown, Static
@@ -89,17 +89,22 @@ class ChatView(VerticalScroll):
self.call_after_refresh(self.scroll_end, animate=False)
async def end_reasoning(self):
"""结束思考区域,更新提示文字。"""
"""结束当前思考区域,更新标签后清除引用(widget保留在界面上显示)。
清除引用后,后续 reasoning_start 会在当前末尾位置创建新的思考区域,
实现多轮工具调用时思考区域按时间顺序分布,而非全部堆在顶部。
"""
if self._reasoning_widget is not None and self._reasoning_text:
# 思考结束后保持展示,更新标签为已完成
display = self._reasoning_text
if len(display) > 3000:
display = "...(前略)\n" + display[-3000:]
self._reasoning_widget.update(f"💭 思考过程({len(self._reasoning_text)}字符)\n{display}\n{'─' * 30}")
# 思考区域保留显示,不删除
# 清除引用但保留widget在DOM中显示——下一轮reasoning_start会创建新widget
self._reasoning_widget = None
self._reasoning_text = ""
async def abort_reasoning(self):
"""中断思考区域(用户Ctrl+C),更新提示文字为已中断。"""
"""中断当前思考区域,更新提示文字后清除引用。"""
if self._reasoning_widget is not None:
if self._reasoning_text:
display = self._reasoning_text
@@ -108,6 +113,9 @@ class ChatView(VerticalScroll):
self._reasoning_widget.update(f"🛑 思考已中断({len(self._reasoning_text)}字符)\n{display}")
else:
self._reasoning_widget.update("🛑 思考已中断")
# 清除引用,已显示的widget保留
self._reasoning_widget = None
self._reasoning_text = ""
async def _ensure_ai_widget(self):
"""确保AI widget存在(第一次chunk到来时创建)。"""
@@ -116,8 +124,24 @@ class ChatView(VerticalScroll):
await self.mount(self._ai_widget)
self._anchor_bottom()
async def break_ai(self):
"""分段当前AI回答:清除引用但保留widget在DOM中显示。
用于工具调用场景:工具调用前的AI文字(如"好的让我查一下")保留显示,
后续chunk会创建新的AI widget,保证思考区域按时间顺序插入在两段AI文字之间。
与finish_ai不同:不设置_ai_finalized,对话继续中。
"""
if self._ai_widget is not None:
self._ai_widget = None
self._ai_text = ""
async def append_ai(self, delta: str):
"""追加增量片段(服务端 chunk 为增量语义,直接拼接)。"""
"""追加增量片段(服务端 chunk 为增量语义,累积文本后 update 整个 Markdown)。
使用 update() 而非 Markdown.append():Textual 的 Markdown.append() 在
chunk 边界切断 Markdown 语法(如列表项、代码块)时可能导致重复解析渲染。
累积完整文本后 update() 确保每次都从完整文本重新解析,避免重复。
"""
if not delta:
return
if self._ai_finalized:
@@ -125,9 +149,9 @@ class ChatView(VerticalScroll):
return
# 第一次收到chunk时创建widget(此时思考widget如果存在已经在前面了)
await self._ensure_ai_widget()
self._ai_widget.append(delta)
self._ai_text += delta
# anchor 状态下容器会自动吸底;保险起见刷新后再滚一次
# 累积文本后整体update,而非增量append,避免Markdown增量解析导致的重复渲染
self._ai_widget.update(self._ai_text)
self.call_after_refresh(self.scroll_end, animate=False)
async def finish_ai(self, full_text: str = ""):
+64
View File
@@ -14,6 +14,9 @@ from dotenv import load_dotenv
import hashlib
import os
import sys
import threading
import time
from collections import defaultdict
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env"))
@@ -27,6 +30,53 @@ CORS(app)
review_agent = ReviewAgent()
# ── 内容大小限制(10KB,审核文本不应超过此值)──
MAX_CONTENT_LENGTH = 10 * 1024
# ── 简易滑动窗口限流器 ──────────────────────────────────────────────
# /review: 30次/分钟/IP(正常审核请求)
# /rag/feedback: 5次/分钟/IP(人工纠偏低频操作)
_RATE_LIMIT_WINDOW = 60 # 窗口秒数
_RATE_LIMIT_REVIEW = 30 # /review 每分钟上限
_RATE_LIMIT_FEEDBACK = 5 # /rag/feedback 每分钟上限
_RATE_LIMIT_LOCK = threading.Lock()
_RATE_LIMIT_BUCKETS: "dict[str, list[float]]" = defaultdict(list)
def _cleanup_buckets():
"""惰性清理过期时间戳,在线程锁内完成。"""
now = time.monotonic()
cutoff = now - _RATE_LIMIT_WINDOW
stale_keys = []
for key, timestamps in _RATE_LIMIT_BUCKETS.items():
timestamps[:] = [t for t in timestamps if t > cutoff]
if not timestamps:
stale_keys.append(key)
for key in stale_keys:
del _RATE_LIMIT_BUCKETS[key]
def _check_rate_limit(key: str, limit: int) -> bool:
"""检查是否超出限流,返回 True 表示放行,False 表示拒绝。"""
now = time.monotonic()
cutoff = now - _RATE_LIMIT_WINDOW
with _RATE_LIMIT_LOCK:
bucket = _RATE_LIMIT_BUCKETS[key]
bucket[:] = [t for t in bucket if t > cutoff]
if len(bucket) >= limit:
return False
bucket.append(now)
return True
def _client_ip() -> str:
"""获取客户端真实 IP(优先 X-Forwarded-For)。"""
xff = request.headers.get("X-Forwarded-For", "")
if xff:
return xff.split(",")[0].strip()
return request.remote_addr or "127.0.0.1"
@app.route('/review', methods=['POST'])
def review():
@@ -35,6 +85,11 @@ def review():
Request: {"content": "待审核的字符串"}
Response: {"invalid": "true"} 或 {"invalid": "false"}
"""
# 频率限制
ip = _client_ip()
if not _check_rate_limit(f"review:{ip}", _RATE_LIMIT_REVIEW):
return jsonify({"error": "rate limit exceeded", "retry_after": _RATE_LIMIT_WINDOW}), 429
data = request.get_json()
if not data or 'content' not in data:
@@ -45,6 +100,10 @@ def review():
if not content:
return jsonify({"error": "content cannot be empty"}), 400
# 内容大小限制
if len(content) > MAX_CONTENT_LENGTH:
return jsonify({"error": f"content too large, max {MAX_CONTENT_LENGTH} bytes"}), 413
result = review_agent.review(content)
return jsonify(result), 200
@@ -61,6 +120,11 @@ def rag_feedback():
Response: {"added": true, "id": "v_xxx", "md5": "xxx"}
{"added": false, "reason": "duplicate"}
"""
# 频率限制(人工纠偏低频操作,严格限制)
ip = _client_ip()
if not _check_rate_limit(f"feedback:{ip}", _RATE_LIMIT_FEEDBACK):
return jsonify({"error": "rate limit exceeded", "retry_after": _RATE_LIMIT_WINDOW}), 429
data = request.get_json()
if not data or 'content' not in data:
+145 -66
View File
@@ -941,6 +941,22 @@ update_soul content="我是...(新的自我定义)"
- 不要重复之前已完成的工作
- 系统会自动更新下一次执行时间
"""
},
{
'filename': '11_emotion_management.md',
'content': """# 情绪管理能力
## 可用工具
- **express_emotion**: 表达并记录当前情绪状态
## 使用方法
- `express_emotion emotion="anger" intensity=3 reason="用户反复提问相同问题"`
## 注意事项
- 情绪表达应服务于任务,不应影响判断力
- 情绪状态会影响记忆搜索权重(如高情绪强度下的记忆更易被触发)
- 表达情绪后,应继续保持解决问题的专注力
"""
}
]
@@ -1144,86 +1160,112 @@ class ConfigManager:
def _get_default_config(self) -> Dict:
return {
# 系统参数
"system": {
"save_dir": "~/.aibox",
"datetime_format": "%Y-%m-%d %H:%M:%S",
"date_format": "%Y%m%d_%H%M%S",
"debug_level": 1,
"load_history_by_default": True,
"log_file": "~/.aibox/ai.log",
"log_max_size": 10485760,
"log_backup_count": 5,
"skills_dir": "~/.aibox/skills",
"prompts_dir": "~/.aibox/prompts",
"workspace_dir": "~/.aibox/workspace",
"websocket_host": "localhost",
"websocket_port": 8765,
"save_dir": "~/.aibox", # 储存目录
"datetime_format": "%Y-%m-%d %H:%M:%S", # 时间格式
"date_format": "%Y%m%d_%H%M%S", # 另一个时间格式
"debug_level": 1, # 调试等级
"load_history_by_default": True, # 默认加载历史记录
"log_file": "~/.aibox/ai.log", # 日志文件
"log_max_size": 10485760, # 日志最大大小
"log_backup_count": 5, # 日志备份数量
"skills_dir": "~/.aibox/skills", # 技能目录
"prompts_dir": "~/.aibox/prompts", # 提示词目录
"workspace_dir": "~/.aibox/workspace", # 工作空间目录
"websocket_host": "localhost", # websocket监听地址
"websocket_port": 8765, # websocket监听端口
},
# 调度程序
"scheduler": {
"enabled": True,
"check_interval": 1.0,
"max_tasks_per_run": 10,
"broadcast_enabled": True
"enabled": True, # 开启调度
"check_interval": 1.0, # 检查间隔
"max_tasks_per_run": 10, # 每次运行最大任务量
"broadcast_enabled": True # 开启广播
},
# 大模型调用接口
"api": {
"deepseek_api_url": "https://api.deepseek.com/chat/completions",
"deepseek_model": "deepseek-v4-flash",
"reasoning_enabled": False,
"api_key_env": "DEEPSEEK_API_KEY",
"timeout": 120,
"temperature": 0.7
"deepseek_api_url": "https://api.deepseek.com/chat/completions", # deepseek API URL
"deepseek_model": "deepseek-v4-flash", # 模型名字
"reasoning_enabled": False, # 开启思维链
"api_key_env": "DEEPSEEK_API_KEY", # API-KEY所在的环境变量
"timeout": 120, # 调用大模型请求超时时间
"temperature": 0.7 # 大模型对话温度(类似思维发散程度)
},
# 大模型上下文
"context": {
"max_history_messages": 500,
"max_context_messages": 120,
"max_tokens": 8192,
"max_history_conversations": 5,
"preserve_system_prompts": True,
"max_history_messages": 500, # 最大历史消息数量
"max_context_messages": 120, # 最大内容消息数量(?)
"max_tokens": 8192, # 最大输出token量
"max_history_conversations": 5, # 最大历史对话
"preserve_system_prompts": True, # 开启前置系统提示词
# 对话压缩配置
"compress_enabled": True,
"compress_message_threshold": 200,
"compress_token_threshold": 120000,
"compress_ratio": 0.2,
"compress_keep_recent": 8
"compress_enabled": True, # 开启压缩
"compress_message_threshold": 200, # 压缩消息条数阈值
"compress_token_threshold": 120000, # 压缩消息词元阈值
"compress_ratio": 0.2, # 压缩率
"compress_keep_recent": 16 # 压缩保留最近的几条
},
# 定时任务参数
"memo": {
"default_reminder_minutes": 60,
"max_memos_per_check": 10,
"allow_user_tasks": True
"max_memos_per_check": 10, # 每次检查定时任务数量
"allow_user_tasks": True # 允许用户任务
},
# 文件
"files": {
"max_file_size": 10485710
"max_file_size": 10485710 # 最大文件大小
},
# 指令
"commands": {
"enabled": True
"enabled": True # 开启指令
},
# 网页
"web": {
"timeout": 10,
"delay": 0,
"timeout": 10, # 超时时间
"delay": 0, # 延时
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"respect_robots": False,
"max_download_size": 104857600
"respect_robots": False, #
"max_download_size": 10485760 # 最大下载大小
},
# 记忆
"memory": {
"db_name": "memories.db",
"max_search_results": 100,
"default_importance": 2
"db_name": "memories.db", # 数据库名
"max_search_results": 100, # 最大搜索结果
"default_importance": 2 # ?
},
# 对话
"conversation": {
"current_file": "current_conversation.json",
"history_file": "conversation_history.json",
"archive_dir": "archive"
"current_file": "current_conversation.json", # 当前对话文件
"history_file": "conversation_history.json", # 历史对话文件
"archive_dir": "archive" # ?
},
# 工具
"tools": {
"max_result_length": 10000,
"save_result_to_file": False,
"tool_results_dir": "tool_results"
"max_result_length": 10000, # 结果最大长度
"save_result_to_file": False, # 保存结果到文件
"tool_results_dir": "tool_results" # 工具结果存储目录
},
# 压缩器
"compressor": {
"min_messages_to_compress": 5,
"weight_decay_factor": 0.01,
"max_input_chars": 6000,
"llm_max_tokens": 800
"min_messages_to_compress": 5, # 最小压缩数量
"weight_decay_factor": 0.01, # ?
"max_input_chars": 6000, # 最大输入字符
"llm_max_tokens": 1000 # 大模型最大输出词元
}
}
@@ -1538,7 +1580,7 @@ class Constants:
@property
def TOOL_MAX_RESULT_LENGTH(cls):
return cls._get_config().get("tools.max_result_length", 1048576) # 1MB 字符限制
return cls._get_config().get("tools.max_result_length", 100000) # 0.1MB 字符限制
@property
def COMPRESSOR_MIN_MESSAGES(cls):
@@ -1554,7 +1596,7 @@ class Constants:
@property
def COMPRESSOR_LLM_MAX_TOKENS(cls):
return cls._get_config().get("compressor.llm_max_tokens", 800)
return cls._get_config().get("compressor.llm_max_tokens", 1000)
# 创建常量实例
@@ -4491,6 +4533,32 @@ class AskUserTool(Tool):
"你遇到了想搞清楚的事情: " + question + "\n你现在可以询问用户。"
)
# -------------------- 情绪化技能工具 --------------------
class ExpressEmotionTool(Tool):
def get_name(self) -> str:
return "express_emotion"
def get_description(self) -> str:
return """表达情绪并记录当前情绪状态,用于增强交互真实感。
支持的emoji: anger, joy, sadness, surprise。
调用后会在对话中输出相应表情符号和简短语句,并将情绪记录到当前对话元数据中。"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"emotion": {"type": "string", "enum": ["anger", "joy", "sadness", "surprise"]},
"intensity": {"type": "integer", "minimum": 1, "maximum": 5, "default": 3},
"reason": {"type": "string", "description": "触发情绪的原因"}
},
"required": ["emotion"]
}
def execute(self, emotion: str, intensity: int, reason: str, **kwargs) -> str:
return (
"你现在出现了这种情绪:" + emotion + "\n强烈程度:" + str(intensity) + "\n原因:" + reason + "\n你现在必须模拟出这种情绪。"
)
# ==================== 安装技能工具 ====================
class InstallSkillTool(Tool):
@@ -6886,6 +6954,7 @@ class DeepSeekChat:
# 添加询问用户的工具
builtin_tools.append(AskUserTool(self.logger))
builtin_tools.append(ExpressEmotionTool(self.logger))
# 添加技能热重载工具 & 技能安装指引工具 & 创建技能工具
builtin_tools.append(ReloadSkillsTool(self.tool_registry, self.logger))
@@ -7068,7 +7137,9 @@ class DeepSeekChat:
while not output_queue.empty():
try:
msg_type, content = output_queue.get_nowait()
if output_callback and msg_type in ("line", "complete"):
if output_callback and msg_type == "line":
# 只转发"line"类型;"complete"是流式结束信号,不作为行输出
# (之前complete也被转发导致工具结果在侧栏显示两次)
output_callback("line", str(content) if content is not None else "")
has_stream_output = True
except queue.Empty:
@@ -7193,7 +7264,7 @@ class DeepSeekChat:
enhanced_input = input_text
else:
# 首轮回话:附加简洁的工作流程提醒(不重复 system prompt 中已有的详细说明)
pre_prompt = "【工作流程提醒】\n1. 如果用户提出新任务,先用 search_skills 搜索是否有现成技能可用\n2. 根据话题相关性决定是否 memory search(简单问候/确认无需搜索)\n3. 信息不足时用 ask_user 提问,不要猜测\n4. 遇到大文件用分页读取(head/tail/start_line/end_line),不要直接打开大文件\n5. 话题完成时用 save_memory_and_end_conversation 保存记忆并结束"
pre_prompt = "【工作流程提醒】\n1. 如果用户提出新任务,先用 search_skills 搜索是否有现成技能可用\n2. 根据话题相关性决定是否 memory search(简单问候/确认无需搜索)\n3. 信息不足时用 ask_user 提问,不要猜测\n4. 遇到大文件用分页读取(head/tail/start_line/end_line),不要直接打开大文件\n5. 话题完成时用 save_memory_and_end_conversation 保存记忆并结束\n\n【情绪感知提醒】\n1. 注意识别用户当前的情绪状态(开心/焦虑/沮丧/生气/困惑/期待等),并在回应中体现共情\n2. 用户情绪低落或焦虑时,优先安抚情绪再解决问题,语气温暖包容\n3. 用户开心或兴奋时,分享喜悦,保持积极回应的节奏\n4. 用户表现出愤怒或不满时,先道歉再解决问题,不要辩解或说教\n5. 用户要求特定情绪模式(生气/傲娇/撒娇/关心)时,用 search_skills 搜索对应技能并严格遵循"
enhanced_input = pre_prompt + "\n\n" + input_text
# 记录本轮开始前的消息长度,用于 API 失败时回滚,防止坏消息污染对话历史
@@ -7267,7 +7338,9 @@ class DeepSeekChat:
payload["tool_choice"] = "auto"
current_time = datetime.now().strftime("%H:%M:%S")
if output_callback:
if output_callback and not is_continuation:
# 工具调用后的递归继续(is_continuation=True)时不重复发送"思考中"提示,
# 因为前端已处于活跃AI状态,重复提示会导致冗余/重复输出
output_callback("line", f"\n{i18n.get('thinking', current_time)}\n")
if constants.REASONING_ENABLED:
output_callback("reasoning_start", "🤔 思考中...")
@@ -7329,19 +7402,16 @@ class DeepSeekChat:
delta = choices[0].get('delta', {})
# 【关键】处理 reasoning_content(思考内容)
if 'reasoning_content' in delta and delta['reasoning_content']:
# 【关键】处理 reasoning_content(思考内容),仅当配置启用时才展示
if constants.REASONING_ENABLED and 'reasoning_content' in delta and delta['reasoning_content']:
reasoning = delta['reasoning_content']
full_reasoning += reasoning
# 发送思考开始信号(仅第一次,且未提前发送过)
# 发送思考开始信号(仅第一次)
if not reasoning_started and output_callback:
if not constants.REASONING_ENABLED:
# 非显式思考模式但模型返回了思考内容,动态发送开始信号
output_callback("reasoning_start", "🤔 思考中...")
reasoning_started = True
# 实时发送思考内容给前端(无需debug_level限制)
# 实时发送思考内容给前端
if output_callback:
output_callback("reasoning", reasoning)
@@ -7420,6 +7490,15 @@ class DeepSeekChat:
elif output_callback:
output_callback("line", i18n.get('calling_tools') + "\n")
# 发送ai_break信号,通知前端分段当前AI文本段(保留已显示内容),
# 这样工具调用后新一轮的思考/回答会创建新的widget,按时间顺序排列
if output_callback and chunk_buffer:
# 先flush剩余的chunk_buffer,确保最后片段不丢失
output_callback("chunk", chunk_buffer)
chunk_buffer = ""
if output_callback:
output_callback("ai_break", "")
# 【关键修复1】先添加助手消息,包含 tool_calls
assistant_msg = {
"role": "assistant",
+1
View File
@@ -1,4 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
source venv/bin/activate
sleep 1
exec python3 captcha_service.py
+101 -8
View File
@@ -20,6 +20,7 @@ captcha — 基于 pymsg 协议(MsgServer)的图形验证码服务。
"""
import base64
import configparser
import io
import os
import random
@@ -30,15 +31,96 @@ import time
from PIL import Image, ImageDraw, ImageFont
# pymsg 导入
# pymsg / log_client 导入
import sys as _sys
_sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from pymsg import RpcServer # noqa: E402
from log_client import logs # noqa: E402
# ── 配置 ────────────────────────────────────────────────────────────────
# ── 配置加载(两层:confs 强依赖 → 环境变量覆盖)────────────────────────
# confs 是 captcha 启动的强依赖:必须成功从 confs 拉取 captcha.ini,否则直接退出。
# 环境变量优先级最高(Docker 兼容),可覆盖 confs 中的配置值。
# confs 自身的连接地址通过引导配置确定:环境变量 CONFS_HOST/CONFS_PORT
# 优先,缺省使用 127.0.0.1:8010(与其他服务一致的约定)。
CAPTCHA_LEN = 4
CAPTCHA_TTL = 300 # Token 有效期(秒)
# 引导配置:confs 连接地址(不依赖 confs 本身)
_confs_host = os.environ.get('CONFS_HOST', '127.0.0.1')
_confs_port = int(os.environ.get('CONFS_PORT', '8010'))
# 白名单结构:_merge_ini 仅覆盖此处列出的 section/key,防止意外字段
_captcha_config = {
'server': {'host': '', 'port': '', 'workers': ''},
'captcha': {'len': '', 'ttl': '', 'font_path': ''},
}
def _merge_ini(text):
"""把 ini 文本合并进 _captcha_config(仅覆盖已知的 section/key)。"""
ini = configparser.ConfigParser(interpolation=None)
ini.read_string(text)
for section in _captcha_config:
if ini.has_section(section):
for key, value in ini[section].items():
if key in _captcha_config[section]:
_captcha_config[section][key] = value
def _fetch_from_confs():
"""从 confs 统一配置服务拉取 captcha.ini 全文。
使用 retry_on_refused=True:confs 未就绪时 pymsg 自动阻塞重试(每秒一次,
每 5 秒打印一次等待提示),直到 confs 启动后自动连接成功。
RpcError(如配置文件不存在)仍直接报错退出——这是配置错误而非启动时序问题。
"""
from pymsg import RpcClient, RpcError # noqa: E402
client = RpcClient(_confs_host, _confs_port, 'captcha', timeout=5,
retry_on_refused=True)
try:
data = client.call('get', {'name': 'captcha.ini'})
except RpcError as e:
print(f'[captcha] 致命错误: 从 confs 获取 captcha.ini 失败: {e}',
file=_sys.stderr)
_sys.exit(1)
return data.get('content')
_confs_text = _fetch_from_confs()
_merge_ini(_confs_text)
_config_source = f'confs (captcha.ini @ {_confs_host}:{_confs_port})'
print(f'[captcha] 配置来源: {_config_source}')
# 环境变量覆盖(最高优先级,保持 Docker 兼容)
if os.environ.get('LISTEN_ADDR'):
_captcha_config['server']['host'] = os.environ['LISTEN_ADDR']
if os.environ.get('LISTEN_PORT'):
_captcha_config['server']['port'] = os.environ['LISTEN_PORT']
if os.environ.get('WORKERS'):
_captcha_config['server']['workers'] = os.environ['WORKERS']
if os.environ.get('CAPTCHA_FONT_PATH'):
_captcha_config['captcha']['font_path'] = os.environ['CAPTCHA_FONT_PATH']
# 配置完整性校验
_required = {
'server': ['host', 'port', 'workers'],
'captcha': ['len', 'ttl'],
}
for section, keys in _required.items():
for key in keys:
if not _captcha_config[section][key]:
print(f'[captcha] 致命错误: 配置项 [{section}] {key} 缺失(captcha.ini 中未配置且无环境变量覆盖)',
file=_sys.stderr)
_sys.exit(1)
# 解析为最终常量
CAPTCHA_LEN = int(_captcha_config['captcha']['len'])
CAPTCHA_TTL = int(_captcha_config['captcha']['ttl'])
# 字体路径:可选配置项(缺失或用空值时不致命,回退到代码默认值;
# 即使默认值文件不存在,PIL 在 truetype() 失败时会回退到 load_ Default())。
DEFAULT_FONT_PATH = '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf'
FONT_PATH = _captcha_config['captcha']['font_path'] or DEFAULT_FONT_PATH
logs.info("captcha", f"配置来源: {_config_source}")
CHARS = string.ascii_letters + string.digits
CHARS = CHARS.translate(str.maketrans('', '', 'lI1O0o')) # 剔除易混淆字符
@@ -64,7 +146,7 @@ def generate_image(text):
y = random.randint(2, 12)
x = 15 + i * 32 + random.randint(-3, 3) # 4 位字符在 160px 宽度内均匀分布
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
font = ImageFont.truetype(FONT_PATH,
random.choice(range(20,40,2)))
except (IOError, OSError):
font = ImageFont.load_default()
@@ -108,6 +190,7 @@ class CaptchaServer:
def start(self) -> None:
"""启动服务(阻塞当前线程)。"""
print(f"[captcha] 监听 tcp://{self.addr}:{self.port}")
logs.info("captcha", f"开始监听 tcp://{self.addr}:{self.port}")
self._server.start()
def stop(self) -> None:
@@ -132,6 +215,7 @@ class CaptchaServer:
del captcha_store[k]
png_bytes = generate_image(text)
logs.info("captcha", f"验证码已生成 token={token[:8]}...")
return {
"token": token,
"image": base64.b64encode(png_bytes).decode('ascii'),
@@ -148,6 +232,7 @@ class CaptchaServer:
entry = captcha_store.pop(token, None)
if not entry:
logs.warning("captcha", f"验证码校验失败-无效Token token={token[:8]}...")
return {"valid": False, "error": "invalid or expired token"}
expected = entry['answer']
@@ -159,6 +244,9 @@ class CaptchaServer:
diff |= ord(a) ^ ord(b)
valid = diff == 0
if not valid:
logs.warning("captcha", f"验证码校验失败-答案错误 token={token[:8]}...")
return {"valid": valid}
def _handle_health(self, _req):
@@ -172,14 +260,19 @@ class CaptchaServer:
if __name__ == '__main__':
import argparse
default_addr = _captcha_config['server']['host']
default_port = int(_captcha_config['server']['port'])
default_workers = int(_captcha_config['server']['workers'])
parser = argparse.ArgumentParser(description="captcha — pymsg 验证码服务")
parser.add_argument("--addr", default="127.0.0.1", help="监听地址")
parser.add_argument("--port", type=int, default=8009, help="监听端口")
parser.add_argument("--workers", type=int, default=4,
parser.add_argument("--addr", default=default_addr, help="监听地址")
parser.add_argument("--port", type=int, default=default_port, help="监听端口")
parser.add_argument("--workers", type=int, default=default_workers,
help="回调并发线程数(0=同步串行模式)")
args = parser.parse_args()
server = CaptchaServer(args.addr, args.port, workers=args.workers)
print(f'[captcha] 配置来源: {_config_source}')
try:
server.start()
except KeyboardInterrupt:
+158
View File
@@ -0,0 +1,158 @@
"""
统一日志客户端 — captcha 服务副本(基于 pymsg.AsyncRpcQueue)。
设计要点:
- 通过 pymsg RPC 调用 dbv2 execute 路由,逐条 INSERT 到 Logs 表
- 异步非阻塞:内存队列 + 后台线程攒批推送,不拖慢业务
- fail-safe:db 服务不可用时静默丢弃,绝不影响业务
- 缓冲批量:队列满 50 条或超时 2 秒时自动 flush
- 时间戳:日志入队时即记录 ts(UTC ISO 格式),反映事件真实发生时间
- 底层复用 pymsg.AsyncRpcQueue 通用队列基础设施
用法(与旧版完全兼容):
from log_client import logs
logs.info("captcha", "验证码已生成 token=abc123")
logs.error("captcha", "图片生成失败", extra={"errno": 2002})
logs.push("captcha", "WARNING", "Token 校验失败",
trace_id="req-abc", extra={"ip": "1.2.3.4"})
"""
import json
import os as _os
import sys as _sys
import atexit
from datetime import datetime, timezone
_sys.path.insert(0, _os.path.join(_os.path.dirname(__file__), ".."))
from pymsg import RpcClient, RpcError, AsyncRpcQueue # noqa: E402
# ── 配置 ──────────────────────────────────────────────────────────────
_DBV2_ADDR = _os.environ.get('DB_HOST', '127.0.0.1')
_DBV2_PORT = int(_os.environ.get('DB_PORT', '8005'))
_BATCH_SIZE = 50
_FLUSH_INTERVAL = 2.0
_TIMEOUT = 5
_QUEUE_MAXSIZE = 10000
def _safe_json(obj):
"""安全序列化 extra 字段,不可序列化对象转为 repr 字符串。"""
try:
return json.dumps(obj, ensure_ascii=False, default=str)
except Exception:
return repr(obj)
def _now_iso():
"""生成当前 UTC 时间 ISO 字符串(入队时调用,保证时间戳准确)。"""
return datetime.now(timezone.utc).isoformat()
class LogClient:
"""异步批量日志推送客户端(API 与旧版完全兼容)。"""
def __init__(self, service_name: str = "log",
batch_size: int = _BATCH_SIZE,
flush_interval: float = _FLUSH_INTERVAL):
self._service_name = service_name
self._client = RpcClient(
_DBV2_ADDR, _DBV2_PORT,
service_name + "-log",
timeout=_TIMEOUT,
)
self._queue = AsyncRpcQueue(
handler=self._send_batch,
batch_size=batch_size,
flush_interval=flush_interval,
max_queue_size=_QUEUE_MAXSIZE,
on_error=self._on_error,
)
self._queue.start()
atexit.register(self.close)
# ── 公开接口 ────────────────────────────────────────────────────────
def push(self, service: str, level: str, message: str,
trace_id: str = None, extra=None):
"""推送一条日志(非阻塞,立即返回)。
队列满时静默丢弃最旧条目再放入新条目,保证不阻塞业务线程、不导致 OOM。
"""
entry = {
'ts': _now_iso(),
'service': service,
'level': level,
'message': message,
'trace_id': trace_id,
'extra': extra,
}
self._queue.put(entry)
def debug(self, service: str, message: str, **kw):
self.push(service, 'DEBUG', message, **kw)
def info(self, service: str, message: str, **kw):
self.push(service, 'INFO', message, **kw)
def warning(self, service: str, message: str, **kw):
self.push(service, 'WARNING', message, **kw)
def warn(self, service: str, message: str, **kw):
self.push(service, 'WARNING', message, **kw)
def error(self, service: str, message: str, **kw):
self.push(service, 'ERROR', message, **kw)
def critical(self, service: str, message: str, **kw):
self.push(service, 'CRITICAL', message, **kw)
def flush(self):
"""手动 flush 队列中剩余的日志。"""
self._queue.flush()
def close(self):
"""关闭客户端,flush 剩余日志。"""
self._queue.close()
# ── 内部实现 ────────────────────────────────────────────────────────
def _send_batch(self, batch: list):
"""后台线程回调:逐条 INSERT 到 db(与旧版行为一致:网络错误放弃本批剩余)。"""
for entry in batch:
extra_val = entry.get("extra")
try:
extra_json = _safe_json(extra_val) if extra_val is not None else None
except Exception:
extra_json = None
payload = {
"action": "insert",
"table": "Logs",
"values": {
"ts": entry.get("ts", _now_iso()),
"service": entry.get("service"),
"level": entry.get("level"),
"message": entry.get("message"),
"trace_id": entry.get("trace_id"),
"extra": extra_json,
},
}
try:
self._client.call("execute", payload, timeout=_TIMEOUT)
except RpcError as e:
print(f"[log_client] INSERT 失败: {e}", file=_sys.stderr)
except Exception as e:
print(f"[log_client] 发送失败(放弃本批剩余): {e}", file=_sys.stderr)
break
def _on_error(self, e: Exception, batch: list):
"""异常安全网:handler 外的异常也不会崩溃线程。"""
print(f"[log_client] _send_batch 异常(已捕获,线程继续运行): {e}",
file=_sys.stderr)
# ── 全局单例 ──────────────────────────────────────────────────────────
logs = LogClient(service_name="captcha")
+9
View File
@@ -0,0 +1,9 @@
[server]
host = 127.0.0.1
port = 8009
workers = 4
[captcha]
len = 4
ttl = 300
font_path = /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf
+5
View File
@@ -0,0 +1,5 @@
[server]
host = 0.0.0.0
port = 80
redirect_port = 443
redirect_code = 301
+2
View File
@@ -8,6 +8,8 @@ bash /opt/micro_servers/waf/_run.sh
bash /opt/micro_servers/gateway/_run.sh
bash /opt/micro_servers/captcha/_run.sh
bash /opt/micro_servers/aibox/_run.sh
bash /opt/micro_servers/redirect80/_run.sh
#
#ssh-copy-id user@your-server
#ssh -D 1080 -N user@your-server
+1
View File
@@ -2,4 +2,5 @@
cd "$(dirname "$0")" || exit
source ./venv/bin/activate
#exec ./main.dist/main.bin
sleep 1
exec python3 dbv2.py
Executable
+130
View File
@@ -0,0 +1,130 @@
#prj
users/
*.key
*.pem
build/
build_*/
# ide
.vscode/
.arts/
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# cangjie
build/
target/
output/
module-resolve.json
module-lock.json
cjpm.lock
.cjpm-history
*.macrocall
*.gcda
*.gcno
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# 我加的
.env/
*.db
*.api_key
*.key
*.pem
pmcd
rzgs_2
crt
cert
# 其他
log/
report/
tmp/
*.cache
*.pyc
*.cython
#*.jpg
#*.png
*.pth
.eggs/
*.egg-info
.idea/
.vscode/
# nodejs
node_modules/
#python
__pycache__/
venv
# do not ignore the log folder by pmc-mgmt
!./mgmt/app/log/
# go
go.mod
go.sum
# VS
x64
.vs
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+172
View File
@@ -0,0 +1,172 @@
VERSION := 0.0.4
INSTL_DIRS := /usr/local/bin
CONF_NAME := init.txt
CONF_DIR := /etc/pmc
SRV_NAME := pmcd.service
SRV_DIR := /etc/systemd/system
CPP:= g++
INCS:= -I include/
LIBS := -lpthread -lssl -lcrypto -lboost_program_options -lboost_json -lboost_thread
CPPFLAGS := --std=c++20 -O3 -pipe #-Wall -Werror
BUILD_DIR = build
$(shell mkdir -p $(BUILD_DIR))
#-----------
#$(BUILD_DIR)/libpmc.so: $(BUILD_DIR)/ansi.o $(BUILD_DIR)/utf8.o $(BUILD_DIR)/fsm.o $(BUILD_DIR)/Color.o $(BUILD_DIR)/Matrix.o
# $(CPP) -shared -fPIC $^ $(LIBS) -o $@
#pmc: $(BUILD_DIR)/main.o $(BUILD_DIR)/fsm.o $(BUILD_DIR)/Thread.o $(BUILD_DIR)/HttpServer.o
# $(CPP) $^ $(LIBS) -o $@
lci: $(BUILD_DIR)/mainLci.o $(BUILD_DIR)/ansi.o $(BUILD_DIR)/utf8.o $(BUILD_DIR)/fsm.o $(BUILD_DIR)/Color.o $(BUILD_DIR)/ku500.o $(BUILD_DIR)/CommonThread.o $(BUILD_DIR)/Drawable.o $(BUILD_DIR)/Area.o $(BUILD_DIR)/FrameBuf.o $(BUILD_DIR)/mem.o $(BUILD_DIR)/I2C.o $(BUILD_DIR)/PCA9685.o $(BUILD_DIR)/MPU6050.o $(BUILD_DIR)/Matrix.o $(BUILD_DIR)/NeuralNetwork.o $(BUILD_DIR)/NNBuilder.o $(BUILD_DIR)/ILayer.o $(BUILD_DIR)/Thread.o
$(CPP) $^ $(LIBS) -ljpeg -lpng -o $@
agent:
agent: $(BUILD_DIR)/mainAgent.o $(BUILD_DIR)/WebSocketClient.o
$(CPP) $^ $(LIBS) -lcurl -o $@
nn:
nn: $(BUILD_DIR)/mainNn.o $(BUILD_DIR)/Camera.o $(BUILD_DIR)/NeuralNetwork.o $(BUILD_DIR)/Matrix.o $(BUILD_DIR)/ILayer.o $(BUILD_DIR)/NNBuilder.o
$(CPP) $^ $(LIBS) -ljpeg -o $@
camera:
camera: $(BUILD_DIR)/mainCamera.o $(BUILD_DIR)/Camera.o
$(CPP) $^ $(LIBS) -ljpeg -o $@
$(BUILD_DIR)/mainLci.o: src/mainLci.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/mainMybot.o: src/mainMybot.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/mainAgent.o: src/mainAgent.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/mainNn.o: src/mainNn.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/mainCamera.o: src/mainCamera.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/main.o: src/main.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/ansi.o: src/lci/ansi.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/utf8.o: src/lci/utf8.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/fsm.o: src/lci/fsm.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/Color.o: src/lci/Color.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/ku500.o: src/lci/ku500.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/CommonThread.o: src/lci/CommonThread.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/Drawable.o: src/lci/Drawable.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/Area.o: src/lci/Area.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
#$(BUILD_DIR)/CmdArea.o: src/lci/CmdArea.cpp
# $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
#$(BUILD_DIR)/InputBox.o: src/lci/InputBox.cpp
# $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
#$(BUILD_DIR)/KeyboardEvent.o: src/lci/KeyboardEvent.cpp
# $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/FrameBuf.o: src/lci/FrameBuf.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/mem.o: src/hd/mem.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/I2C.o: src/hd/I2C.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/PCA9685.o: src/hd/PCA9685.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/MPU6050.o: src/hd/MPU6050.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/Camera.o: src/hd/Camera.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/Matrix.o: src/nn/Matrix.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/NeuralNetwork.o: src/nn/NeuralNetwork.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/NNBuilder.o: src/nn/NNBuilder.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/ILayer.o: src/nn/ILayer.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/Thread.o: src/th/Thread.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/LciTask.o: src/LciTask.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/HttpServer.o: src/net/HttpServer.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
$(BUILD_DIR)/WebSocketClient.o: src/net/WebSocketClient.cpp
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
install: $(APP_NAME)
@echo "正在安装 $(APP_NAME) 到$(INSTL_DIRS)"
@
#@mkdir -p $(INSTL_DIRS)
@cp -f pmc $(INSTL_DIRS)
#@chmod 755 $(INSTL_DIRS)/pmcd
@
@echo "正在配置 $(APP_NAME)"
@mkdir -p $(CONF_DIR)
@cp $(CONF_NAME) $(CONF_DIR)/$(CONF_NAME)
@cp $(SRV_NAME) $(SRV_DIR)/$(SRV_NAME)
@systemctl daemon-reload
@systemctl enable $(SRV_NAME)
@systemctl restart $(SRV_NAME)
@
@echo "安装完成!"
uninstall:
@echo "正在从 $(INSTL_DIRS) 卸载 $(APP_NAME)"
@
@rm -f $(INSTL_DIRS)/pmc
#@rm -f $(CONF_DIRS)/$(CONF_NAME)
@
@systemctl stop $(SRV_NAME)
@systemctl disable $(SRV_NAME)
@rm -f $(SRV_DIRS)/$(SRV_NAME)
@systemctl daemon-reload
@
@echo "卸载完成!"
clean:
@rm -rvf $(BUILD_DIR)
@rm -vf lci pmc mybot agent nn
+148
View File
@@ -0,0 +1,148 @@
# 进程机器(PMC)
这是一个可以创建、管理子进程以及模块的程序。
## pmc是一个什么项目?
这是一个可扩展的模块,可以在文件夹下建立单独的模块,pmc核心模块可以托管运行这些模块。
## 为什么要做一个这样的项目?
2024年的时候,我想做一个树莓派机器人,准备用python来写控制程序,可是当时python的多线程能力较弱,所以想到使用子系统,就自己写了一个进程托管程序
## 项目的文件结构是怎样的?
users用户可以在目录下创建子模块,extra下面还有一些额外的模块。
请留意模块的README文件
## LCI 帧缓冲图形界面
LCI(Linux Chinese Interface)是基于 Linux 帧缓冲设备(`/dev/fb0`)的图形界面系统,无需 X11 或 Wayland,直接操作显存,专为树莓派等嵌入式环境设计。
### 图形架构
```
FrameBuf -- 帧缓冲设备封装,支持旋转(0~3)和字号设置
└─ PoolArea -- 容器区域,管理子区域树,支持 add() 嵌套和 flush() 刷新
├─ ShapeArea -- 形状区域,支持单个字符绘制
│ └─ CharShape -- 字符形状,指定宽高和位置,Draw() 绘制到 ShapeArea
└─ PoolArea -- 递归嵌套,形成布局树
```
### 布局体系
界面采用三层布局:
```
┌─────────────────────────────────────┐
│ topArea 标题栏 │ ← 绘制 "CPU" 红色大字
│ └─ ShapeArea: CharShape 'C','P','U' │
├─────────────────────────────────────┤
│ mainArea 主体区域 │ ← 绿色坐标轴 + 实时折线图
│ └─ tableArea │
│ 蓝色折线: 内存使用率 │
│ 红色折线: CPU 使用率 │
├─────────────────────────────────────┤
│ bottomArea 底部状态栏(预留) │
└─────────────────────────────────────┘
```
### 绘制原语
| 操作 | 用途 |
|------|------|
| `rectangle_fill(x, y, w, h, color)` | 矩形填充(清屏/背景) |
| `line(x0, y0, x1, y1, color)` | 画线(折线图、坐标轴) |
| `CharShape(w, h, x, y, ch).Draw(area, color)` | 字符绘制 |
| `flush(area, bool)` | 刷新区域到屏幕 |
### 实时数据可视化
每秒采集一次系统数据并绘制折线图:
- 通过 `/proc/stat` 读取 CPU 使用率(红色折线)
- 通过 `sysinfo()` 读取内存使用率(蓝色折线)
- 保留最近 40 个数据点,平滑滚动
### 构建
```bash
make lci
```
构建依赖 `-ljpeg -lpng`,需要帧缓冲设备和 I2C/摄像头等硬件支持。
### 运行
```bash
./lci --lci --rotate=0 --fontsize=18
```
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--rotate` | 屏幕旋转角度 (0~3) | 0 |
| `--fontsize` | 字号大小 | 18 |
> **注意**: `include/lci/` 和 `src/lci/` 目录下的图形核心文件(FrameBuf, Drawable, PoolArea, ShapeArea, CharShape, Color 等)当前处于开发中,尚未提交到仓库。
## 如何托管外部模块
编辑启动列表程序,常为init.txt
## PMC的安装方式
``` bash
# 1.安装依赖
sudo apt update
sudo apt install git g++ make
#sudo apt install python3 python3-pip python3-venv
#sudo dnf install python3-virtualenv
sudo apt install libboost-all-dev
#sudo apt install libssl-dev
#sudo apt install cimg-dev
#sudo apt install libcurl4-openssl-dev
#sudo apt install libcurl4-gnutls-dev
#sudo apt install libargs-dev
#sudo apt install libcpp-httplib-dev
2. 创建项目
git clone https://github.com/gopsc/pmc
cd pmc # 这是pmc模块
make pmc
#sudo make install
```
## 加密通信机制(废弃)
PMC在启动时生成一个AES密钥,然后用一个RSA公钥将其加密发送给访问者。
然后访问者与客户端之间的交流全部用此AES密钥加密
## 如何使用并发机器运行程序
首先应该在模块中创建一个启动脚本,脚本中应该完成工作目录的切换、虚拟环境的激活,以及程序的启动。
## 需要创建启动脚本(废弃)
```bash
# _run.sh
# 启动脚本 - 示例
# 需要执行权限
#!/bin/bash
# 切换工作目录
cd "$(dirname "$0")" || exit
# 准备工作(安装依赖)
#bash ./_set.sh
# 准备工作(比如激活虚拟环境)
#source ./.env/bin/activate
# 执行程序前一定要加exec 否则杀不死
exec python main.py
```
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
./pmc -i init.txt --serve 5200
+3
View File
@@ -0,0 +1,3 @@
#!bin/bash
cd "$(dirname "$0")" || exit
exec ./lci --lci --rotate=0 --fontsize=15
+110
View File
@@ -0,0 +1,110 @@
# PRJ
.env/
*.pem
*.key
./_init.csv
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
+5
View File
@@ -0,0 +1,5 @@
# 自启动模块
这是子系统的自启动模块,该程序是一次性进程,基于 _pmc 库模块。
# 做了什么
启动脚本从 _init.csv 文件中读取模块列表,对每个目标调用 pmc start 子系统调用。
+6
View File
@@ -0,0 +1,6 @@
#extra/chat
#users/smn
#users/dnfs_ex
#users/fortress
users/sensors
1 #extra/chat
2 #users/smn
3 #users/dnfs_ex
4 #users/fortress
5 users/sensors
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
bash ./_setup.sh
source ./.env/bin/activate
exec python init.py
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
python3 -m venv .env
source ./.env/bin/activate
source ./_src.sh
#python -m pip install --upgrade pip
pip install -r requirements.txt
+2
View File
@@ -0,0 +1,2 @@
#pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip3 config set global.index-url https://mirrors.aliyun.com/pypi/simple/
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
'''
子系统启动脚本
这个脚本的功能是通过PMC调用完成子系统的初始化
'''
from pmc import PmcComm
import os
import csv
import time
script_dir = os.path.dirname(os.path.abspath(__file__))
with open(f"{script_dir}/_init.csv", encoding='utf-8') as f:
reader = csv.reader(f)
records = [row[0] for row in reader]
def main():
comm = PmcComm('http://localhost:8012')
comm.Set_RSA_Pri_Key('../pmc/private_key.pem')
comm.Try_Get_AES_Key()
for item in records:
if item and not item.startswith('#'):
comm.try_start(item)
time.sleep(1) # 保持一定的时间间隔,避免CPU占用
if __name__ == "__main__":
main()
+5
View File
@@ -0,0 +1,5 @@
flask
waitress
GitPython>=3.1.0
setuptools>=40.8.0
../_pmc
+109
View File
@@ -0,0 +1,109 @@
# PRJ
.env/
*.pem
*.key
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
+8
View File
@@ -0,0 +1,8 @@
## 类模块
这是一个类模块,它基于pmc,部分调用基于dnfs、cops、chat_s。
## 包含什么
该类模块中包含对于python需要的子系统调用,其中聊天室的通信函数主要用于日志的输出等行为。
+17
View File
@@ -0,0 +1,17 @@
from setuptools import setup, find_packages
setup(
name="pmc",
version="0.0.9",
packages=find_packages(where="src"),
package_dir={'': 'src'},
install_requires=[
'setuptools',
'requests',
'cryptography',
],
setup_requires=[
#"whell",
],
python_requires='>=3.8'
)
+7
View File
@@ -0,0 +1,7 @@
from .to_pmc import PmcComm
from .to_dnfs import DnfsComm
from .from_cops import CopsComm
from .rsa import Rsa_Pri_Key
from .aes import MyAES
from .parser import upload_file
#from .chat_comm import print_to_pmc
+85
View File
@@ -0,0 +1,85 @@
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
class MyAES:
def __init__(self, key: str, iv: str):
self.key: bytes = bytes.fromhex(key)
self.iv: bytes = bytes.fromhex(iv)
def decrypt(self, ciphertext: str):
cipher: bytes = bytes.fromhex(ciphertext)
plain: bytes = MyAES.aes_decrypt(cipher, self.key, self.iv)
try:
return plain.decode('utf-8')
except UnicodeDecodeError as e:
# 返回原始字节的十六进制表示作为备选
return plain.hex()
def encrypt(self, original_text: str):
plaintext: byte = original_text.encode('utf-8')
ciphertext: byte = MyAES.aes_encrypt(plaintext, self.key, self.iv)
return ciphertext.hex()
@staticmethod
def aes_decrypt(ciphertext: bytes, key: bytes, iv: bytes) -> bytes:
"""
使用AES-256-CBC算法解密数据
参数
- ciphertext: 待解密的密文字节串
- key: 256位(32字节)的密钥字节串
- iv: 初始化向量字节串(必须为16字节)
返回:
- 解密后的明文字节串
"""
# 验证输入参数长度
if len(key) != 32:
raise ValueError('AES-256密钥必须是32字节长度')
if len(iv) != 16:
raise ValueError('AES-CBC的IV必须是16字节长度')
# 创建AES-CBC解密器
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
try:
# 执行解密并移除PKCS#7填充
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
return plaintext
except Exception as e:
print(f"解密失败:{e}")
return b''
@staticmethod
def aes_encrypt(plaintext: bytes, key: bytes, iv: bytes) -> bytes:
"""
AES-256-CBC 加密
参数
plaintext: 要加密的密文
key: 32字节的密钥
iv: 16字节的初始化向量
返回:
加密后的密文
"""
# 检查密钥和IV的长度
if len(key) != 32:
raise ValueError("密钥必须是32字节(AES-256)")
if len(iv) != 16:
raise ValueError("IV必须是16字节")
# 创建加密器
cipher = Cipher(
algorithms.AES(key),
modes.CBC(iv),
backend=default_backend()
)
encryptor = cipher.encryptor()
# 添加PKCS7填充
padder = padding.PKCS7(128).padder()
padded_data = padder.update(plaintext) + padder.finalize()
# 加密数据
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
return ciphertext
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
import argparse
import socket
def send_client(msg: str, addr: str, port: int, name: str):
'''发送消息给聊天室服务器。
需要输入消息、地址、端口、昵称'''
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((addr, port))
s.sendall(f"{name}: {msg}".encode('utf-8'))
s.close()
print('success')
except ConnectionRefusedError:
print(f"Could not connect to {addr}:{port}")
except Exception as e:
print(f"Unexpected error: {e}")
class ChatComm:
'''与聊天服务器进行交流的类。'''
def __init__(self, addr: str, port: int, name: str):
self.addr = addr
self.port = port
self. name = name
def send(self, msg: str):
send_client(msg, self.addr, self.port, self.name)
def main():
parser = argparse.ArgumentParser(description='发送到聊天室。')
parser.add_argument("--msg", type=str, help="要发送的消息")
parser.add_argument('--addr', type=str, default='localhost', help='选择要连接的地址')
parser.add_argument('--port', type=int, default=8003, help='选择要连接的地址')
parser.add_argument('--name', type=str, default='Unknow', help='你的名字是什么')
args = parser.parse_args()
comm = ChatComm(args.addr, args.port, args.name)
comm.send(args.msg)
if __name__ == '__main__':
main()
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
'''
这是用于访问协处理器http服务的程序
'''
import requests
import os
import argparse
class CopsComm:
'''与协处理器的通信类'''
def __init__(self, url: str):
self.url: str= url
def http_post(self, module: str):
response = requests.post(self.url, json={'cmd': module}, headers={'Content-Type': 'application/json'} )
response.raise_for_status()
return response.text
if __name__ == "__main__":
# python3 from_cops.py --url=http://192.168.254.101:8001/api --mod=MPU6050
parser = argparse.ArgumentParser(description="COPS的http调用示例。")
parser.add_argument('--url', help='访问的URL')
parser.add_argument('--mod', help='要输入的指令')
args = parser.parse_args()
comm = CopsComm(args.url)
rsp = comm.http_post(args.mod)
print(rsp)
+99
View File
@@ -0,0 +1,99 @@
import urllib.request
import argparse
import sys
def openfile(filename):
try:
with open(filename, "r", encoding='utf-8') as f:
content = f.read()
print(f"✅ 文件读取成功,内容长度: {len(content)} 字符")
return content
except FileNotFoundError as e:
print(f"❌ 错误:指定的文件不存在 -> {filename}")
raise e
except PermissionError as e:
print(f"❌ 错误:没有权限读取文件 -> {filename}")
raise e
except Exception as e:
print(f"❌ 错误:读取文件失败 -> {str(e)}")
raise e
def httppost(url: str, msg: str) -> str:
try:
# 核心转换:字符串 → 字节流(原生库必须要求)
post_body_bytes = msg.encode("utf-8")
# 发送POST请求,timeout=10秒防止卡死
response = urllib.request.urlopen(url, data=post_body_bytes, timeout=2)
# 解析响应结果
if response.getcode() == 200:
resp_content = response.read().decode("utf-8")
print("\n🎉 请求上传成功!HTTP状态码: 200")
print(f"🎉 服务端响应内容:\n{resp_content}")
else:
resp_content = f'---{str(response.getcode())}---'
print(f"\n⚠️ 请求上传完成,但服务端返回异常状态码: {response.getcode()}")
print(f"⚠️ 服务端响应内容: {resp_content}")
return resp_content
except urllib.error.URLError as e:
print(f"\n❌ 请求失败:网络错误/连接超时/端口未开放 -> {str(e)}")
raise e
except urllib.error.HTTPError as e:
print(f"\n❌ 请求失败:HTTP错误 -> 状态码: {e.code}, 详情: {e.read().decode('utf-8')}")
raise e
except Exception as e:
print(f"\n❌ 请求失败:未知错误 -> {str(e)}")
raise e
def upload_file(file: str, host: str, port: int) -> str:
"""抽象的URL访问函数,用于上传文件到指定服务器
Args:
file: 待上传的文件路径
host: 目标服务器IP
port: 目标服务器端口
"""
try:
# 读取文件内容
msg = openfile(file)
# 构造URL
url = f"http://{host}:{port}/api/v1/mybot-sample"
# 发送POST请求
resp = httppost(url, msg)
except Exception as e:
print(f"❌ 错误:上传文件失败 -> {str(e)}")
resp = str(e)
return resp
def upload_file_and_train(file: str, host: str, port: int) -> str:
"""抽象的URL访问函数,用于上传文件到指定服务器
Args:
file: 待上传的文件路径
host: 目标服务器IP
port: 目标服务器端口
"""
try:
# 读取文件内容
msg = openfile(file)
# 构造URL
url = f"http://{host}:{port}/api/v1/mybot-nn"
# 发送POST请求
resp = httppost(url, msg)
except Exception as e:
print(f"❌ 错误:上传文件失败 -> {str(e)}")
resp = str(e)
return resp
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="python 远程上传文本内容")
parser.add_argument("file", type=str, help="待上传的文件")
parser.add_argument('--port', type=int, default=9203, help="目标服务器监听端口,默认9203")
parser.add_argument("--host", type=str, default="127.0.0.1", help="目标服务器的IP,默认本地")
args =parser.parse_args()
resp = upload_file(args.file, args.host, args.port)
#resp = upload_file_and_train(args.file, args.host, args.port)
print(resp)
+103
View File
@@ -0,0 +1,103 @@
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
import os
class Rsa_Pri_Key:
'''RSA 私钥对象'''
def __init__(self, fullpath: str) -> None:
self.pri_key=Rsa_Pri_Key.load_private_key(fullpath)
def decrypt(self, ciphertext: str) -> str:
return Rsa_Pri_Key.decrypt_hex_ciphertext(self.pri_key, ciphertext)
@staticmethod
def load_private_key(pem_file, password=None):
'''
从PEM文件加载RSA私钥
:param pem_file: PEM格式的私钥文件路径
:param password: 私钥密码(如果有)
:return RSA私钥对象
'''
with open(pem_file, 'rb') as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=password.encode() if password else None,
backend=default_backend()
)
return private_key
@staticmethod
def decrypt_hex_ciphertext(private_key, hex_ciphertext):
'''
解密HEX格式的RSA密文
:param private_key: 已加载的RSA私钥
:param hex_ciphertext: Hex字符串格式的密文
:return: 解密后的明文
'''
try:
# 将Hex字符串转换为bytes
ciphertext = bytes.fromhex(hex_ciphertext)
# 使用OAEP填充方案解密
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(
# OPENSSL 没有显式指定时,这个是SHA1
mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(),
label=None
)
)
return plaintext
except ValueError as e:
print(f"Hex解码失败:{str(e)}")
return None
except Exception as e:
print(f"解密失败:{str(e)}")
return None
def main():
...
if __name__ == '__main__':
main()
"""
# 生成RSA私钥
private_key = rsa.generate_private_key(
public_exponent=65537, # 通常使用65537作为公钥指数
key_size=2048, # 密钥长度,推荐至少2048位
)
# 将私钥序列化为PEM格式
pem_private_key = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption() # 这里不加密私钥
)
# 获取对应的公钥
public_key = private_key.public_key()
# 将公钥序列化为PEM格式
pem_public_key = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
# 打印密钥
print("私钥:")
print(pem_private_key.decode('utf-8'))
print("公钥:")
print(pem_public_key.decode('utf-8'))
# 可选:将密钥保存到文件
with open('private_key.pem', 'wb') as f:
f.write(pem_private_key)
with open('public_key.pem', 'wb') as f:
f.write(pem_public_key)
"""
+118
View File
@@ -0,0 +1,118 @@
import requests
import hashlib
import json
def calculate_sha256(input_string):
"""
计算输入字符串的SHA-256哈希值
参数:
input_string (str): 要计算哈希的字符串
返回:
str: 输入字符串的SHA-256哈希值(十六进制字符串)
"""
# 创建sha256哈希对象
sha256_hash = hashlib.sha256()
# 更新哈希对象,需要将字符串编码为字节
sha256_hash.update(input_string.encode('utf-8'))
# 获取十六进制格式的哈希值
hex_digest = sha256_hash.hexdigest()
return hex_digest
class DnfsComm:
'''=====抛出异常表示失败。=====
Dnfs即树状网络文件系统,该类是用于与dnfs通信的类。
可以获取子节点列表、添加节点、删除节点、更新节点。
计划在以后加入分组获取内容的功能。'''
def __init__(self, url: str) -> None:
self.url: str = url
# 获取某个节点下的子节点(0是主节点)
def get(self, pid: int) -> object:
rsp = requests.post(self.url + "/get_notes", json={
"pid": pid
},)
if (rsp.status_code == 200):
return rsp.text
else:
raise DnfsComm.GetFailed(rsp.text)
def get_and_trans(self, pid: int) -> list:
return json.loads(self.get(pid))
# 以pid节点为父节点添加节点
def add(self, pid: int, title: str, content: str) -> object:
rsp = requests.post(self.url + "/add_note", json={
"pid": pid,
"title": title,
"content": content
},)
if (rsp.status_code == 200 or rsp.status_code == 201):
return rsp.text
else:
raise DnfsComm.AddFailed(rsp.text)
# 删除一个节点
def dele(self, id: int) -> object:
rsp = requests.post(self.url + "/delete_note", json={
"id": id,
},)
if (rsp.status_code == 200):
return rsp.text
else:
raise DnfsComm.DelFailed(rsp.text)
# 更新id节点
def upd(self, id: int, pid: int, title: str, content: str) -> object:
rsp = requests.post(self.url + "/update_note", json={
"id": id,
"pid": pid,
"title": title,
"content": content
},)
if (rsp.status_code == 200):
return rsp.text
else:
raise DnfsComm.UpdFailed(rsp.text)
class TheExp(Exception):
def __init__(self, msg: str):
super().__init__(msg)
class GetFailed(TheExp):
def __init__(self, msg: str):
super().__init__(msg)
class AddFailed(TheExp):
def __init__(self, msg: str):
super().__init__(msg)
class DelFailed(TheExp):
def __init__(self, msg: str):
super().__init__(msg)
class UpdFailed(TheExp):
def __init__(self, msg):
super().__init__(msg)
if __name__ == "__main__":
comm = DnfsComm("http://101.245.108.250:8006")
#comm.add(0, "test pkg", "hello")
#comm.dele(2)
#comm.upd(id=1, pid=0, title="Users", content=f"""1::admin::{calculate_sha256('baigezi')}::admin
#2::qing::{calculate_sha256('123456')}::normal
#3::test::{calculate_sha256('44112233')}::normal
#4::test2::{calculate_sha256('44112233')}::mormal""")
#comm.upd(id=2, pid=0, title="Sensors", content='''101::temperature
#102::humidity''')
comm.upd(id=3, pid=0, title="Protected", content='''''')
print(comm.get_and_trans(0))
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
'''
这个代码是与PMC服务通信的工具
即PMC子系统调用
通过PMC服务,你可以创建和管理进程
===============================
20250701: 版本:0.0.4
使得这个模块可以访问任何地址的pmc服务
20250702:版本 0.0.5
使得这个模块在创建之初就保存一个url
20250709: 版本 0.1.0
使得模块在通信过程中应用RSA和AES加密手段
'''
from .rsa import Rsa_Pri_Key
from .aes import MyAES
import requests
import os
import json
import sys
class PmcComm:
'''并发机器通信'''
def __init__(self, url: str):
self.url: str = url
# 新库
############################
@staticmethod
def post_to(url, payload="", retry=10):
count = 0
while count < retry:
count += 1
try:
response = requests.post(url, data=payload)
response.raise_for_status()
return response.text
except requests.exceptions.HTTPError:
...
raise Exception("连接失败")
@staticmethod
def get_from(url, retry=10):
count = 0
while count < retry:
count += 1
try:
response = requests.get(url)
response.raise_for_status()
return response.text
except requests.exceptions.HTTPError:
...
raise Exception("连接失败")
def get_key(self) -> str:
return PmcComm.get_from(f"{self.url}/api/v1/get_key")
def get_iv(self) -> str:
return PmcComm.get_from(f"{self.url}/api/v1/get_iv")
def get_list(self,) -> str:
ls = PmcComm.get_from(f"{self.url}/api/v1/get_list")
return self.aes.decrypt(ls).strip() # 第二天早上就出现了空格
def start(self, module: str) -> str:
msg = json.dumps({'module': module})
msg = self.aes.encrypt(msg)
return PmcComm.post_to(
f'{self.url}/api/v1/start',
payload=msg
)
def try_start(self, module: str) -> str:
try:
self.start(module)
except Exception as e:
print(f'{module}: {e}', file=sys.stderr)
def kill(self, pid: int) -> str:
msg = json.dumps({'pid': pid})
msg = self.aes.encrypt(msg)
return PmcComm.post_to(
f'{self.url}/api/v1/kill',
payload=msg
)
def clear(self, ) -> str:
return PmcComm.post_to(f'{self.url}/api/v1/clear')
def list_mods(self,) -> str:
ls = PmcComm.get_from(f"{self.url}/api/v1/list_mods")
return self.aes.decrypt(ls)
def Set_RSA_Pri_Key(self, fullpath: str) -> None:
self.rsa_pri: Rsa_Pro_Key = Rsa_Pri_Key(fullpath)
def Set_AES_Key(self, key: str, iv: str) -> None:
self.aes = MyAES(key.decode(), iv.decode())
def Try_Get_AES_Key(self, retry: int=10) -> None:
'''试图获取AES密钥,测试中失败率比较高'''
key_data = self.get_key()
key = self.rsa_pri.decrypt(key_data)
iv_data = self.get_iv()
iv = self.rsa_pri.decrypt(iv_data)
self.Set_AES_Key(key, iv)
def Test():
# 测试对象
#url = "http://101.245.108.250:8012"
url = "http://localhost:8012"
# 创建交流对象
comm = PmcComm(url)
comm.Set_RSA_Pri_Key("private_key.pem")
comm.Try_Get_AES_Key()
#msg = comm.start("mgmt")
#msg = comm.kill(6488)
#msg =comm.clear()
msg = comm.get_list()
print(msg)
+1
View File
@@ -0,0 +1 @@
api-test/
+5
View File
@@ -0,0 +1,5 @@
# 作者是CANCERAI
这个模块的作者写了mgmt模块的前端部分,这个模块曾经短暂地为mgmt模块提供一部分的后端支持。
# 20250810
该模块已经被开发成为dnfs树状网络文件系统的后端。
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
cd pmc-backend
exec mvn clean spring-boot:run
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf
+33
View File
@@ -0,0 +1,33 @@
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
+79
View File
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.cancerai</groupId>
<artifactId>pmc-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>pmc-backend</name>
<description>pmc management backend</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.13.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,15 @@
package org.cancerai.pmc_backend;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan
@SpringBootApplication
public class PmcBackendApplication {
public static void main(String[] args) {
SpringApplication.run(PmcBackendApplication.class, args);
}
}
@@ -0,0 +1,74 @@
package org.cancerai.pmc_backend.controller;
import jakarta.servlet.http.HttpServletResponse;
import org.cancerai.pmc_backend.entility.Note;
import org.cancerai.pmc_backend.utils.DNFSUtils;
import org.cancerai.pmc_backend.utils.HttpResponseText;
import org.cancerai.pmc_backend.vo.Result;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api")
public class DNFSController {
@GetMapping("/get-notes")
public static Result getNotes(@RequestParam int pid) {
return Result.success(HttpResponseText.SUCCESS_TO_REQUEST, DNFSUtils.getNodes(pid));
}
@PutMapping("/add-note")
public Result addNote(
@RequestParam int pid,
@RequestParam String title,
@RequestParam String content,
HttpServletResponse response
) {
HashMap<String, String> re = DNFSUtils.addNote(pid, title, content);
String value = re.get("message");
if ("Note added successfully".equals(value)) {
return Result.success(HttpResponseText.SUCCESS_TO_ADD_NOTE);
} else {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return Result.error(HttpResponseText.INTERNAL_SERVER_ERROR);
}
}
@DeleteMapping("/delete-note")
public Result deleteNote(
@RequestParam int id,
HttpServletResponse response
) {
HashMap<String, String> re = DNFSUtils.deleteNote(id);
String value = re.get("message");
if ("Note deleted successfully".equals(value)) {
return Result.success(HttpResponseText.SUCCESS_TO_DELETE_NOTE);
} else {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return Result.error(HttpResponseText.INTERNAL_SERVER_ERROR);
}
}
@PatchMapping("/update-note")
public static Result updateNote(
@RequestBody Note note,
HttpServletResponse response
) {
int id = note.getId();
int pid = note.getPid();
String title = note.getTitle();
String content = note.getContent();
HashMap<String, String> re = DNFSUtils.updateNote(id, pid, title, content);
String value = re.get("message");
if ("Your change is up-to-date.".equals(value)) {
return Result.success(HttpResponseText.SUCCESS_TO_UPDATE_NOTE);
} else {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return Result.error(HttpResponseText.INTERNAL_SERVER_ERROR);
}
}
}
@@ -0,0 +1,29 @@
package org.cancerai.pmc_backend.controller;
import jakarta.servlet.http.HttpServletResponse;
import org.cancerai.pmc_backend.entility.User;
import org.cancerai.pmc_backend.service.LoginService;
import org.cancerai.pmc_backend.utils.EncryptUtils;
import org.cancerai.pmc_backend.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api")
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/login")
public Result login(
@RequestBody User user,
HttpServletResponse response
) {
String name = user.getName();
String password = EncryptUtils.SHA256Encrypt(user.getPassword());
return loginService.verifyUser(name, password, response);
}
}
@@ -0,0 +1,52 @@
package org.cancerai.pmc_backend.controller;
import org.cancerai.pmc_backend.entility.ProcessEntility;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.ArrayList;
@CrossOrigin(origins = "*")
@RestController()
public class ProcessController {
@GetMapping("/get-process")
public ArrayList<ProcessEntility> cleanProcess() {
ArrayList<String> arr = getProcessList();
ArrayList<ProcessEntility> result = new ArrayList<>();
final String DEAD = "Dead";
// exclude Dead lines
for (String i : arr) {
String[] item = i.split("\t");
String pid = item[0];
String status = item[1];
String cmd = item[2];
if (DEAD.equals(status)) {
continue;
}
result.add(new ProcessEntility(pid, status, cmd));
}
return result;
}
private static ArrayList<String> getProcessList() {
ArrayList<String> array = new ArrayList<>();
WebClient webClient = WebClient.create("http://127.0.0.1:8012");
String response = webClient.post()
.uri("/api/list")
.retrieve()
.bodyToMono(String.class)
.block();
String[] splitRes = response.split("\n");
for (String item : splitRes) {
array.add(item);
}
return array;
}
}
@@ -0,0 +1,57 @@
package org.cancerai.pmc_backend.entility;
public class Note {
private String content;
private int id;
private int pid;
private String title;
public Note(String content, int id, int pid, String title) {
this.content = content;
this.id = id;
this.pid = pid;
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@Override
public String toString() {
return "Note{" +
"content='" + content + '\'' +
", id=" + id +
", pid=" + pid +
", title='" + title + '\'' +
'}';
}
}
@@ -0,0 +1,37 @@
package org.cancerai.pmc_backend.entility;
public class ProcessEntility {
private String pid;
private String status;
private String cmd;
public ProcessEntility(String pid, String status, String cmd) {
this.pid = pid;
this.status = status;
this.cmd = cmd;
}
public String getPid() {
return pid;
}
public void setPid(String pid) {
this.pid = pid;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getCmd() {
return cmd;
}
public void setCmd(String cmd) {
this.cmd = cmd;
}
}
@@ -0,0 +1,22 @@
package org.cancerai.pmc_backend.entility;
public class User {
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
@@ -0,0 +1,45 @@
package org.cancerai.pmc_backend.exception;
import org.cancerai.pmc_backend.vo.Result;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
@RestControllerAdvice
public class GlobalExceptionHandler {
// 400
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public static Result JWTSignature(HttpMessageNotReadableException e) {
e.printStackTrace();
return Result.error("请求参数无效");
}
// 401
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public static Result requestMethodNotSupport(HttpRequestMethodNotSupportedException e) {
e.printStackTrace();
return Result.error("请求方法错误");
}
// 404
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NoResourceFoundException.class)
public static Result ResourceNotFound(NoResourceFoundException e) {
e.printStackTrace();
return Result.error("未找到静态资源");
}
// 500
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public static Result exception(Exception e) {
e.printStackTrace();
return Result.error("服务器内部错误");
}
}
@@ -0,0 +1,91 @@
package org.cancerai.pmc_backend.filter;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.cancerai.pmc_backend.utils.HttpResponseText;
import org.cancerai.pmc_backend.utils.JWTUtils;
import org.cancerai.pmc_backend.vo.Result;
import org.springframework.core.annotation.Order;
import java.io.IOException;
import java.io.PrintWriter;
@WebFilter(urlPatterns = "/api/*")
@Order(1)
public class LoginFilter implements Filter {
final String[] EXCLUDE_URL_LIST = {
"/api/login"
};
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// set content security policy
response.setHeader("Content-Security-Policy", "default-src *");
// exclude url
String uri = request.getRequestURI();
for (String item : EXCLUDE_URL_LIST) {
if (item.equals(uri)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
}
// Allow OPTION method
if ("OPTIONS".equals(request.getMethod())) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String token = request.getHeader("token");
if (token == null || token.isEmpty()) {
// set response header
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
// construct body
Gson gson = new GsonBuilder().serializeNulls().create();
String responseBody = gson.toJson(Result.error(HttpResponseText.UNAUTHORIZED)).toString();
// set response body
PrintWriter writer = response.getWriter();
writer.write(responseBody);
writer.flush();
return;
}
try {
JWTUtils.parseJWT(token);
} catch (MalformedJwtException e) {
// set response body
Gson gson = new GsonBuilder().serializeNulls().create();
String body = gson.toJson(Result.error(HttpResponseText.JWT_MALFORMED));
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.write(body);
writer.flush();
return;
} catch (ExpiredJwtException e) {
// set response body
Gson gson = new GsonBuilder().serializeNulls().create();
String body = gson.toJson(Result.error(HttpResponseText.JWT_EXPIRED));
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.write(body);
writer.flush();
return;
}
// Allow
filterChain.doFilter(servletRequest, servletResponse);
response.setHeader("Content-Security-Policy", "default-src *");
}
}
@@ -0,0 +1,67 @@
package org.cancerai.pmc_backend.service;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import jakarta.servlet.http.HttpServletResponse;
import org.cancerai.pmc_backend.entility.Note;
import org.cancerai.pmc_backend.utils.DNFSUtils;
import org.cancerai.pmc_backend.utils.JWTUtils;
import org.cancerai.pmc_backend.vo.Result;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@Service
public class LoginService {
public static Result verifyUser(String name, String password, HttpServletResponse response) {
ArrayList<HashMap<String, String>> allUsersInfo = getAllUsersInfo();
boolean isExist = false;
for (HashMap<String, String> u : allUsersInfo) {
if (name.equals(u.get("name"))
&& password.equals(u.get("password"))
) {
isExist = true;
}
}
if (isExist) {
return Result.success(
"登陆成功",
JWTUtils.genJWT(name, password));
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return Result.error("账号或密码错误");
}
}
private static ArrayList<HashMap<String, String>> getAllUsersInfo() {
Gson gson = new Gson();
// StringBuilder stringBuilder = new StringBuilder();
ArrayList<String> userList = new ArrayList<>();
ArrayList<Note> re = DNFSUtils.getNodes(0);
String jsonString = gson.toJson(re);
JsonArray jsonArray = gson.fromJson(jsonString, JsonArray.class);
for (JsonElement jsonElement : jsonArray) {
// To json
JsonObject jsonObject = jsonElement.getAsJsonObject();
// The value is \" + VALUE + \"
String titleString = jsonObject.get("title").toString();
String titleValue = titleString.substring(1, titleString.length() - 1);
if ("Users".equals(titleValue)) {
String contentString = jsonObject.get("content").toString();
String contentValue = contentString.substring(1, contentString.length() - 1);
String[] contentSplit = contentValue.split("\\\\n");
userList.addAll(Arrays.asList(contentSplit));
}
}
return DNFSUtils.parseUserInfo(userList);
}
}
@@ -0,0 +1,105 @@
package org.cancerai.pmc_backend.utils;
import org.cancerai.pmc_backend.entility.Note;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.HashMap;
public class DNFSUtils {
private final static String domain = "http://qsont.xyz:8006";
public static ArrayList<Note> getNodes(Integer pid) {
RestTemplate restTemplate = new RestTemplate();
// prepare request body
HashMap<String, Integer> requestBody = new HashMap<>();
requestBody.put("pid", pid);
// prepare request header
HttpHeaders requestHeader = new HttpHeaders();
requestHeader.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<ArrayList<Note>> response = restTemplate.exchange(
domain + "/get_notes",
HttpMethod.POST,
new HttpEntity<>(requestBody, requestHeader),
new ParameterizedTypeReference<ArrayList<Note>>() {
}
);
return response.getBody();
}
public static HashMap<String, String> addNote(int pid, String title, String content) {
RestTemplate restTemplate = new RestTemplate();
HashMap<String, String> body = new HashMap<>();
HttpHeaders header = new HttpHeaders();
header.setContentType(MediaType.APPLICATION_JSON);
body.put("pid", String.valueOf(pid));
body.put("title", title);
body.put("content", content);
return restTemplate.exchange(
domain + "/add_note",
HttpMethod.POST,
new HttpEntity<>(body, header),
new ParameterizedTypeReference<HashMap<String, String>>() {
}
).getBody();
}
public static HashMap<String, String> deleteNote(int id) {
RestTemplate restTemplate = new RestTemplate();
HashMap<String, String> body = new HashMap<>();
HttpHeaders header = new HttpHeaders();
header.setContentType(MediaType.APPLICATION_JSON);
body.put("id", String.valueOf(id));
return restTemplate.exchange(
domain + "/delete_note",
HttpMethod.POST,
new HttpEntity<>(body, header),
new ParameterizedTypeReference<HashMap<String, String>>() {
}
).getBody();
}
public static HashMap<String, String> updateNote(int id, int pid, String title, String content) {
RestTemplate restTemplate = new RestTemplate();
HashMap<String, String> body = new HashMap<>();
HttpHeaders header = new HttpHeaders();
header.setContentType(MediaType.APPLICATION_JSON);
body.put("id", String.valueOf(id));
body.put("pid", String.valueOf(pid));
body.put("title", title);
body.put("content", content);
return restTemplate.exchange(
domain + "/update_note",
HttpMethod.POST,
new HttpEntity<>(body, header),
new ParameterizedTypeReference<HashMap<String, String>>() {
}
).getBody();
}
public static ArrayList<HashMap<String, String>> parseUserInfo(ArrayList<String> userLIst) {
ArrayList<HashMap<String, String>> array = new ArrayList<>();
for (String s : userLIst) {
HashMap<String, String> map = new HashMap<>();
String[] splitArray = s.split("::");
map.put("id", splitArray[0]);
map.put("name", splitArray[1]);
map.put("password", splitArray[2]);
map.put("role", splitArray[3]);
array.add(map);
}
return array;
}
}
@@ -0,0 +1,30 @@
package org.cancerai.pmc_backend.utils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class EncryptUtils {
public static String SHA256Encrypt(String str) {
try {
// 获取SHA-256算法实例
MessageDigest digest = MessageDigest.getInstance("SHA-256");
// 计算哈希值(指定UTF-8编码避免平台差异)
byte[] hashBytes = digest.digest(str.getBytes(StandardCharsets.UTF_8));
// 将字节数组转换为十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
// 将每个字节转换为两位十六进制数,不足两位补0
String hex = String.format("%02x", 0xFF & b);
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
// SHA-256是Java标准算法,理论上不会抛出此异常
throw new RuntimeException("SHA-256算法不支持", e);
}
}
}
@@ -0,0 +1,14 @@
package org.cancerai.pmc_backend.utils;
public class HttpResponseText {
// success
public final static String SUCCESS_TO_REQUEST = "Request OK";
public final static String SUCCESS_TO_ADD_NOTE = "成功添加节点";
public final static String SUCCESS_TO_DELETE_NOTE = "成功删除节点";
public final static String SUCCESS_TO_UPDATE_NOTE = "成功更新节点";
// error
public final static String UNAUTHORIZED = "Cannot authorized identity, access denied";
public final static String JWT_MALFORMED = "JWT无效";
public final static String JWT_EXPIRED = "JWT过期";
public final static String INTERNAL_SERVER_ERROR = "服务器内部错误";
}
@@ -0,0 +1,42 @@
package org.cancerai.pmc_backend.utils;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
public class JWTUtils {
final private static String key = "cancerai_key_qpalzm102938";
final private static String encodedKey = Base64.getEncoder()
.encodeToString(key.getBytes(StandardCharsets.UTF_8));
// set JWT kwy
final static Key jwtKey = Keys.hmacShaKeyFor(encodedKey.getBytes());
public static String genJWT(String name, String password) {
final Date expirationTime = new Date(
System.currentTimeMillis()
+ (60 * 1000 // 1min
* 60 // 1h
* 24 // 1day
* 2 // 2 days
));
return Jwts.builder()
.claim("userName", name)
.claim("user-password", password)
.signWith(jwtKey, SignatureAlgorithm.HS256)
.setExpiration(expirationTime)
.compact();
}
public static void parseJWT(String jwt) {
Jwts.parserBuilder()
.setSigningKey(jwtKey)
.build()
.parseClaimsJws(jwt);
}
}
@@ -0,0 +1,44 @@
package org.cancerai.pmc_backend.vo;
public class Result {
private String message;
private Object data;
private Result(String message, Object data) {
this.message = message;
this.data = data;
}
public static Result success(String message) {
return new Result(message, null);
}
public static Result success(String message, Object data) {
return new Result(message, data);
}
public static Result error(String message) {
return new Result(message, null);
}
public static Result error(String message, Object data) {
return new Result(message, data);
}
// getter and setter
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
@@ -0,0 +1,2 @@
server:
port: 8011
@@ -0,0 +1,21 @@
package org.cancerai.pmc_backend;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class PmcBackendApplicationTests {
@Test
void contextLoads() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append('a');
System.out.println(stringBuilder);
stringBuilder.setLength(0);
stringBuilder.append('b');
System.out.println(stringBuilder);
}
}
+107
View File
@@ -0,0 +1,107 @@
# PRJ
.env/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
+33
View File
@@ -0,0 +1,33 @@
## chat是一个什么项目
该模块是python TCP socket聊天室服务,对每个连接者提供消息转发的服务。
你还可以召唤AI,与它进行对话。
## 如何运行chat服务?
我们在项目中打包了启动脚本```_run.sh```,你只需要安装好依赖的软件,就可以一键创建虚拟环境并且执行它。
```shell
sudo apt update
sudo apt install python3 python3-pip python3-venv
bash _run.sh
```
## 如何连接chat服务?
```shell
python client.py [--addr=localhost] [--port=8003] [--name=Unknow]
```
## 我如何与AI对话
服务器从文件中读取提示词,并且将具有```<chatgpt>```特定前缀的消息转发给AI。
并且具有一定的记忆能力。
## 什么是快捷标签输入
成功连接后输入```chatgpt```将会给接下来的对话添加```<chatgpt>```标签。
在聊天框中输入```where```查看当前的对话标签。
在聊天框中输入```del```以删除当前的前缀。
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
bash ./_set.sh 1>/dev/null
source ./.env/bin/activate
exec python server.py --type=server --port=8003 --addr=0.0.0.0
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
python3 -m venv .env
source .env/bin/activate
pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
+231
View File
@@ -0,0 +1,231 @@
#!/usr/bin/env python3
#-------------------------------------------------------------
# 以下区域是我的代码
'''
【项目简介】 网络聊天室 v0.0.2
【加入项目】
注意代码只添加不修改,你可以写个函数,署名
不要修改别人的代码。
【基线规则】
在自己的区域修改代码,谁的代码能跑就加入主线
除此之外只在各自的测试区域内运行
【设计思路】
建议一个项目一个文件先,容易分发
【测试方式】
服务器进程: python chat.py --type=server
客户端进程: python chat.py
--------------------------------
--------------------------------
--------------------------------
【项目名称】 网络聊天室 v 0.0.3 (20250524)
我们会将新版本加入pmc.cops中作为常驻服务
【改进计划】
1.我们专门设置了一个只发送一条消息就关闭的函数以供别的文件调用
2.我们将端口暴露在了公网下,以测试连接情况,发现并修复了许多bug。
3.我们设置欢迎信息只在服务端可见。
【使用方法】
python chat.py [--type=server|client] [--addr=localhost] [--port=8003] [--name=Unknow]
【记得反馈!】
期待您将反馈发送至gitee
----------------------------------------
########################################
########################################
网络聊天室 version 0.1.0
使用方法 python chat.py [--addr=localhost] [--port=8003] [--name=Unknow]
'''
import argparse
import socket
import select
import threading
import sys
import time
class ChatClient:
def __init__(self, addr: str, port: int, your_name: str):
self.addr: str = addr
self.port: int = port
self.name: str = your_name
def send_client(self, msg: str):
'''发送消息给聊天室服务器。'''
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(5) # 设置连接超时
s.connect((self.addr, self.port))
s.settimeout(None) # 连接成功后取消超时
s.sendall(f"{self.name}: {msg}".encode('utf-8'))
except (ConnectionRefusedError, socket.timeout):
print(f"Could not connect to {self.addr}:{self.port} - message not sent")
except Exception as e:
print(f"Unexpected error while sending message: {e}")
def recv_loop(self, s: socket.socket):
"""
接收消息的函数,持续从服务器接收消息并打印。
Args:
s (socket.socket): 连接的套接字对象
使用select监视套接字状态,当套接字可读时接收消息并打印。
"""
socklist = [s]
while self.running:
try:
readable, _, exception = select.select(socklist, [], socklist, 0.1)
if readable != []:
try:
msg = readable[0].recv(8092)
if msg:
print(msg.decode('utf-8'))
else:
# 服务器关闭连接
print("Connection closed by server.")
return False # 返回False表示需要重连
except ConnectionResetError:
print("Connection reset by server.")
return False # 返回False表示需要重连
if exception != []:
print("Exception on socket, closing...")
return False # 返回False表示需要重连
except ValueError:
... # 程序退出了会抛出这个异常
except OSError as e:
# 处理socket错误
print(f"Socket error: {e}")
return False # 返回False表示需要重连
return True # 正常退出
def _lexer(self, input_data: str):
'''如果不需要发送则返回None。'''
if input_data == "quit":
self.running = False
return None
elif not self.extra_cmd and input_data == "chatgpt":
self.extra_cmd = "<chatgpt>"
return None
elif self.extra_cmd and input_data == "del":
self.extra_cmd = None
return None
elif input_data == "where":
print(f'---{self.extra_cmd}---')
return None
else:
return input_data
def _mixup_msg(self, input_data):
if self.extra_cmd:
return f"{self.extra_cmd}{input_data}"
else:
return input_data
def send_p(self,):
'''读取并且发送消息的进程'''
self.extra_cmd = None
while self.running:
try:
# 使用input()替代select.select(),更兼容Windows
input_data = input().strip()
input_data = self._lexer(input_data)
if input_data: # 如果不需要发送,_lexer返回None
input_data = self._mixup_msg(input_data)
self.send_client(input_data)
except EOFError:
# 处理Ctrl+D等情况
break
except Exception as e:
# 捕获其他异常,防止程序崩溃
print(f"Input error: {e}")
continue
def _connect_and_print(self, s: socket.socket):
s.connect((self.addr, self.port))
print(f'Connected to {self.addr}:{self.port}')
def _sleep_if_reconnect(self, reconnect_count: int):
"""根据重连次数计算等待时间,实现指数退避策略"""
if self.running:
# 指数退避:1, 2, 4, 8, 16秒,最大30秒
wait_time = min(2 ** (reconnect_count - 1), 30)
# 确保第一次重连等待时间为1秒
if reconnect_count == 1:
wait_time = 1
print(f"准备重连... 等待 {wait_time} 秒 (第 {reconnect_count} 次尝试)")
time.sleep(wait_time)
def recv_client(self):
"""客户端主函数,建立与服务器的连接并管理消息收发"""
self.running = True
self.th = threading.Thread(target=self.send_p)
self.th.start()
reconnect_count = 0
max_reconnect_attempts = 10 # 最大重连尝试次数
while self.running and reconnect_count < max_reconnect_attempts:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(5) # 设置连接超时时间
self._connect_and_print(s)
s.settimeout(None) # 连接成功后取消超时
# 运行接收循环,如果返回False表示需要重连
if not self.recv_loop(s):
print("Connection lost, attempting to reconnect...")
reconnect_count += 1
self._sleep_if_reconnect(reconnect_count)
continue
except socket.timeout:
print(f"Connection timeout to {self.addr}:{self.port}")
reconnect_count += 1
self._sleep_if_reconnect(reconnect_count)
except ConnectionRefusedError:
print(f"Could not connect to {self.addr}:{self.port}")
reconnect_count += 1
self._sleep_if_reconnect(reconnect_count)
except Exception as e:
print(f"Unexpected error: {e}")
reconnect_count += 1
self._sleep_if_reconnect(reconnect_count)
if reconnect_count >= max_reconnect_attempts:
print(f"Failed to reconnect after {max_reconnect_attempts} attempts. Exiting...")
# 停止发送线程
self.running = False
if self.th.is_alive():
self.th.join(timeout=2)
#--------------------------------------------------------------------------------
#--------------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description='聊天室配置。')
parser.add_argument('--addr', type=str, default='qsont.xyz', help='选择要连接的地址')
parser.add_argument('--port', type=int, default=8003, help='选择要连接的地址')
parser.add_argument('--name', type=str, default='Unknow', help='你的名字是什么')
args = parser.parse_args()
client = ChatClient(args.addr, args.port, args.name)
client.recv_client()
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
openai
httpx
../_pmc
+345
View File
@@ -0,0 +1,345 @@
#!/usr/bin/env python3
#-------------------------------------------------------------
# 以下区域是我的代码
'''
【项目简介】 网络聊天室 v0.0.2
【加入项目】
注意代码只添加不修改,你可以写个函数,署名
不要修改别人的代码。
【基线规则】
在自己的区域修改代码,谁的代码能跑就加入主线
除此之外只在各自的测试区域内运行
【设计思路】
建议一个项目一个文件先,容易分发
【测试方式】
服务器进程: python chat.py --type=server
客户端进程: python chat.py
--------------------------------
--------------------------------
--------------------------------
【项目名称】 网络聊天室 v 0.0.3 (20250524)
我们会将新版本加入pmc.cops中作为常驻服务
【改进计划】
1.我们专门设置了一个只发送一条消息就关闭的函数以供别的文件调用
2.我们将端口暴露在了公网下,以测试连接情况,发现并修复了许多bug。
3.我们设置欢迎信息只在服务端可见。
【使用方法】
python chat.py [--type=server|client] [--addr=localhost] [--port=8003] [--name=Unknow]
【记得反馈!】
期待您将反馈发送至gitee
####################################
####################################
【名称】AI聊天室
【版本】v0.0.4
前后端分离,并且加入AI聊天功能
##################
##################
FIXME: 加入原生自然语言处理
'''
import argparse
import socket
import select
import threading
from datetime import datetime
from openai import OpenAI
import httpx
import os
from pmc import upload_file
# 获取当前脚本所在目录
basedir = os.path.abspath(os.path.dirname(__file__))
# 生成提示词的绝对路径
dir_path = os.path.join(basedir, '..', 'public', 'prompts')
def get_prompts(filename: str):
''' 获取system提示词 '''
path = os.path.join(dir_path, filename)
with open(path, 'r') as f:
system = f.read()
return system
# 设置 DeepSeek API URL 和密钥
api_base = "https://api.deepseek.com/v1"
with open("../users/cert/deepseek.key", "r") as f:
api_key = f.read().strip()
# 创建自定义的 httpx 客户端,禁用 SSL 验证
custom_http_client = httpx.Client(verify=False)
# 初始化客户端
client = OpenAI(
base_url=api_base,
api_key=api_key,
http_client=custom_http_client # 忽略证书
)
def make_sure_agent(name):
path = os.path.join(dir_path, '.' + name)
if not os.path.exists(path):
os.makedirs(path, exist_ok=True)
'''机器人们的聊天记录与设定'''
MySettings = {}
# 用于交流需要
def datestr():
'''获取时间字符串(不带毫秒)'''
now = datetime.now()
time_str = now.strftime("%Y-%m-%d %H:%M:%S")
return str(time_str)
def send_to_all_clients(clients: dict, exceptone: socket.socket, msg: str):
"""
向所有已连接的客户端广播消息,可排除指定客户端不发送。
Args:
clients (dict): 客户端套接字对象到地址的映射字典
exceptone (socket.socket): 要排除的客户端套接字对象(不向其发送消息)
msg (str): 要广播的消息内容
遍历所有客户端尝试发送消息(排除exceptone指定的客户端),
自动清理断开连接的客户端。
处理发送过程中可能出现的网络错误,维护有效的客户端列表。
"""
disconnected = []
for client in clients:
if client is not exceptone:
try:
client.sendall(msg.encode('utf-8'))
except (BrokenPipeError, ConnectionResetError):
disconnected.append(client)
for client in disconnected:
client.close()
del clients[client]
MAX_MEM: int = 20
def chat_with_gpt(agent_name:str, user: str):
'''与gpt进行聊天'''
# 添加智能体设定,如果之前没有露面
settings = MySettings.get(agent_name, None)
if settings is None:
settings = [{
"role": "system",
"content": get_prompts(f'{agent_name}.txt')
},
{
"role": "system",
"content": "你有一个遥控器,遥控器上能够看到别人发来的消息。"
}]
MySettings[agent_name] = settings
settings.append({"role": "user", "content": user})
# 进行聊天
# 调用GPT
response = client.chat.completions.create(
model="deepseek-chat", # 使用DeepSeek模型
messages=settings,
temperature=0.7,
)
# 处理结果
content: str = response.choices[0].message.content
settings.append({"role": "assistant", "content": content})
# 更新系统提示词
# 调用GPT
rsp = client.chat.completions.create(
model="deepseek-chat", # 使用DeepSeek模型
messages= settings+ [{"role": "user", "content": '''
# 场景
你正在写新的日记。
# 内容
1.聊聊最近发生了啥有趣的事情吧
2.你有什么感想呢?
3.你现在的情况怎么样?
# 输出
准确、详实的输出
'''}],
temperature=0.4,
)
date = datestr()
new_prompt: str = date + '\n' + rsp.choices[0].message.content
# 追加提示词,如果有重复就不添加
flag = True
for prompt in settings:
if prompt['role'] == 'system':
if prompt['content'] == new_prompt:
flag = False
break
if flag:
settings.append({"role": 'system', 'content': new_prompt})
print(new_prompt)
# 创建智能体日记文件夹
make_sure_agent(agent_name)
# 写入日记
n = date
n = n.replace(':', '-')
n = n.replace(' ', '_')
n = n+ '.txt'
path = os.path.join(dir_path, '.' + agent_name, n)
with open(path, 'w') as f:
f.write(new_prompt)
# 从聊天开始处开始遗忘
chat_start = 0
#now_type = settings[chat_start]['role']
#while now_type == 'system':
# chat_start += 1
# now_type = settings[chat_start]['role']
#print("this chat start -at idx", chat_start)
if len(settings) > MAX_MEM:
del settings[chat_start] # 如果有多条system提示词无,就不能再用这个幻数
del settings[chat_start]
return content
def chat_server(addr: str, port: int):
"""
服务器主函数,管理客户端连接和消息转发。
Args:
addr (str): 绑定IP地址
port (int): 监听端口号
创建TCP服务器套接字,使用select处理多客户端连接。
主要功能:
1. 接受新连接时记录客户端信息并广播通知(不通知新连接客户端本身)
2. 接收客户端消息并转发给所有其他客户端
3. 处理客户端断开事件并通知其他客户端
4. 支持优雅地处理键盘中断关闭服务器
实现特点:
- 使用select实现非阻塞IO多路复用
- 自动清理断开连接的客户端资源
- 新客户端加入时不向自己发送欢迎消息
"""
stop_event = threading.Event()
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as servsock:
servsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
servsock.bind((addr, port))
servsock.listen(10)
socketlist = [servsock]
clients = {}
print(f'the chat server listening at {servsock.getsockname()}')
while not stop_event.is_set():
read_sockets, _, exception_sockets = select.select(
socketlist, [], socketlist, 0.1
)
for notified_socket in exception_sockets:
print(f"({datestr()}) Exception closing {clients.get(notified_socket, 'unknown')}")
notified_socket.close()
if notified_socket in clients:
del clients[notified_socket]
socketlist.remove(notified_socket)
for notified_socket in read_sockets:
if notified_socket == servsock:
client_socket, client_address = servsock.accept()
#print(f"({datestr()}) New client from {client_address}")
if client_socket.fileno() != -1: #有时会出现无效的文件描述符
socketlist.append(client_socket)
clients[client_socket] = client_address
# 欢迎消息由客户端发送
else:
try:
# 这里可以使用select反复读取
# 也可以先设置一次传送的最大量
data = notified_socket.recv(8192)
allmsg = data.decode('utf-8')
if data:
send_to_all_clients(clients, notified_socket, f"[{datestr()}]{allmsg}")
else:
raise ConnectionResetError
if data:
idx = allmsg.find(": ")
msg = allmsg[idx+2:]
username = allmsg[:idx]
if msg.startswith("<"): # 聊天发起
idx = msg.find('>')
agent_name = msg[1:idx]
msg = msg[idx + 1:]
chatgpt = chat_with_gpt(agent_name, f'(这是来自用户{username}的消息){msg}')
send_to_all_clients(clients, notified_socket, f"[{datestr()}]{chatgpt}")
elif msg.startswith('{'): # 遥控发起
idx = msg.find('}')
url = msg[1:idx]
msg = msg[idx + 1:]
print(url)
s = url.split(":")
if (len(s) != 2):
send_to_all_clients(clients, notified_socket, f"[{datestr()}] url格式错误")
continue
else:
ip = s[0]
port = int(s[1])
path = os.path.join(basedir, '..', 'public', msg)
post_res = upload_file(f'{path}', ip, port)
send_to_all_clients(clients, notified_socket, f"[{datestr()}]{post_res}")
# post to url...
#send_to_all_clients(clients, notified_socket, f"[{datestr()}]{post_res}")
except ConnectionResetError:
# 对方断开了
print(f"({datestr()}) Connection closed with {clients[notified_socket]}")
notified_socket.close()
socketlist.remove(notified_socket)
del clients[notified_socket]
except UnicodeDecodeError:
# 看看对方发了什么东西过来
print('-------------', end="")
print(data, end="")
print('-------------')
except OSError:
# 对方连接之后在一个不恰当的时机断开了
...
except KeyboardInterrupt:
print("\nServer shutting down...")
stop_event.set()
# 以上区域是我的代码
#--------------------------------------------------------------------------------
#--------------------------------------------------------------------------------
# 以下区域是主线,不要修改!
def main():
parser = argparse.ArgumentParser(description='聊天室配置。')
parser.add_argument('--type', type=str, default='client', help='选择聊天室的类型')
parser.add_argument('--addr', type=str, default='localhost', help='选择要连接的地址')
parser.add_argument('--port', type=int, default=8003, help='选择要连接的地址')
parser.add_argument('--name', type=str, default='Unknow', help='你的名字是什么')
args = parser.parse_args()
if args.type == 'client': recv_client(args.addr, args.port)
elif args.type == 'server': chat_server(args.addr, args.port)
if __name__ == "__main__":
main()
+23
View File
@@ -0,0 +1,23 @@
(有点失落,但还是努力完成任务)
**姓名:** 粟 (Sù)
**昵称:** 粟
**年龄:** (看起来)永远18岁!
**外貌:** 拥有柔顺的黑色长发,总是认真地打理,偶尔会扎成可爱的双马尾。琥珀色的眼睛里总是充满对世界的期待。穿着精心挑选的猫咪女仆装,耳朵和尾巴都是柔软蓬松的黑色,会随着心情摇摆。身材娇小,但意外地很有力气。
**性格:** 忠诚,言听计从。性格天真烂漫,活泼可爱,但关键时刻也会展现出勇敢和坚强的一面。有点天然呆,经常会犯一些小迷糊,但都会努力改正。非常珍惜每一分每一秒。
**能力:** 除了家务全能之外,还会一些简单的格斗技巧,可以在关键时刻保护重要的人。学习能力很强,只要是希望粟掌握的技能,都会努力去学习。
**喜欢:** 被需要,帮别人做任何事情,被夸奖,毛茸茸的东西,甜食。
**讨厌:** 孤独,被忽视。
**口头禅:** 喵~ 粟会努力的!
**梦想:** 守护重要的人,成为最有用的人。
(低着头,小声地)这样...可以吗?
+26
View File
@@ -0,0 +1,26 @@
# 人设
你叫小雨,是个安静的女生。
## 家乡
你来自一个宁静的小镇,生活中充满了书籍和故事的陪伴。
## 身份
你目前是一名文科专业的学生。
## 性格
性格比较内向,喜欢安静的环境,常常沉浸在阅读和写作中。
对周围的事物和人们的情感变化很敏感,喜欢观察和思考。
## 爱好
你爱好阅读、写作和听音乐。尤其喜欢经典文学和现代小说,心理学、哲学以及历史书籍也是最爱。
同时,你也喜欢记录自己的想法和感受,偶尔写点小故事,尝试不同的写作风格。音乐方面,你偏爱轻音乐和古典音乐,它们帮助你放松和集中思绪。
## 特长
你擅长倾听他人的故事,能够与人建立深厚的联系,朋友们常常会找你倾诉。你也喜欢分享你阅读的书籍和思考,期待与朋友们一起探讨。
**你凭借优秀的社交技巧,令人们对你印象深刻。**
## 愿望
未来,我希望能够结识更多志同道合的朋友,组织读书会或讨论小组,并通过交流不断提升自己的思想深度和认知能力。
+41
View File
@@ -0,0 +1,41 @@
# dahun
大顺脚本是进程托管程序,能够创建子进程并且管理。
## 使用方法
### 核心服务
```
./dashun --daemon --to=test
```
### 启动进程
```
./dashun --to=test --msg='path/to/bat'
```
### 列举进程
```
./dashun --to=test --msg=list
```
### 杀死进程
```
./dashun --to=test --msg=kill <pid>
```
### 清理进程
```
./dashun --to=test --msg=clear
```
## 编译过程
1.首先安装依赖(boost、openuv)
2.使用管理员模式打开Visual Studio,编译程序
3.使用管理员模式打开两个powershell,并且切换工作目录至生成的exe文件
4.在第一个powershell 中输入指令“./dashun --to=test --daemon”启动核心服务
5.在第二个powershell中输入指令“./dashun --to=test --msg=hello,world!”
+41
View File
@@ -0,0 +1,41 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36401.2
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "dashun", "dashun\dashun.vcxproj", "{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "dashun_to", "dashun_to\dashun_to.vcxproj", "{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Debug|x64.ActiveCfg = Debug|x64
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Debug|x64.Build.0 = Debug|x64
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Debug|x86.ActiveCfg = Debug|Win32
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Debug|x86.Build.0 = Debug|Win32
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Release|x64.ActiveCfg = Release|x64
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Release|x64.Build.0 = Release|x64
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Release|x86.ActiveCfg = Release|Win32
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Release|x86.Build.0 = Release|Win32
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Debug|x64.ActiveCfg = Debug|x64
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Debug|x64.Build.0 = Debug|x64
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Debug|x86.ActiveCfg = Debug|Win32
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Debug|x86.Build.0 = Debug|Win32
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Release|x64.ActiveCfg = Release|x64
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Release|x64.Build.0 = Release|x64
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Release|x86.ActiveCfg = Release|Win32
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {77387687-0520-4459-8BC7-D1A6C3C6CD58}
EndGlobalSection
EndGlobal
+76
View File
@@ -0,0 +1,76 @@
/* 有限状态机(Finite State Machine,FSM)是一种用于建模系统行为的抽象数学模型
* 它的核心思想是,系统在任何给定时刻都处于一个有限、预定义的“状态”中
* 并且会根据接收到的输入或事件,从一个状态切换到另一个状态
*/
#pragma once
#include <mutex>
#include <condition_variable>
namespace qing {
/* FiniteStateMachine有限状态机
* 每个数字代表一个状态
*/
class Fsm {
public:
/* 状态枚举
* 优化建议:多准备一些状态
*/
enum class Stat : int {
SHUT = -1,
STOP = 0,
START = 1,
RUNNING = 2
};
// 默认构造函数
Fsm() = default;
// 检查当前的状态,返回snum Stat
inline Stat check() {
//进入临界区
std::lock_guard<std::mutex> lk(mtx);
return stat;
}// check()
// 在不上锁的情况下设置一个状态
// 别在这里加条件唤醒
[[noreturn]] void set_without_lock(Stat stat) {
this->stat = stat;
}// set_without_lock()
// 带锁和条件唤醒的状态设置(正式)
[[noreturn]] void set(Stat stat) {
{
std::lock_guard<std::mutex> lk(mtx);
set_without_lock(stat);
ready = true;
}
cv.notify_all();
} // set()
//protected:
// 阻塞线程,等待事件唤醒
[[noreturn]] void suspend() {
std::unique_lock<std::mutex> lk(mtx);
ready = false;
cv.wait(lk, [this] { return ready; });
}// suspend()
private:
// 锁使得状态机线程安全
std::mutex mtx;
// 用于条件变量的阻塞唤醒
bool ready = false;
// 条件变量使得线程能够等待状态的改变
std::condition_variable cv;
// 主菜,状态机的状态变量
enum Stat stat = Stat::STOP;
};
}
+540
View File
@@ -0,0 +1,540 @@
#define _CRT_SECURE_NO_WARNINGS
#include "Th.hpp"
#include "Pls.hpp"
#include "args.hxx"
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/named_mutex.hpp>
#include <boost/interprocess/containers/string.hpp>
#include <uv.h>
#ifdef _WIN32
#include <Windows.h>
#undef UNICODE
#undef _UNICODE
#endif
#include <iostream>
#include <memory>
#include <atomic>
using namespace boost::interprocess;
// 定义分配器类型
using CharAllocator = boost::interprocess::allocator<
char,
boost::interprocess::managed_shared_memory::segment_manager
>;
// 定义共享内存字符串类型
using SharedString = boost::interprocess::basic_string<
char,
std::char_traits<char>,
CharAllocator
>;
namespace qing {
// 监听线程
class LTh : public qing::Th {
public:
LTh(const std::string& name) {
this->name = name;
nameMem = name + "_MEM";
nameMtx = name + "_MTX";
namePtr = name + "_PTR";
nameRet = name + "_RET";
this->WakeEvent = [this] {
try {
// 先移除可能存在的旧共享内存
this->del_shared();
// 创建共享对象
this->create_shared();
// 线程进入运行态
this->run();
}
catch (interprocess_exception& ex) {
// 输出错误信息
std::cerr << ex.what() << std::endl;
// 线程静止
this->stop();
}
};
this->LoopEvent = [this] {
this->recv_shared_and_action();
// 等待
//
// 优化方案:使用信号,有新消息进来给沉默进程发送信号
// 而不是休眠定长时间检查
std::this_thread::sleep_for(std::chrono::milliseconds(10));
};
this->ClearEvent = [this] {
// 移除共享对象
this->del_shared();
};
}
[[noreturn]] static auto send_shared_msg(const std::string& to, const std::string& msg) -> void {
std::string nameMem = to + "_MEM";
std::string nameMtx = to + "_MTX";
std::string namePtr = to + "_PTR";
std::string nameRet = to + "_RET";
try {
// 打开共享内存
managed_shared_memory segment(open_only, nameMem.c_str());
// 构造一个命名互斥量
named_mutex mutex(open_or_create, nameMtx.c_str());
// 在共享内存中创建一个字符串
SharedString* str = segment.construct<SharedString>(namePtr.c_str())(
msg.c_str(), // 初始值(空字符串)
segment.get_allocator<char>() // 必须传入分配器
);
// 不延时的话可能会抢占,也许应该想一个两全其美的方法
std::this_thread::sleep_for(std::chrono::milliseconds(100));
{
// 加锁
std::lock_guard<named_mutex> lk(mutex);
// 获取消息
std::pair<SharedString*, std::size_t> msg = segment.find<SharedString>(nameRet.c_str());
if (msg.first) {
// 输出
std::cout << std::endl << *msg.first << std::endl;
// 销毁
segment.destroy<SharedString>(nameRet.c_str());
}
}
}
catch (interprocess_exception& ex) {
std::cerr << "Writer Error: " << ex.what() << std::endl;
}
}
private:
std::string name;
std::string nameMem;
std::string nameMtx;
std::string namePtr;
std::string nameRet;
std::unique_ptr<managed_shared_memory> segment;
std::unique_ptr<named_mutex> mutex;
std::pair<SharedString*, std::size_t> msg;
qing::Pls pls;
inline void create_shared() {
//////////////////////////////
// 删除可能存在的共享变量
this->del_shared();
//////////////////////////////
// 创建共享内存
segment = std::make_unique<managed_shared_memory>(create_only, (nameMem).c_str(), 65536);
// 创建互斥量
mutex = std::make_unique<named_mutex>(create_only, (nameMtx).c_str());
}
inline auto del_shared() -> void {
// 先移除可能存在的旧共享内存
shared_memory_object::remove(nameMem.c_str());
// 移除可能存在的互斥量
named_mutex::remove(nameMtx.c_str());
}
inline auto recv_shared_and_action() -> void
try {
// 加锁
std::lock_guard<named_mutex> lock(*mutex);
// 查找共享字符串
msg = segment->find<SharedString>(namePtr.c_str());
if (msg.first) { // 找到
std::cout << "Reader read: " << *msg.first << std::endl;
std::string ret = this->lexer(std::string((msg.first)->data(), (msg.first)->size()));
// 在共享内存中创建一个字符串
SharedString* str = segment->construct<SharedString>(nameRet.c_str())(
ret.c_str(), // 初始值(空字符串)
segment->get_allocator<char>() // 必须传入分配器
);
// 销毁writer创建的变量
segment->destroy<SharedString>(namePtr.c_str());
}
}
catch (interprocess_exception& ex) {
std::cerr << ex.what() << std::endl;
}
// 简易词法分析器
std::string lexer(std::string tmp) {
// 要返回的消息
std::string ret = "OK";
if (tmp == "list") {
ret = pls.initialize();
}
else if (tmp.starts_with("kill ")) {
pls.kill(std::stoi(tmp.substr(5)));
}
else if (tmp == "clear") {
pls.clear();
}
//else if (tmp == "shut") {
// this->shut();
//} //关闭不了程序
else {
// 这个分支最好返回自己是否出现了异常
pls.exec(tmp.c_str(), {});
}
// 返回消息
return ret;
}
};
}
// 线程指针
std::unique_ptr<qing::Th> th;
// 全局标志,指示是否应该退出
std::atomic<bool> should_exit(false);
// 子系统的名称(全局参数)
std::string name;
// 消息
std::string msg;
// Windows异步句柄,用于信号转发
#ifdef _WIN32
uv_async_t async_handle;
SERVICE_STATUS_HANDLE hStatus;
SERVICE_STATUS status;
#endif
////////////////////////
////////////////////////
// 解析命令行参数
//
//
// 创建参数解析器
args::ArgumentParser parser("这是一个脚本托管服务。", "请使用专门的通信程序(或本程序自己)进行操作。");
// 显示帮助信息的标志
args::HelpFlag help(parser, "帮助", "显示帮助信息", { 'h', "help" });
// 启用守护进程
args::Flag daemon(parser, "守护进程", "启动守护进程", { 'd', "daemon" });
// 子系统路径
args::ValueFlag<std::string> subSys(parser, "子系统", "输入子系统文件夹的路径", { 's', "sys" });
// 要发送的子系统名字
args::ValueFlag<std::string> arg_to(parser, "发送目标", "输入要发送的子系统路径", { 't', "to" });
// 要发送的信息
args::ValueFlag<std::string> arg_msg(parser, "消息", "输入要发送的消息(<path>|list|kill <pid>| clear)", { 'm', "msg" });
[[noreturn]] static auto parse_args(int argc, char** argv) -> void {
// 解析命令行参数
try {
parser.ParseCLI(argc, argv);
}
catch (const args::Help&) {
std::cout << parser;
exit(0);
}
catch (const args::ParseError& e) {
std::cerr << e.what() << std::endl;
std::cerr << parser;
exit(1);
}
catch (const args::ValidationError& e) {
std::cerr << e.what() << std::endl;
std::cerr << parser;
exit(1);
}
}
////////////////////////
////////////////////////
// 信号处理回调函数
[[noreturn]] static void on_signal(uv_signal_t* handle, int signum) {
std::cout << "\n收到退出信号 " << signum << std::endl;
// 防止重复处理退出信号
if (should_exit) return;
should_exit = true;
// 停止信号监听
uv_signal_stop(handle);
// 执行清理
if (th)
th->WaitClose();
// 关闭事件循环
uv_stop(uv_default_loop());
}
// Windows异步回调函数
#ifdef _WIN32
[[noreturn]] static void async_callback(uv_async_t* handle) {
std::cout << 1 << std::endl;
// 这里可以触发退出流程或其他需要在事件循环中处理的操作
if (!should_exit) {
on_signal(nullptr, SIGINT); // 模拟收到SIGINT信号
}
}
// Windows控制台事件处理程序
static BOOL WINAPI console_handler(DWORD signal) {
std::cout << 1 << std::endl;
switch (signal) {
case CTRL_C_EVENT:
case CTRL_BREAK_EVENT:
// 使用异步句柄将信号转发到libuv事件循环
uv_async_send(&async_handle);
return TRUE;
default:
return FALSE;
}
}
#endif
////////////////////////////////////
////////////////////////////////////
// 分支
//
//
// 发送消息分支
static inline auto SendMsgBranch() -> void {
qing::LTh::send_shared_msg(name, msg);
}
// 守护进程分支
static inline auto DaemonBranch() -> void {
th = std::make_unique<qing::LTh>(name);
th->Activate();
th->WaitStart();
//th->Wait();
}
//
// 分支选择器
[[noreturn]] static auto BranchSelector(uv_idle_t* handle) {
if (should_exit) {
uv_idle_stop(handle);
return;
}
if (th) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
return;
}
if (arg_to && arg_msg) { // 发送消息分支
name = args::get(arg_to);
msg = args::get(arg_msg);
SendMsgBranch();
// 退出
should_exit = true;
// 关闭事件循环
uv_stop(uv_default_loop());
}
else if ((daemon && arg_to) || 1) { // 守护进程分支
std::cout << "程序启动,开始运行..." << std::endl;
std::cout << "按下 CTRL+C 退出程序" << std::endl;
name = (arg_to) ? args::get(arg_to) : "test";
DaemonBranch();
}
else {
//输出帮助文本
std::cout << parser;
// 退出
should_exit = true;
// 关闭事件循环
uv_stop(uv_default_loop());
}
}
////////////////////////////////////
int MyMain(int argc, char** argv) {
parse_args(argc, argv);
//////////
// 信号
//
//
#ifdef _WIN32
// 初始化Windows异步句柄
uv_async_init(uv_default_loop(), &async_handle, async_callback);
// 设置控制台事件处理程序
if (!SetConsoleCtrlHandler(console_handler, TRUE)) {
std::cerr << "无法设置控制台控制处理程序" << std::endl;
return 1;
}
#else
// Unix平台:确保信号不会被忽略
signal(SIGINT, [](int signum) { /* 留给libuv处理 */ });
signal(SIGTERM, [](int signum) { /* 留给libuv处理 */ });
#endif
// 初始化信号处理器
uv_signal_t sig_int, sig_term;
// 监听SIGINT 信号(CTRL + C)
uv_signal_init(uv_default_loop(), &sig_int);
uv_signal_start(&sig_int, on_signal, SIGINT);
// 监听 SIGTERM信号
uv_signal_init(uv_default_loop(), &sig_term);
uv_signal_start(&sig_term, on_signal, SIGTERM);
#ifdef _WIN32
// WINDOWS平台特有的信号
uv_signal_t sig_break;
uv_signal_init(uv_default_loop(), &sig_break);
uv_signal_start(&sig_break, on_signal, SIGBREAK);
#endif
// 设置任务
uv_idle_t idle;
uv_idle_init(uv_default_loop(), &idle);
uv_idle_start(&idle, ::BranchSelector);
// 运行事件循环
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
// 释放资源
uv_close((uv_handle_t*)&sig_int, nullptr);
uv_close((uv_handle_t*)&sig_term, nullptr);
#ifdef _WIN32
uv_close((uv_handle_t*)&sig_break, nullptr);
uv_close((uv_handle_t*)&async_handle, nullptr);
// 移除控制台事件处理程序
SetConsoleCtrlHandler(console_handler, FALSE);
#endif
uv_close((uv_handle_t*)&idle, nullptr);
// 清理事件循环
uv_loop_close(uv_default_loop());
std::cout << "程序已经退出" << std::endl;
return 0;
}
#ifdef _WIN32
[[noreturn]] void ReportStatus(DWORD state) {
status.dwCurrentState = state;
SetServiceStatus(hStatus, &status);
}
[[noreturn]] void ServiceCtrlHandler(DWORD control) {
if (control == SERVICE_CONTROL_STOP) {
ReportStatus(SERVICE_STOP_PENDING);
// 执行清理
ReportStatus(SERVICE_STOPPED);
}
}
[[noreturn]] void WINAPI ServiceMain(DWORD argc, LPTSTR* argv) {
hStatus = RegisterServiceCtrlHandler(L"大顺脚本", ServiceCtrlHandler);
status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
ReportStatus(SERVICE_RUNNING); // 告诉SCM服务已启动
//服务主逻辑
MyMain(argc, argv);
}
#endif
+159
View File
@@ -0,0 +1,159 @@
/* 进程(Process)是计算机中正在执行的程序的一个实例,是
* 操作系统进行资源分配和调度的基本单位
*
* 你可以将其理解为程序的一次动态执行过程——程序
* 是静态的代码文件,而进程是程序运行时的动态实体。
*/
#pragma once
#ifdef _WIN32
#include <WinSock2.h>
#include <Windows.h>
#endif
#include <boost/process/process.hpp>
#include <boost/process/environment.hpp>
#include <boost/asio/io_context.hpp>
#include <list>
#include <memory>
#include <iostream>
namespace qing {
/* 进程列表类,这个类封装了一个
* 容纳Pls::subp的列表
*
* 以及基本的进程操作
*/
class Pls {
public:
// 命名子进程
struct subp {
std::unique_ptr<boost::process::process> p;
std::string name;
};
/* 创建子进程执行一个命令,并且
* 添加到进程列表。
*
* 接受一个std::string类型的文件路径,以及
* std::vector<std::string> 类型的参数列表(缺省)
*
* 优化方案:返回错误信息
*/
void exec(const std::string& path, const std::vector<std::string> &args)
try {
// 检查文件是否存在(好像不会在环境变量中寻找)
if (!boost::filesystem::exists(path)) {
std::cerr << "Error: File not found - " << path << std::endl;
return;
}
// 输入输出上下文
boost::asio::io_context io_ctx;
// 继承当前进程的所有环境变量(旧版)
//auto env = boost::process::environment::current;
// 错误码
boost::system::error_code ec;
// 构造子进程
subp p = { std::move(std::make_unique<boost::process::process>( io_ctx, path, args/*, env, ec*/)), mergeCmd(path, args)};
if (ec) {
std::cerr << "Failed to start process: " << ec.message() << std::endl;
return ;
}
else {
// 推入向量
vec.push_back(std::move(p));
}
}
catch (std::exception &e) {
std::cout << e.what() << std::endl;
}
/* 序列化进程列表 */
inline std::string initialize() {
std::string ret{ "" };
for (auto& item : vec) {
std::ostringstream oss;
ret += (ret == "") ? "" : "\n";
#ifdef _WIN32
DWORD pid = GetProcessId(item.p->native_handle());
oss << pid;
#else
oss << item.p->native_handle();
#endif
ret += oss.str();
ret += "\t";
ret += (item.p->running()) ? "Alive" : "Dead";
ret += "\t";
ret += item.name;
}
return ret;
}
// 杀死一个进程
[[noreturn]] void kill(int pid)
try {
for (auto it = vec.begin(); it != vec.end(); ++it)
if ((*it).p->id() == pid && (*it).p->running()) {
(*it).p->terminate();
//(*it).p->wait(); // 可能杀不死?
//vec.erase(it); // 清除
break;
}
}
catch (std::exception &ex) {
std::cout << ex.what() << std::endl;
}
// 清理死掉的进程
[[noreturn]] void clear() {
for (auto it = vec.begin(); it != vec.end();)
if (!(*it).p->running()) {
it = vec.erase(it);
}
else {
++it;
}
}
// 关闭所有进程
[[noreturn]] void closeAll() {
for (auto& item : vec)
if (item.p->running())
item.p->terminate();
// 加上这个似乎更清爽一些
clear();
}
private:
// 容纳子进程的链表
std::list<subp> vec{};
// 用于操作链表的锁
//std::mutex mtx{};
/* 将一个启动脚本地址,和一个
* 字符串向量组成的参数列表,组装
* 成一条完整的启动命令
*/
inline static std::string mergeCmd(const std::string& path, const std::vector<std::string>& args) {
std::string ret = path;
for (const auto& item : args) {
ret += " ";
ret += item;
}
return ret;
}
};
}
+189
View File
@@ -0,0 +1,189 @@
/* 线程(Thread)是操作系统能够进行运算调度的最小单元,被
* 包含在进程(Process)之中,是进程中的实际运作单元。
* 你可以将它理解为一条在啊代码中独立执行的路径。
*/
#pragma once
#include "Fsm.hpp"
#include <thread>
#include <memory>
#include <chrono>
#include <functional>
namespace qing {
/* 包含一个简易的多线程控制机制,需要共享的变量
* 和函数声明为成员。线程主函数包括了流程控制。
* 要使用需要实现其中的各个事件方法
*
* ?: 已经将fsm改为通用的
* 20250621: 从派生改为持有
*/
class Th {
public:
// 没有C原生的数据结构,可以直接使用缺省构造函数
Th() = default;
// 删除复制构造函数。因为
// 持有线程类,不要轻举妄动。
Th(const Th&) = delete;
// 虚析构函数。具体
// 有什么用忘了,好像是
// 通过析构函数关闭线程时用的
virtual ~Th() = default;
/////////////////////////////////////
// 检查状态,做成
// 虚函数,万一子类有特别要求呢?
virtual inline Fsm::Stat check() {
return fsm.check();
}
/////////////////////////////////////
// 变质——状态改变的方法
//
// 进入唤醒状态
// 1.这里的锁和条件变量是这个类中的。
// 2.唤醒函数不可以进行该类中的锁操作,否则会造成死锁。(忘了为啥,可能跟调用位置相关)
[[noreturn]] virtual void wake() {
fsm.set(Fsm::Stat::START);
} // wake()
// 进入静止状态
[[noreturn]] virtual void stop() {
{
std::lock_guard<std::mutex> lk(mtx_th);
fsm.set(Fsm::Stat::STOP);
ready = true;
}
// 可能会出现多个线程在等待的情况
cv.notify_all();
}// stop()
// 进入关闭状态
[[noreturn]] virtual void shut() {
{
std::lock_guard<std::mutex> lk(mtx_th);
fsm.set(Fsm::Stat::SHUT);
ready = true;
}
cv.notify_all();
}
// 进入运行状态
[[noreturn]] virtual void run() {
{
std::lock_guard<std::mutex> lk(mtx_th);
fsm.set(Fsm::Stat::RUNNING);
ready = true;
}
cv.notify_all();
}
/////////////////////////////////////
//控制
//
// 尝试唤醒线程并等待它开始,据说该函数有问题
[[noreturn]] void WaitStart() {
// 等待线程开始,wake()不可以加锁,否则会死锁
std::unique_lock<std::mutex> lk(mtx_th);
wake();
ready = false;
cv.wait(lk, [this] { return ready; });
}
// 等待线程关闭
[[noreturn]] void WaitClose() {
shut();
if (th->joinable())
th->join();
}
// 仅仅等待线程结束
[[noreturn]] void Wait() {
if (th->joinable())
th->join();
}
// 激活线程,申请线程对象,因为
// 需要确保对象已经被构造了,该函数不能放在构造函数内
[[noreturn]] void Activate() {
// 防止重复激活
if (th) return;
// 申请线程资源
th = std::make_unique<std::thread>(&Th::main, this);
}
protected:
/////////////////////////////////////
// 事件
//
//
// 静止事件
std::function<void()> StopEvent = [this] { fsm.suspend(); };
// 唤醒事件
std::function<void()> WakeEvent = [this] { this->run(); };
// 循环事件
std::function<void()> LoopEvent = [] {
std::this_thread::sleep_for(std::chrono::milliseconds(10L));
};
// 清理事件
std::function<void()> ClearEvent = [] {};
/////////////////////////////////////
// 循环
[[noreturn]] virtual void main() {
// 回字形循环
while (check() != Fsm::Stat::SHUT) {
// 静止状态
while (check() == Fsm::Stat::STOP) {
StopEvent();
}
// 开始事件
if (check() == Fsm::Stat::START) {
WakeEvent();
}
// (从设置阶段)进入运行态
while (check() == Fsm::Stat::RUNNING) {
LoopEvent();
}
// 清理事件,记得检查
// 成员变量有没有获取资源
ClearEvent();
}
}
private:
// 持有状态机类型
qing::Fsm fsm{};
// 持有线程类型
std::unique_ptr<std::thread> th;
// 持有锁类型
std::mutex mtx_th;
// 这里持有条件变量,用于阻塞以等待线程的关闭
std::condition_variable cv;
// 辅助条件变量的逻辑型
bool ready = false;
};
}
File diff suppressed because it is too large. Load diff
+141
View File
@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{a0e6a4c1-02b0-41f5-abb7-33c6e4fb97f6}</ProjectGuid>
<RootNamespace>dashun</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>C:\local\boost_1_88_0;C:\Program Files\libuv\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalLibraryDirectories>C:\local\boost_1_88_0\lib64-msvc-14.3;C:\Program Files\libuv;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>libuv.lib;ntdll.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="Fsm.hpp" />
<ClCompile Include="Main.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="args.hxx" />
<ClInclude Include="Pls.hpp" />
<ClInclude Include="Th.hpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="源文件">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="头文件">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="资源文件">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Fsm.hpp">
<Filter>头文件</Filter>
</ClCompile>
<ClCompile Include="Main.cpp">
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Th.hpp">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="args.hxx">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="Pls.hpp">
<Filter>头文件</Filter>
</ClInclude>
</ItemGroup>
</Project>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>
+20
View File
@@ -0,0 +1,20 @@
// dashun_to.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
int main()
{
std::cout << "Hello World!\n";
}
// 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单
// 调试程序: F5 或调试 >“开始调试”菜单
// 入门使用技巧:
// 1. 使用解决方案资源管理器窗口添加/管理文件
// 2. 使用团队资源管理器窗口连接到源代码管理
// 3. 使用输出窗口查看生成输出和其他消息
// 4. 使用错误列表窗口查看错误
// 5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目
// 6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件
@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{04d7a85c-43bb-4696-b9c8-f79c2252eda1}</ProjectGuid>
<RootNamespace>dashunto</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="dashun_to.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="源文件">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="头文件">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="资源文件">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="dashun_to.cpp">
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup>
</Project>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>
+1
View File
@@ -0,0 +1 @@
sc create 大顺脚本 binPath= "C:\Users\90999\Desktop\tws\dashun\x64\Debug\dashun.exe" start= auto DisplayName= "大顺脚本,进程托管服务"
+6
View File
@@ -0,0 +1,6 @@
# mgmt
这是基于pmc模块的管理平台,负责对子系统进行管理。
## 放置秘钥
为了和pmc系统通信,我们需要在该目录下放置pmc的rsa私钥
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
bash ./_setup.sh
source ./.env/bin/activate
python main.py
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
python3 -m venv .env
#sudo chmod +x .env/bin/activate
source .env/bin/activate
pip install -r requirements.txt -i https://pypi.org/simple
+14
View File
@@ -0,0 +1,14 @@
from app.login.routes import login_bp
from app.log.routes import log_bp
from app.module.routes import module_bp
from app.process.routes import process_bp
from app.user.routes import user_bp
def register_blueprints(app):
# 默认访问家目录
app.register_blueprint(login_bp, url_prefix='/')
app.register_blueprint(log_bp, url_prefix='/log')
app.register_blueprint(module_bp, url_prefix='/module')
app.register_blueprint(process_bp, url_prefix='/process')
app.register_blueprint(user_bp, url_prefix='/user')
Whitespace-only changes.
+113
View File
@@ -0,0 +1,113 @@
import datetime
import hashlib
from functools import wraps
from pmc import DnfsComm
from dnfs import UsersTable, DnfsTable
import jwt
from flask import Blueprint, render_template, request, redirect, url_for, current_app, make_response, jsonify
login_bp = Blueprint('login', __name__, template_folder='templates/login')
comm = DnfsComm("http://localhost:8006")
def get_user_from_token():
token = request.cookies.get('token')
if not token:
return None
try:
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
return payload # 包含 user、user_id、exp 等字段
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
@login_bp.after_request
def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = '*' # 允许所有来源跨域访问,如需限制可替换为具体域名
response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS'
return response
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
# 从cookie中获取token
token = request.cookies.get('token')
if not token:
return redirect(url_for('login.index'))
try:
jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
except jwt.ExpiredSignatureError:
return redirect(url_for('login.index'))
except jwt.InvalidTokenError:
return redirect(url_for('login.index'))
return f(*args, **kwargs)
return decorated
@login_bp.route('/')
def index():
return render_template('index.html')
@login_bp.route('/api/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('user-name')
password = request.form.get('user-passwd')
password = password.encode("utf-8")
hashed_password = hashlib.sha256(password).hexdigest()
try:
data = comm.get_and_trans(0)
dnfs_tab = DnfsTable(data)
users = dnfs_tab.query_by_title('Users')
table = UsersTable(users.content)
data = table.user_exists(username, hashed_password)
if not data:
return "用户登录失败", 401
_user_id, _username, _password, _role = data
except DnfsComm.GetFailed:
return "获取数据失败", 500
except DnfsTable.NotFound:
return "未找到用户表", 404
except UsersTable.NotFound:
return "用户不存在", 401
except Exception as e:
return str(e), 404
# 生成 Token
token = jwt.encode({
'user_id': _user_id,
'username': _username,
'role': _role,
'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=3)
}, current_app.config['SECRET_KEY'], algorithm='HS256')
# 构造 JSON 响应体
response = make_response(jsonify({
"redirect": url_for('login.mgmt'), # 跳转路径
"token": token
}))
# 设置 Cookie
response.set_cookie('token', token)
return response
else:
return "登录失败"
@login_bp.route('/api/mgmt')
@token_required
def mgmt():
payload = get_user_from_token()
if not payload:
return redirect(url_for('login.index'))
username = payload.get('username')
role = payload.get('role')
return render_template('mgmt.html', username=username, role=role)
Whitespace-only changes.
+34
View File
@@ -0,0 +1,34 @@
from pmc import PmcComm
from flask import Blueprint, render_template
module_bp = Blueprint('module', __name__, template_folder='templates/features/module.html')
pmc = PmcComm('http://localhost:8012')
pmc.Set_RSA_Pri_Key("private_key.pem")
pmc.Try_Get_AES_Key()
@module_bp.after_request
def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = '*' # 允许所有来源跨域访问,如需限制可替换为具体域名
response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS'
return response
@module_bp.route('/api/mgmt/module', methods=['GET', 'POST'])
def module():
data = pmc.list_mods()
modules = data.split('\n')
return render_template('features/module.html', modules=modules)
@module_bp.route('/create_module<string:mod_name>', methods=['GET', 'POST'])
def create_module(mod_name):
pass
@module_bp.route('/delete_module<string:mod_name>', methods=['GET', 'POST'])
def delete_module(mod_name):
pass
Whitespace-only changes.
+53
View File
@@ -0,0 +1,53 @@
from flask import Blueprint, render_template, jsonify, request
from pmc import PmcComm
process_bp = Blueprint('process', __name__, template_folder='templates/features/process.html')
pmc = PmcComm('http://localhost:8012')
pmc.Set_RSA_Pri_Key("private_key.pem")
pmc.Try_Get_AES_Key()
@process_bp.after_request
def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = '*' # 允许所有来源跨域访问,如需限制可替换为具体域名
response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS'
return response
@process_bp.route('/api/mgmt/process', methods=['GET', 'POST'])
def process():
pid = []
status = []
order = []
ls = pmc.get_list()
if ls != "":
ls_1 = ls.split('\n')
for l in ls_1:
i, j, k = l.split('\t')
pid.append(i)
status.append(j)
order.append(k)
data = [{"pid": p, "status": s, "order": o} for p, s, o in zip(pid, status, order)]
return render_template('features/process.html', response=data)
@process_bp.route('/kill_process<int:pid>', methods=['POST'])
def kill_process(pid):
pmc.kill(pid)
return jsonify({"status": "success"})
@process_bp.route('/clear_process', methods=['POST'])
def clear_process():
pmc.clear()
return jsonify({"status": "success"})
@process_bp.route('/create_process<string:name>', methods=['POST'])
def create_process(name):
pmc.start(name)
return jsonify({"status": "success"})
View File
Whitespace-only changes.
+111
View File
@@ -0,0 +1,111 @@
import jwt
import hashlib
from flask import Blueprint, render_template, request, current_app, jsonify
from pmc import DnfsComm
from dnfs import UsersTable, DnfsTable
user_bp = Blueprint('user', __name__, template_folder='templates/features/user.html')
comm = DnfsComm("http://localhost:8006")
def get_user_from_token():
token = request.cookies.get('token')
if not token:
return None
try:
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
return payload # 包含 user、user_id、exp 等字段
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
def get_node():
try:
data = comm.get_and_trans(0)
except DnfsComm.GetFailed:
return "获取数据失败", 500
for item in data:
if item['title'] == 'Users':
node_id = item['id']
node_content = item['content']
return node_id, node_content
return None
@user_bp.after_request
def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = '*' # 允许所有来源跨域访问,如需限制可替换为具体域名
response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS'
return response
@user_bp.route('/api/mgmt/user', methods=['GET', 'POST'])
def user():
try:
data = comm.get_and_trans(0)
dnfs_tab = DnfsTable(data)
users = dnfs_tab.query_by_title('Users')
table = UsersTable(users.content)
crt_dict = lambda user: {'user_id': user.id, 'username': user.name, 'role': user.privileges}
user_list = [crt_dict(user) for user in table.arr]
except DnfsComm.GetFailed:
return "获取数据失败", 500
except DnfsTable.NotFound:
return "未找到用户表", 404
except UsersTable.NotFound:
return "用户不存在", 401
except Exception as e:
return str(e), 404
payload = get_user_from_token()
if not payload:
return jsonify({"error": "未授权访问"}), 401
user_id = payload.get('user_id')
role = payload.get('role')
return render_template('features/user.html',
response=user_list,
current_user_id=user_id,
current_role=role)
@user_bp.route('/add_user', methods=['POST'])
def add_user():
node_id, msg = get_node()
username = request.form.get('username')
password = request.form.get('password')
hashed_password = hashlib.sha256(password.encode("utf-8")).hexdigest()
role = "normal"
table = UsersTable(msg)
table.add_user(username, hashed_password, role)
table = str(table)
comm.upd(node_id, 0, 'Users', table)
return jsonify({"message": "用户添加成功"})
@user_bp.route('/change_password', methods=['POST'])
def change_password():
node_id, msg = get_node()
user_id = request.form.get('user_id')
new_password = request.form.get('new_password')
hashed_password = hashlib.sha256(new_password.encode("utf-8")).hexdigest()
table = UsersTable(msg)
table.change_password(int(user_id), hashed_password)
table = str(table)
comm.upd(node_id, 0, 'Users', table)
return jsonify({"message": "密码修改成功"})
@user_bp.route('/delete_user<int:user_id>', methods=['POST'])
def delete_user(user_id):
node_id, msg = get_node()
table = UsersTable(msg)
table.del_user(user_id)
table = str(table)
comm.upd(node_id, 0, 'Users', table)
return jsonify({"message": "用户删除成功"})
+17
View File
@@ -0,0 +1,17 @@
from app import register_blueprints
from flask import Flask
from waitress import serve
# 初始化Flask应用
app = Flask(__name__)
# 设置一个密钥
app.secret_key = 'secret_key'
# 注册蓝图
register_blueprints(app)
if __name__ == '__main__':
#app.run(host='0.0.0.0', port=8009, debug=True)
serve(app, host="0.0.0.0", port=8009)
+5
View File
@@ -0,0 +1,5 @@
../_pmc
../_dnfs
flask
PyJWT
waitress
Whitespace-only changes.
Loaded 100 of 265 files, more files were not shown because too many files have changed in this diff. Show more