修改了后台视频录制等功能
This commit is contained in:
parent
5059ad6158
commit
cd35871476
@ -1103,12 +1103,15 @@ class DatabaseManager:
|
|||||||
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO detection_video (
|
INSERT INTO detection_video (
|
||||||
id, session_id, screen_video, timestamp
|
id, session_id, screen_video, body_video, foot_video1, foot_video2, timestamp
|
||||||
) VALUES (?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
''', (
|
''', (
|
||||||
video_id,
|
video_id,
|
||||||
session_id,
|
session_id,
|
||||||
video.get('screen_video_path'),
|
video.get('screen_video_path'),
|
||||||
|
video.get('body_video_path'),
|
||||||
|
video.get('foot_video1_path'),
|
||||||
|
video.get('foot_video2_path'),
|
||||||
china_time
|
china_time
|
||||||
))
|
))
|
||||||
|
|
||||||
@ -1120,6 +1123,50 @@ class DatabaseManager:
|
|||||||
logger.error(f'保存检测视频失败: {e}')
|
logger.error(f'保存检测视频失败: {e}')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def update_detection_video_latest(self, session_id: str, video: Dict[str, Any]) -> bool:
|
||||||
|
conn = self.get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT id, screen_video, body_video, foot_video1, foot_video2
|
||||||
|
FROM detection_video
|
||||||
|
WHERE session_id = ?
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT 1
|
||||||
|
''', (session_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if not row:
|
||||||
|
return self.save_detection_video(session_id, video)
|
||||||
|
|
||||||
|
row_dict = dict(row)
|
||||||
|
video_id = row_dict.get('id')
|
||||||
|
|
||||||
|
screen_video = video.get('screen_video_path')
|
||||||
|
body_video = video.get('body_video_path')
|
||||||
|
foot_video1 = video.get('foot_video1_path')
|
||||||
|
foot_video2 = video.get('foot_video2_path')
|
||||||
|
|
||||||
|
if not screen_video:
|
||||||
|
screen_video = row_dict.get('screen_video')
|
||||||
|
if not body_video:
|
||||||
|
body_video = row_dict.get('body_video')
|
||||||
|
if not foot_video1:
|
||||||
|
foot_video1 = row_dict.get('foot_video1')
|
||||||
|
if not foot_video2:
|
||||||
|
foot_video2 = row_dict.get('foot_video2')
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE detection_video
|
||||||
|
SET screen_video = ?, body_video = ?, foot_video1 = ?, foot_video2 = ?
|
||||||
|
WHERE id = ?
|
||||||
|
''', (screen_video, body_video, foot_video1, foot_video2, video_id))
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
logger.error(f'更新检测视频失败: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
def delete_detection_video(self, video_ids: Union[str, List[str]]) -> bool:
|
def delete_detection_video(self, video_ids: Union[str, List[str]]) -> bool:
|
||||||
"""删除检测视频记录(支持单个或多个ID)"""
|
"""删除检测视频记录(支持单个或多个ID)"""
|
||||||
conn = self.get_connection()
|
conn = self.get_connection()
|
||||||
|
|||||||
@ -14,6 +14,7 @@ from typing import Optional, Dict, Any
|
|||||||
import logging
|
import logging
|
||||||
import queue
|
import queue
|
||||||
import gc
|
import gc
|
||||||
|
import os
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .base_device import BaseDevice
|
from .base_device import BaseDevice
|
||||||
@ -110,7 +111,19 @@ class CameraManager(BaseDevice):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 全局帧队列(用于录制)
|
# 全局帧队列(用于录制)
|
||||||
self.frame_queue = queue.Queue(maxsize=10) # 最大长度10,自动丢弃旧帧
|
self.frame_queue = queue.Queue(maxsize=10)
|
||||||
|
|
||||||
|
self._recording_enabled = False
|
||||||
|
self._recording_session_id = None
|
||||||
|
self._recording_frames_dir = None
|
||||||
|
self._recording_target_fps = None
|
||||||
|
self._recording_last_ts = 0.0
|
||||||
|
self._recording_index = 0
|
||||||
|
self._recording_written = 0
|
||||||
|
self._recording_drop = 0
|
||||||
|
self._recording_queue = queue.Queue(maxsize=300)
|
||||||
|
self._recording_thread = None
|
||||||
|
self._recording_stop_event = threading.Event()
|
||||||
|
|
||||||
# 属性缓存机制 - 避免重复设置相同属性值
|
# 属性缓存机制 - 避免重复设置相同属性值
|
||||||
self._property_cache = {}
|
self._property_cache = {}
|
||||||
@ -125,6 +138,130 @@ class CameraManager(BaseDevice):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
self.logger.info(f"相机管理器初始化完成 - 设备索引: {self.device_index}")
|
self.logger.info(f"相机管理器初始化完成 - 设备索引: {self.device_index}")
|
||||||
|
|
||||||
|
def start_jpeg_recording(self, session_id: str, frames_dir: str, record_fps: Optional[int] = None) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
if not session_id:
|
||||||
|
return {'success': False, 'message': '缺少session_id'}
|
||||||
|
if not frames_dir:
|
||||||
|
return {'success': False, 'message': '缺少frames_dir'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(frames_dir, exist_ok=True)
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'message': f'创建录制目录失败: {e}'}
|
||||||
|
|
||||||
|
if record_fps is None:
|
||||||
|
record_fps = int(self.config_manager.get_config_value('CAMERA_RECORDING', 'fps', fallback='10'))
|
||||||
|
record_fps = max(1, int(record_fps))
|
||||||
|
|
||||||
|
self._recording_session_id = session_id
|
||||||
|
self._recording_frames_dir = frames_dir
|
||||||
|
self._recording_target_fps = record_fps
|
||||||
|
self._recording_last_ts = 0.0
|
||||||
|
self._recording_index = 0
|
||||||
|
self._recording_written = 0
|
||||||
|
self._recording_drop = 0
|
||||||
|
self._recording_stop_event.clear()
|
||||||
|
self._recording_enabled = True
|
||||||
|
|
||||||
|
if not self._recording_thread or not self._recording_thread.is_alive():
|
||||||
|
self._recording_thread = threading.Thread(
|
||||||
|
target=self._recording_writer_loop,
|
||||||
|
name=f"{self.device_id}-JpegWriter",
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
self._recording_thread.start()
|
||||||
|
|
||||||
|
return {'success': True, 'message': '相机JPEG录制已启动', 'device_id': self.device_id, 'fps': record_fps, 'frames_dir': frames_dir}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'message': str(e)}
|
||||||
|
|
||||||
|
def stop_jpeg_recording(self, session_id: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
if session_id and self._recording_session_id and session_id != self._recording_session_id:
|
||||||
|
return {'success': False, 'message': 'session_id不匹配'}
|
||||||
|
|
||||||
|
self._recording_enabled = False
|
||||||
|
self._recording_session_id = None
|
||||||
|
frames_dir = self._recording_frames_dir
|
||||||
|
fps = self._recording_target_fps
|
||||||
|
written = int(self._recording_written)
|
||||||
|
dropped = int(self._recording_drop)
|
||||||
|
|
||||||
|
self._recording_frames_dir = None
|
||||||
|
self._recording_target_fps = None
|
||||||
|
self._recording_last_ts = 0.0
|
||||||
|
|
||||||
|
self._recording_stop_event.set()
|
||||||
|
if self._recording_thread and self._recording_thread.is_alive():
|
||||||
|
self._recording_thread.join(timeout=2.0)
|
||||||
|
self._recording_thread = None
|
||||||
|
|
||||||
|
while not self._recording_queue.empty():
|
||||||
|
try:
|
||||||
|
self._recording_queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
|
||||||
|
self._recording_stop_event.clear()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': '相机JPEG录制已停止',
|
||||||
|
'device_id': self.device_id,
|
||||||
|
'frames_dir': frames_dir,
|
||||||
|
'fps': fps,
|
||||||
|
'frames_written': written,
|
||||||
|
'frames_dropped': dropped
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'message': str(e)}
|
||||||
|
|
||||||
|
def _recording_writer_loop(self):
|
||||||
|
while not self._recording_stop_event.is_set():
|
||||||
|
try:
|
||||||
|
item = self._recording_queue.get(timeout=0.2)
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
frames_dir, idx, jpeg_bytes = item
|
||||||
|
if not frames_dir or not jpeg_bytes:
|
||||||
|
continue
|
||||||
|
filename = f"frame_{idx:06d}.jpg"
|
||||||
|
fpath = os.path.join(frames_dir, filename)
|
||||||
|
with open(fpath, 'wb') as f:
|
||||||
|
f.write(jpeg_bytes)
|
||||||
|
self._recording_written += 1
|
||||||
|
except Exception:
|
||||||
|
self._recording_drop += 1
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
self._recording_queue.task_done()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _maybe_enqueue_recording(self, timestamp: float, frame_bytes: bytes):
|
||||||
|
if not self._recording_enabled:
|
||||||
|
return
|
||||||
|
frames_dir = self._recording_frames_dir
|
||||||
|
target_fps = self._recording_target_fps
|
||||||
|
if not frames_dir or not target_fps or target_fps <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._recording_last_ts > 0:
|
||||||
|
min_interval = 1.0 / float(target_fps)
|
||||||
|
if (timestamp - self._recording_last_ts) < min_interval:
|
||||||
|
return
|
||||||
|
|
||||||
|
idx = self._recording_index
|
||||||
|
self._recording_index += 1
|
||||||
|
self._recording_last_ts = timestamp
|
||||||
|
try:
|
||||||
|
self._recording_queue.put_nowait((frames_dir, idx, frame_bytes))
|
||||||
|
except queue.Full:
|
||||||
|
self._recording_drop += 1
|
||||||
|
|
||||||
def _set_property_optimized(self, prop, value):
|
def _set_property_optimized(self, prop, value):
|
||||||
"""
|
"""
|
||||||
@ -649,23 +786,6 @@ class CameraManager(BaseDevice):
|
|||||||
# 更新心跳时间,防止连接监控线程判定为超时
|
# 更新心跳时间,防止连接监控线程判定为超时
|
||||||
self.update_heartbeat()
|
self.update_heartbeat()
|
||||||
|
|
||||||
# 保存原始帧到队列(用于录制)
|
|
||||||
try:
|
|
||||||
self.frame_queue.put_nowait({
|
|
||||||
'frame': frame.copy(),
|
|
||||||
'timestamp': time.time()
|
|
||||||
})
|
|
||||||
except queue.Full:
|
|
||||||
# 队列满时丢弃最旧的帧,添加新帧
|
|
||||||
try:
|
|
||||||
self.frame_queue.get_nowait() # 移除最旧的帧
|
|
||||||
self.frame_queue.put_nowait({
|
|
||||||
'frame': frame.copy(),
|
|
||||||
'timestamp': time.time()
|
|
||||||
})
|
|
||||||
except queue.Empty:
|
|
||||||
pass # 队列为空,忽略
|
|
||||||
|
|
||||||
# 处理帧(降采样以优化传输负载)
|
# 处理帧(降采样以优化传输负载)
|
||||||
processed_frame = self._process_frame(frame)
|
processed_frame = self._process_frame(frame)
|
||||||
|
|
||||||
@ -740,6 +860,7 @@ class CameraManager(BaseDevice):
|
|||||||
|
|
||||||
# 转换为bytes再做base64,减少中间numpy对象的长生命周期
|
# 转换为bytes再做base64,减少中间numpy对象的长生命周期
|
||||||
frame_bytes = buffer.tobytes()
|
frame_bytes = buffer.tobytes()
|
||||||
|
self._maybe_enqueue_recording(time.time(), frame_bytes)
|
||||||
frame_data = base64.b64encode(frame_bytes).decode('utf-8')
|
frame_data = base64.b64encode(frame_bytes).decode('utf-8')
|
||||||
|
|
||||||
# 发送数据
|
# 发送数据
|
||||||
@ -985,6 +1106,21 @@ class CameraManager(BaseDevice):
|
|||||||
self.frame_queue.get_nowait()
|
self.frame_queue.get_nowait()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
self._recording_enabled = False
|
||||||
|
self._recording_session_id = None
|
||||||
|
self._recording_frames_dir = None
|
||||||
|
self._recording_target_fps = None
|
||||||
|
self._recording_last_ts = 0.0
|
||||||
|
self._recording_stop_event.set()
|
||||||
|
if self._recording_thread and self._recording_thread.is_alive():
|
||||||
|
self._recording_thread.join(timeout=2.0)
|
||||||
|
self._recording_thread = None
|
||||||
|
while not self._recording_queue.empty():
|
||||||
|
try:
|
||||||
|
self._recording_queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
|
||||||
self.last_frame = None
|
self.last_frame = None
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,8 @@ import signal
|
|||||||
import base64
|
import base64
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional, Callable
|
||||||
|
import threading
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pyautogui
|
import pyautogui
|
||||||
@ -39,11 +40,38 @@ class RecordingManager:
|
|||||||
# FFmpeg进程管理
|
# FFmpeg进程管理
|
||||||
self._ffmpeg_processes = {}
|
self._ffmpeg_processes = {}
|
||||||
self._ffmpeg_meta = {}
|
self._ffmpeg_meta = {}
|
||||||
|
self._transcode_threads = {}
|
||||||
|
|
||||||
# 默认参数
|
# 默认参数
|
||||||
self.screen_fps = 25
|
self.screen_fps = 25
|
||||||
self.screen_size = self._get_screen_size()
|
self.screen_size = self._get_screen_size()
|
||||||
|
|
||||||
|
def _resolve_ffmpeg_path(self) -> Optional[str]:
|
||||||
|
ffmpeg_path = None
|
||||||
|
if self.config_manager:
|
||||||
|
ffmpeg_path = (
|
||||||
|
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_path', fallback=None) or
|
||||||
|
self.config_manager.get_config_value('RECORDING', 'ffmpeg_path', fallback=None)
|
||||||
|
)
|
||||||
|
if ffmpeg_path and os.path.isfile(str(ffmpeg_path)):
|
||||||
|
return str(ffmpeg_path)
|
||||||
|
base_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(os.path.abspath(__file__))
|
||||||
|
alt_path = os.path.join(base_dir, 'ffmpeg', 'bin', 'ffmpeg.exe')
|
||||||
|
if os.path.isfile(alt_path):
|
||||||
|
return alt_path
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_active_video_base(self, session_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
meta = self._ffmpeg_meta.get('screen') or {}
|
||||||
|
if meta.get('session_id') != session_id:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
'base_path': meta.get('base_path'),
|
||||||
|
'file_dir': meta.get('file_dir'),
|
||||||
|
'patient_id': meta.get('patient_id'),
|
||||||
|
'session_id': meta.get('session_id')
|
||||||
|
}
|
||||||
|
|
||||||
def _get_screen_size(self):
|
def _get_screen_size(self):
|
||||||
try:
|
try:
|
||||||
import pyautogui
|
import pyautogui
|
||||||
@ -82,20 +110,10 @@ class RecordingManager:
|
|||||||
os.makedirs(base_path, exist_ok=True)
|
os.makedirs(base_path, exist_ok=True)
|
||||||
screen_video_path = os.path.join(base_path, 'screen.mp4')
|
screen_video_path = os.path.join(base_path, 'screen.mp4')
|
||||||
target_fps = fps or self.screen_fps
|
target_fps = fps or self.screen_fps
|
||||||
ffmpeg_path = None
|
ffmpeg_path = self._resolve_ffmpeg_path()
|
||||||
if self.config_manager:
|
if not ffmpeg_path:
|
||||||
ffmpeg_path = (
|
result['message'] = '未配置有效的ffmpeg_path,请在配置中设置 SCREEN_RECORDING.ffmpeg_path 或 RECORDING.ffmpeg_path'
|
||||||
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_path', fallback=None) or
|
return result
|
||||||
self.config_manager.get_config_value('RECORDING', 'ffmpeg_path', fallback=None)
|
|
||||||
)
|
|
||||||
if not ffmpeg_path or not os.path.isfile(str(ffmpeg_path)):
|
|
||||||
base_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(os.path.abspath(__file__))
|
|
||||||
alt_path = os.path.join(base_dir, 'ffmpeg', 'bin', 'ffmpeg.exe')
|
|
||||||
if os.path.isfile(alt_path):
|
|
||||||
ffmpeg_path = alt_path
|
|
||||||
else:
|
|
||||||
result['message'] = '未配置有效的ffmpeg_path,请在配置中设置 SCREEN_RECORDING.ffmpeg_path 或 RECORDING.ffmpeg_path'
|
|
||||||
return result
|
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
str(ffmpeg_path),
|
str(ffmpeg_path),
|
||||||
@ -136,7 +154,7 @@ class RecordingManager:
|
|||||||
creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
|
creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
|
||||||
)
|
)
|
||||||
self._ffmpeg_processes['screen'] = proc
|
self._ffmpeg_processes['screen'] = proc
|
||||||
self._ffmpeg_meta['screen'] = {'base_path': base_path, 'patient_id': patient_id, 'session_id': session_id, 'video_path': screen_video_path}
|
self._ffmpeg_meta['screen'] = {'base_path': base_path, 'file_dir': file_dir, 'patient_id': patient_id, 'session_id': session_id, 'video_path': screen_video_path}
|
||||||
result['success'] = True
|
result['success'] = True
|
||||||
result['message'] = 'ffmpeg录制已启动'
|
result['message'] = 'ffmpeg录制已启动'
|
||||||
result['database_updates'] = {
|
result['database_updates'] = {
|
||||||
@ -151,6 +169,86 @@ class RecordingManager:
|
|||||||
result['message'] = f'ffmpeg启动失败: {e}'
|
result['message'] = f'ffmpeg启动失败: {e}'
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def transcode_jpeg_sequence_async(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
frames_dir: str,
|
||||||
|
output_mp4_path: str,
|
||||||
|
fps: int,
|
||||||
|
on_done: Optional[Callable[[int], None]] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
if not frames_dir or not os.path.isdir(frames_dir):
|
||||||
|
return {'success': False, 'message': 'frames_dir不存在'}
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(output_mp4_path), exist_ok=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ffmpeg_path = self._resolve_ffmpeg_path()
|
||||||
|
if not ffmpeg_path:
|
||||||
|
return {'success': False, 'message': 'ffmpeg_path无效'}
|
||||||
|
|
||||||
|
codec = (
|
||||||
|
self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_codec', fallback=None) or
|
||||||
|
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_codec', fallback=None) or
|
||||||
|
'libx264'
|
||||||
|
)
|
||||||
|
preset = (
|
||||||
|
self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_preset', fallback=None) or
|
||||||
|
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_preset', fallback=None) or
|
||||||
|
('p1' if codec == 'h264_nvenc' else 'ultrafast')
|
||||||
|
)
|
||||||
|
threads = int(self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_threads', fallback='1') or '1')
|
||||||
|
bframes = int(self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_bframes', fallback='0') or '0')
|
||||||
|
gop = int(self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_gop', fallback=str(max(1, int(fps * 2)))) or str(max(1, int(fps * 2))))
|
||||||
|
|
||||||
|
input_pattern = os.path.join(frames_dir, 'frame_%06d.jpg')
|
||||||
|
cmd = [
|
||||||
|
str(ffmpeg_path),
|
||||||
|
'-y',
|
||||||
|
'-framerate', str(int(max(1, fps))),
|
||||||
|
'-start_number', '0',
|
||||||
|
'-i', input_pattern,
|
||||||
|
'-c:v', str(codec),
|
||||||
|
'-preset', str(preset),
|
||||||
|
'-bf', str(bframes),
|
||||||
|
'-g', str(gop),
|
||||||
|
'-pix_fmt', 'yuv420p',
|
||||||
|
'-threads', str(threads),
|
||||||
|
str(output_mp4_path)
|
||||||
|
]
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
code = 1
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
|
||||||
|
)
|
||||||
|
self._ffmpeg_processes[name] = proc
|
||||||
|
code = proc.wait()
|
||||||
|
except Exception:
|
||||||
|
code = 1
|
||||||
|
finally:
|
||||||
|
self._ffmpeg_processes.pop(name, None)
|
||||||
|
self._transcode_threads.pop(name, None)
|
||||||
|
if on_done:
|
||||||
|
try:
|
||||||
|
on_done(int(code))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
t = threading.Thread(target=worker, name=f"FFmpegTranscode-{name}", daemon=True)
|
||||||
|
self._transcode_threads[name] = t
|
||||||
|
t.start()
|
||||||
|
return {'success': True, 'message': '转码任务已启动', 'name': name, 'output': output_mp4_path}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'message': str(e)}
|
||||||
|
|
||||||
def stop_recording_ffmpeg(self, session_id: str = None) -> Dict[str, Any]:
|
def stop_recording_ffmpeg(self, session_id: str = None) -> Dict[str, Any]:
|
||||||
result = {'success': False, 'message': ''}
|
result = {'success': False, 'message': ''}
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -16,6 +16,7 @@ from cryptography.hazmat.primitives import hashes, serialization
|
|||||||
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
||||||
from cryptography.exceptions import InvalidSignature
|
from cryptography.exceptions import InvalidSignature
|
||||||
import base64
|
import base64
|
||||||
|
import shutil
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -51,85 +52,345 @@ class LicenseManager:
|
|||||||
def __init__(self, config_manager=None):
|
def __init__(self, config_manager=None):
|
||||||
self.config_manager = config_manager
|
self.config_manager = config_manager
|
||||||
self._machine_id = None
|
self._machine_id = None
|
||||||
|
self._machine_id_candidates = None
|
||||||
self._license_cache = None
|
self._license_cache = None
|
||||||
self._cache_timestamp = None
|
self._cache_timestamp = None
|
||||||
|
|
||||||
|
def _get_backend_base_dir(self) -> str:
|
||||||
|
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
def _get_persistent_license_dir(self) -> str:
|
||||||
|
system = platform.system()
|
||||||
|
if system == "Windows":
|
||||||
|
base = os.environ.get("PROGRAMDATA") or os.environ.get("ALLUSERSPROFILE") or ""
|
||||||
|
if not base:
|
||||||
|
base = os.path.expanduser("~\\AppData\\Local")
|
||||||
|
else:
|
||||||
|
base = os.path.expanduser("~/.config")
|
||||||
|
return os.path.join(base, "BodyCheck", "license")
|
||||||
|
|
||||||
|
def _get_configured_license_paths(self) -> Tuple[str, str, int]:
|
||||||
|
license_path = "data/license.json"
|
||||||
|
public_key_path = "backend/license_pub.pem"
|
||||||
|
grace_days = 3
|
||||||
|
if self.config_manager:
|
||||||
|
license_path = self.config_manager.get_config_value("LICENSE", "path", license_path)
|
||||||
|
public_key_path = self.config_manager.get_config_value("LICENSE", "public_key", public_key_path)
|
||||||
|
grace_days = int(self.config_manager.get_config_value("LICENSE", "grace_days", str(grace_days)))
|
||||||
|
|
||||||
|
if not os.path.isabs(license_path):
|
||||||
|
license_path = os.path.join(self._get_backend_base_dir(), license_path)
|
||||||
|
if not os.path.isabs(public_key_path):
|
||||||
|
public_key_path = os.path.join(self._get_backend_base_dir(), public_key_path)
|
||||||
|
return license_path, public_key_path, grace_days
|
||||||
|
|
||||||
|
def _get_persistent_license_paths(self) -> Tuple[str, str]:
|
||||||
|
pdir = self._get_persistent_license_dir()
|
||||||
|
return os.path.join(pdir, "license.json"), os.path.join(pdir, "license_public_key.pem")
|
||||||
|
|
||||||
|
def _resolve_license_paths(self) -> Tuple[str, str, int]:
|
||||||
|
cfg_license_path, cfg_public_key_path, grace_days = self._get_configured_license_paths()
|
||||||
|
p_license_path, p_public_key_path = self._get_persistent_license_paths()
|
||||||
|
|
||||||
|
effective_license_path = cfg_license_path
|
||||||
|
if not os.path.exists(effective_license_path) and os.path.exists(p_license_path):
|
||||||
|
effective_license_path = p_license_path
|
||||||
|
|
||||||
|
effective_public_key_path = cfg_public_key_path
|
||||||
|
if not os.path.exists(effective_public_key_path) and os.path.exists(p_public_key_path):
|
||||||
|
effective_public_key_path = p_public_key_path
|
||||||
|
|
||||||
|
return effective_license_path, effective_public_key_path, grace_days
|
||||||
|
|
||||||
|
def _mirror_license_assets(self, license_path: Optional[str] = None, public_key_path: Optional[str] = None) -> None:
|
||||||
|
p_license_path, p_public_key_path = self._get_persistent_license_paths()
|
||||||
|
pdir = os.path.dirname(p_license_path)
|
||||||
|
try:
|
||||||
|
os.makedirs(pdir, exist_ok=True)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
if license_path and os.path.exists(license_path):
|
||||||
|
try:
|
||||||
|
shutil.copyfile(license_path, p_license_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if public_key_path and os.path.exists(public_key_path):
|
||||||
|
try:
|
||||||
|
shutil.copyfile(public_key_path, p_public_key_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def install_license_file(self, source_license_path: str) -> Tuple[bool, str]:
|
||||||
|
try:
|
||||||
|
if not os.path.exists(source_license_path):
|
||||||
|
return False, "源授权文件不存在"
|
||||||
|
|
||||||
|
cfg_license_path, cfg_public_key_path, _ = self._get_configured_license_paths()
|
||||||
|
os.makedirs(os.path.dirname(cfg_license_path), exist_ok=True)
|
||||||
|
shutil.copyfile(source_license_path, cfg_license_path)
|
||||||
|
self._mirror_license_assets(license_path=cfg_license_path, public_key_path=cfg_public_key_path)
|
||||||
|
return True, cfg_license_path
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def mirror_license_dir(self, source_dir: str) -> None:
|
||||||
|
if not source_dir or not os.path.isdir(source_dir):
|
||||||
|
return
|
||||||
|
license_candidate = os.path.join(source_dir, "license.json")
|
||||||
|
pub_candidates = [
|
||||||
|
os.path.join(source_dir, "license_public_key.pem"),
|
||||||
|
os.path.join(source_dir, "license_pub.pem"),
|
||||||
|
os.path.join(source_dir, "public_key.pem"),
|
||||||
|
]
|
||||||
|
pub_path = ""
|
||||||
|
for c in pub_candidates:
|
||||||
|
if os.path.exists(c):
|
||||||
|
pub_path = c
|
||||||
|
break
|
||||||
|
self._mirror_license_assets(license_path=license_candidate if os.path.exists(license_candidate) else None, public_key_path=pub_path or None)
|
||||||
|
|
||||||
|
def _run_powershell(self, command: str, timeout: int = 10) -> str:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"powershell",
|
||||||
|
"-NoProfile",
|
||||||
|
"-NonInteractive",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-Command",
|
||||||
|
command,
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
return (result.stdout or "").strip()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _get_windows_cpu_id(self) -> str:
|
||||||
|
out = self._run_powershell(
|
||||||
|
"(Get-CimInstance -ClassName Win32_Processor | Select-Object -First 1 -ExpandProperty ProcessorId)"
|
||||||
|
)
|
||||||
|
return out.strip()
|
||||||
|
|
||||||
|
def _get_windows_baseboard_serial(self) -> str:
|
||||||
|
out = self._run_powershell(
|
||||||
|
"(Get-CimInstance -ClassName Win32_BaseBoard | Select-Object -First 1 -ExpandProperty SerialNumber)"
|
||||||
|
)
|
||||||
|
serial = out.strip()
|
||||||
|
if serial and serial != "To be filled by O.E.M.":
|
||||||
|
return serial
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _get_windows_disk_serials(self) -> list:
|
||||||
|
raw = self._run_powershell(
|
||||||
|
"$d=Get-CimInstance -ClassName Win32_DiskDrive | Select-Object SerialNumber,InterfaceType,PNPDeviceID,MediaType; $d | ConvertTo-Json -Compress"
|
||||||
|
)
|
||||||
|
if not raw:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
disks = data if isinstance(data, list) else ([data] if isinstance(data, dict) else [])
|
||||||
|
serials = []
|
||||||
|
for d in disks:
|
||||||
|
serial = str((d.get("SerialNumber") or "")).strip()
|
||||||
|
iface = str((d.get("InterfaceType") or "")).strip().upper()
|
||||||
|
pnp = str((d.get("PNPDeviceID") or "")).strip().upper()
|
||||||
|
media = str((d.get("MediaType") or "")).strip().upper()
|
||||||
|
if serial and iface != "USB" and not pnp.startswith("USBSTOR") and "REMOVABLE" not in media:
|
||||||
|
serials.append(serial)
|
||||||
|
serials = sorted(set(serials))
|
||||||
|
return serials
|
||||||
|
|
||||||
|
def _get_windows_identifiers(self) -> Dict[str, Any]:
|
||||||
|
cpu_id = self._get_windows_cpu_id()
|
||||||
|
board_serial = self._get_windows_baseboard_serial()
|
||||||
|
disk_serials = self._get_windows_disk_serials()
|
||||||
|
|
||||||
|
if not cpu_id:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["wmic", "cpu", "get", "ProcessorId", "/value"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
for line in (result.stdout or "").split("\n"):
|
||||||
|
if "ProcessorId=" in line:
|
||||||
|
cpu_id = line.split("=", 1)[1].strip()
|
||||||
|
if cpu_id:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not board_serial:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["wmic", "baseboard", "get", "SerialNumber", "/value"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
for line in (result.stdout or "").split("\n"):
|
||||||
|
if "SerialNumber=" in line:
|
||||||
|
board_serial = line.split("=", 1)[1].strip()
|
||||||
|
if board_serial and board_serial != "To be filled by O.E.M.":
|
||||||
|
break
|
||||||
|
board_serial = ""
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not disk_serials:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"wmic",
|
||||||
|
"path",
|
||||||
|
"Win32_DiskDrive",
|
||||||
|
"get",
|
||||||
|
"SerialNumber,InterfaceType,PNPDeviceID,MediaType",
|
||||||
|
"/value",
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
block = {}
|
||||||
|
serials = []
|
||||||
|
for line in (result.stdout or "").split("\n"):
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
serial = (block.get("SerialNumber") or "").strip()
|
||||||
|
iface = (block.get("InterfaceType") or "").strip().upper()
|
||||||
|
pnp = (block.get("PNPDeviceID") or "").strip().upper()
|
||||||
|
media = (block.get("MediaType") or "").strip().upper()
|
||||||
|
if serial and iface != "USB" and not pnp.startswith("USBSTOR") and "REMOVABLE" not in media:
|
||||||
|
serials.append(serial)
|
||||||
|
block = {}
|
||||||
|
continue
|
||||||
|
if "=" in line:
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
block[k] = v
|
||||||
|
if block:
|
||||||
|
serial = (block.get("SerialNumber") or "").strip()
|
||||||
|
iface = (block.get("InterfaceType") or "").strip().upper()
|
||||||
|
pnp = (block.get("PNPDeviceID") or "").strip().upper()
|
||||||
|
media = (block.get("MediaType") or "").strip().upper()
|
||||||
|
if serial and iface != "USB" and not pnp.startswith("USBSTOR") and "REMOVABLE" not in media:
|
||||||
|
serials.append(serial)
|
||||||
|
disk_serials = sorted(set(serials))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
mac = ""
|
||||||
|
try:
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
mac = ":".join(
|
||||||
|
["{:02x}".format((uuid.getnode() >> elements) & 0xFF) for elements in range(0, 2 * 6, 2)][::-1]
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
mac = ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cpu_id": cpu_id.strip() if cpu_id else "",
|
||||||
|
"board_serial": board_serial.strip() if board_serial else "",
|
||||||
|
"disk_serials": disk_serials or [],
|
||||||
|
"mac": mac,
|
||||||
|
"node": platform.node(),
|
||||||
|
"processor": platform.processor(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _hash_core_info(self, core_info: list) -> str:
|
||||||
|
combined_info = "|".join(sorted(core_info))
|
||||||
|
return hashlib.sha256(combined_info.encode("utf-8")).hexdigest()[:16].upper()
|
||||||
|
|
||||||
|
def _build_machine_id_candidates(self) -> list:
|
||||||
|
system = platform.system()
|
||||||
|
info: Dict[str, Any] = {}
|
||||||
|
if system == "Windows":
|
||||||
|
info = self._get_windows_identifiers()
|
||||||
|
else:
|
||||||
|
info = {
|
||||||
|
"cpu_id": "",
|
||||||
|
"board_serial": "",
|
||||||
|
"disk_serials": [],
|
||||||
|
"mac": "",
|
||||||
|
"node": platform.node(),
|
||||||
|
"processor": platform.processor(),
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu_id = info.get("cpu_id") or ""
|
||||||
|
board_serial = info.get("board_serial") or ""
|
||||||
|
disk_serials = info.get("disk_serials") or []
|
||||||
|
mac = info.get("mac") or ""
|
||||||
|
|
||||||
|
variants = []
|
||||||
|
|
||||||
|
full_core = []
|
||||||
|
if cpu_id:
|
||||||
|
full_core.append(f"CPU:{cpu_id}")
|
||||||
|
if board_serial:
|
||||||
|
full_core.append(f"BOARD:{board_serial}")
|
||||||
|
for s in disk_serials:
|
||||||
|
full_core.append(f"DISK:{s}")
|
||||||
|
if full_core:
|
||||||
|
variants.append(full_core)
|
||||||
|
|
||||||
|
no_disk = []
|
||||||
|
if cpu_id:
|
||||||
|
no_disk.append(f"CPU:{cpu_id}")
|
||||||
|
if board_serial:
|
||||||
|
no_disk.append(f"BOARD:{board_serial}")
|
||||||
|
if no_disk:
|
||||||
|
variants.append(no_disk)
|
||||||
|
|
||||||
|
if cpu_id:
|
||||||
|
variants.append([f"CPU:{cpu_id}"])
|
||||||
|
if board_serial:
|
||||||
|
variants.append([f"BOARD:{board_serial}"])
|
||||||
|
|
||||||
|
if mac:
|
||||||
|
variants.append([f"MAC:{mac}"])
|
||||||
|
if cpu_id and mac:
|
||||||
|
variants.append([f"CPU:{cpu_id}", f"MAC:{mac}"])
|
||||||
|
|
||||||
|
fallback_core = []
|
||||||
|
node = (info.get("node") or "").strip()
|
||||||
|
proc = (info.get("processor") or "").strip()
|
||||||
|
if node:
|
||||||
|
fallback_core.append(f"NODE:{node}")
|
||||||
|
if proc:
|
||||||
|
fallback_core.append(f"PROCESSOR:{proc}")
|
||||||
|
if fallback_core:
|
||||||
|
variants.append(fallback_core)
|
||||||
|
|
||||||
|
prefix = "W10-" if system == "Windows" else "FB-"
|
||||||
|
candidates = []
|
||||||
|
for core in variants:
|
||||||
|
try:
|
||||||
|
mid = f"{prefix}{self._hash_core_info(core)}"
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if mid not in candidates:
|
||||||
|
candidates.append(mid)
|
||||||
|
return candidates
|
||||||
|
|
||||||
def get_machine_id(self) -> str:
|
def get_machine_id(self) -> str:
|
||||||
"""生成机器硬件指纹"""
|
"""生成机器硬件指纹"""
|
||||||
if self._machine_id:
|
if self._machine_id:
|
||||||
return self._machine_id
|
return self._machine_id
|
||||||
try:
|
try:
|
||||||
core_info = []
|
candidates = self._build_machine_id_candidates()
|
||||||
aux_info = []
|
if not candidates:
|
||||||
try:
|
raise RuntimeError("无法生成机器指纹")
|
||||||
if platform.system() == "Windows":
|
self._machine_id_candidates = candidates
|
||||||
result = subprocess.run(['wmic', 'cpu', 'get', 'ProcessorId', '/value'], capture_output=True, text=True, timeout=10)
|
self._machine_id = candidates[0]
|
||||||
for line in result.stdout.split('\n'):
|
|
||||||
if 'ProcessorId=' in line:
|
|
||||||
cpu_id = line.split('=')[1].strip()
|
|
||||||
if cpu_id:
|
|
||||||
core_info.append(f"CPU:{cpu_id}")
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"获取CPU信息失败: {e}")
|
|
||||||
try:
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
result = subprocess.run(['wmic', 'baseboard', 'get', 'SerialNumber', '/value'], capture_output=True, text=True, timeout=10)
|
|
||||||
for line in result.stdout.split('\n'):
|
|
||||||
if 'SerialNumber=' in line:
|
|
||||||
board_serial = line.split('=')[1].strip()
|
|
||||||
if board_serial and board_serial != "To be filled by O.E.M.":
|
|
||||||
core_info.append(f"BOARD:{board_serial}")
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"获取主板信息失败: {e}")
|
|
||||||
try:
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
# 获取磁盘信息并过滤掉USB/可移动介质,收集所有内部磁盘序列号
|
|
||||||
result = subprocess.run(
|
|
||||||
['wmic', 'path', 'Win32_DiskDrive', 'get', 'SerialNumber,InterfaceType,PNPDeviceID,MediaType', '/value'],
|
|
||||||
capture_output=True, text=True, timeout=10
|
|
||||||
)
|
|
||||||
block = {}
|
|
||||||
for line in result.stdout.split('\n'):
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
# 结束一个块
|
|
||||||
serial = (block.get('SerialNumber') or '').strip()
|
|
||||||
iface = (block.get('InterfaceType') or '').strip().upper()
|
|
||||||
pnp = (block.get('PNPDeviceID') or '').strip().upper()
|
|
||||||
media = (block.get('MediaType') or '').strip().upper()
|
|
||||||
if serial and iface != 'USB' and not pnp.startswith('USBSTOR') and 'REMOVABLE' not in media:
|
|
||||||
core_info.append(f"DISK:{serial}")
|
|
||||||
block = {}
|
|
||||||
continue
|
|
||||||
if '=' in line:
|
|
||||||
k, v = line.split('=', 1)
|
|
||||||
block[k] = v
|
|
||||||
# 处理最后一个块
|
|
||||||
if block:
|
|
||||||
serial = (block.get('SerialNumber') or '').strip()
|
|
||||||
iface = (block.get('InterfaceType') or '').strip().upper()
|
|
||||||
pnp = (block.get('PNPDeviceID') or '').strip().upper()
|
|
||||||
media = (block.get('MediaType') or '').strip().upper()
|
|
||||||
if serial and iface != 'USB' and not pnp.startswith('USBSTOR') and 'REMOVABLE' not in media:
|
|
||||||
core_info.append(f"DISK:{serial}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"获取磁盘信息失败: {e}")
|
|
||||||
try:
|
|
||||||
import uuid
|
|
||||||
mac = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff) for elements in range(0, 2 * 6, 2)][::-1])
|
|
||||||
aux_info.append(f"MAC:{mac}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"获取MAC地址失败: {e}")
|
|
||||||
aux_info.append(f"OS:{platform.system()}")
|
|
||||||
aux_info.append(f"MACHINE:{platform.machine()}")
|
|
||||||
if len(core_info) < 1:
|
|
||||||
core_info.append(f"NODE:{platform.node()}")
|
|
||||||
core_info.append(f"PROCESSOR:{platform.processor()}")
|
|
||||||
combined_info = "|".join(sorted(core_info))
|
|
||||||
machine_id = hashlib.sha256(combined_info.encode('utf-8')).hexdigest()[:16].upper()
|
|
||||||
self._machine_id = f"W10-{machine_id}"
|
|
||||||
logger.info(f"生成机器指纹: {self._machine_id}")
|
logger.info(f"生成机器指纹: {self._machine_id}")
|
||||||
return self._machine_id
|
return self._machine_id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -137,7 +398,13 @@ class LicenseManager:
|
|||||||
fallback_info = f"{platform.system()}-{platform.node()}-{platform.machine()}"
|
fallback_info = f"{platform.system()}-{platform.node()}-{platform.machine()}"
|
||||||
fallback_id = hashlib.md5(fallback_info.encode('utf-8')).hexdigest()[:12].upper()
|
fallback_id = hashlib.md5(fallback_info.encode('utf-8')).hexdigest()[:12].upper()
|
||||||
self._machine_id = f"FB-{fallback_id}"
|
self._machine_id = f"FB-{fallback_id}"
|
||||||
|
self._machine_id_candidates = [self._machine_id]
|
||||||
return self._machine_id
|
return self._machine_id
|
||||||
|
|
||||||
|
def get_machine_id_candidates(self) -> list:
|
||||||
|
if self._machine_id_candidates is None:
|
||||||
|
self.get_machine_id()
|
||||||
|
return list(self._machine_id_candidates or [])
|
||||||
|
|
||||||
def load_license(self, license_path: str) -> Optional[Dict[str, Any]]:
|
def load_license(self, license_path: str) -> Optional[Dict[str, Any]]:
|
||||||
"""加载授权文件"""
|
"""加载授权文件"""
|
||||||
@ -231,7 +498,13 @@ class LicenseManager:
|
|||||||
|
|
||||||
# 检查机器绑定
|
# 检查机器绑定
|
||||||
license_machine_id = license_data.get('machine_id', '')
|
license_machine_id = license_data.get('machine_id', '')
|
||||||
if license_machine_id != machine_id:
|
candidates = []
|
||||||
|
if machine_id:
|
||||||
|
candidates.append(machine_id)
|
||||||
|
for mid in self.get_machine_id_candidates():
|
||||||
|
if mid not in candidates:
|
||||||
|
candidates.append(mid)
|
||||||
|
if license_machine_id not in candidates:
|
||||||
return False, f"授权文件与当前机器不匹配 (当前: {machine_id}, 授权: {license_machine_id})"
|
return False, f"授权文件与当前机器不匹配 (当前: {machine_id}, 授权: {license_machine_id})"
|
||||||
|
|
||||||
# 检查有效期
|
# 检查有效期
|
||||||
@ -268,19 +541,8 @@ class LicenseManager:
|
|||||||
# 获取配置
|
# 获取配置
|
||||||
if not self.config_manager:
|
if not self.config_manager:
|
||||||
return LicenseStatus(valid=False, message="配置管理器未初始化")
|
return LicenseStatus(valid=False, message="配置管理器未初始化")
|
||||||
|
|
||||||
license_path = self.config_manager.get_config_value('LICENSE', 'path', 'data/license.json')
|
license_path, public_key_path, grace_days = self._resolve_license_paths()
|
||||||
public_key_path = self.config_manager.get_config_value('LICENSE', 'public_key', 'backend/license_pub.pem')
|
|
||||||
grace_days = int(self.config_manager.get_config_value('LICENSE', 'grace_days', '3'))
|
|
||||||
|
|
||||||
# 转换为绝对路径
|
|
||||||
if not os.path.isabs(license_path):
|
|
||||||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # backend目录
|
|
||||||
license_path = os.path.join(base_dir, license_path)
|
|
||||||
|
|
||||||
if not os.path.isabs(public_key_path):
|
|
||||||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # backend目录
|
|
||||||
public_key_path = os.path.join(base_dir, public_key_path)
|
|
||||||
|
|
||||||
# 获取机器指纹
|
# 获取机器指纹
|
||||||
machine_id = self.get_machine_id()
|
machine_id = self.get_machine_id()
|
||||||
@ -308,6 +570,8 @@ class LicenseManager:
|
|||||||
self._license_cache = status
|
self._license_cache = status
|
||||||
self._cache_timestamp = datetime.now().timestamp()
|
self._cache_timestamp = datetime.now().timestamp()
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
self._mirror_license_assets(license_path=license_path, public_key_path=public_key_path)
|
||||||
|
|
||||||
# 检查有效性
|
# 检查有效性
|
||||||
is_valid, message = self.check_validity(license_data, machine_id, grace_days)
|
is_valid, message = self.check_validity(license_data, machine_id, grace_days)
|
||||||
@ -362,14 +626,7 @@ class LicenseManager:
|
|||||||
if not self.config_manager:
|
if not self.config_manager:
|
||||||
return False, "配置管理器未初始化"
|
return False, "配置管理器未初始化"
|
||||||
|
|
||||||
# 解析公钥路径与宽限期
|
_, public_key_path, grace_days = self._resolve_license_paths()
|
||||||
public_key_path = self.config_manager.get_config_value('LICENSE', 'public_key', 'backend/license_pub.pem')
|
|
||||||
grace_days = int(self.config_manager.get_config_value('LICENSE', 'grace_days', '3'))
|
|
||||||
|
|
||||||
# 转换为绝对路径(相对backend目录)
|
|
||||||
if not os.path.isabs(public_key_path):
|
|
||||||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
||||||
public_key_path = os.path.join(base_dir, public_key_path)
|
|
||||||
|
|
||||||
if not os.path.exists(license_path):
|
if not os.path.exists(license_path):
|
||||||
return False, f"授权文件不存在: {license_path}"
|
return False, f"授权文件不存在: {license_path}"
|
||||||
|
|||||||
127
backend/main.py
127
backend/main.py
@ -10,6 +10,7 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
import shutil
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import Flask, jsonify
|
from flask import Flask, jsonify
|
||||||
from flask import request as flask_request
|
from flask import request as flask_request
|
||||||
@ -377,7 +378,11 @@ class AppServer:
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': '授权管理器未初始化'
|
'error': '授权管理器未初始化'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
if self.license_manager:
|
||||||
|
self.license_status = self.license_manager.get_license_status(force_reload=True)
|
||||||
|
self.app.license_status = self.license_status
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'data': {
|
'data': {
|
||||||
@ -548,22 +553,14 @@ class AppServer:
|
|||||||
|
|
||||||
if is_valid:
|
if is_valid:
|
||||||
# 覆盖系统授权文件为上传的文件
|
# 覆盖系统授权文件为上传的文件
|
||||||
try:
|
ok, result = self.license_manager.install_license_file(temp_path)
|
||||||
license_path_cfg = self.config_manager.get_config_value('LICENSE', 'path', 'data/license.json') if self.config_manager else 'data/license.json'
|
if not ok:
|
||||||
# 解析目标路径为绝对路径
|
self.logger.error(f'保存授权文件失败: {result}')
|
||||||
if not os.path.isabs(license_path_cfg):
|
return jsonify({'success': False, 'error': f'保存授权文件失败: {result}'}), 500
|
||||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
license_path_cfg = os.path.join(base_dir, license_path_cfg)
|
|
||||||
os.makedirs(os.path.dirname(license_path_cfg), exist_ok=True)
|
|
||||||
# 移动/覆盖授权文件
|
|
||||||
import shutil
|
|
||||||
shutil.copyfile(temp_path, license_path_cfg)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f'保存授权文件失败: {e}')
|
|
||||||
return jsonify({'success': False, 'error': f'保存授权文件失败: {str(e)}'}), 500
|
|
||||||
|
|
||||||
# 更新授权状态(强制刷新)
|
# 更新授权状态(强制刷新)
|
||||||
self.license_status = self.license_manager.get_license_status(force_reload=True)
|
self.license_status = self.license_manager.get_license_status(force_reload=True)
|
||||||
|
self.app.license_status = self.license_status
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@ -631,8 +628,11 @@ class AppServer:
|
|||||||
return jsonify({'success': False, 'error': '压缩包包含非法路径'}), 400
|
return jsonify({'success': False, 'error': '压缩包包含非法路径'}), 400
|
||||||
zip_ref.extractall(target_dir)
|
zip_ref.extractall(target_dir)
|
||||||
|
|
||||||
|
self.license_manager.mirror_license_dir(target_dir)
|
||||||
|
|
||||||
# 刷新授权
|
# 刷新授权
|
||||||
self.license_status = self.license_manager.get_license_status(force_reload=True)
|
self.license_status = self.license_manager.get_license_status(force_reload=True)
|
||||||
|
self.app.license_status = self.license_status
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@ -1448,6 +1448,30 @@ class AppServer:
|
|||||||
try:
|
try:
|
||||||
# 使用新的ffmpeg录制方法
|
# 使用新的ffmpeg录制方法
|
||||||
recording_response = self.recording_manager.start_recording_ffmpeg(session_id, patient_id, screen_location)
|
recording_response = self.recording_manager.start_recording_ffmpeg(session_id, patient_id, screen_location)
|
||||||
|
|
||||||
|
base_info = None
|
||||||
|
try:
|
||||||
|
base_info = self.recording_manager.get_active_video_base(session_id)
|
||||||
|
except Exception:
|
||||||
|
base_info = None
|
||||||
|
|
||||||
|
if base_info and base_info.get('base_path'):
|
||||||
|
base_path = base_info.get('base_path')
|
||||||
|
try:
|
||||||
|
cam1 = self.device_coordinator.devices.get('camera1') if self.device_coordinator and hasattr(self.device_coordinator, 'devices') else None
|
||||||
|
if cam1 and getattr(cam1, 'is_streaming', False):
|
||||||
|
cam1_frames_dir = os.path.join(base_path, 'camera1_frames')
|
||||||
|
cam1.start_jpeg_recording(session_id=session_id, frames_dir=cam1_frames_dir)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'启动camera1 JPEG录制失败: {e}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
cam2 = self.device_coordinator.devices.get('camera2') if self.device_coordinator and hasattr(self.device_coordinator, 'devices') else None
|
||||||
|
if cam2 and getattr(cam2, 'is_streaming', False):
|
||||||
|
cam2_frames_dir = os.path.join(base_path, 'camera2_frames')
|
||||||
|
cam2.start_jpeg_recording(session_id=session_id, frames_dir=cam2_frames_dir)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'启动camera2 JPEG录制失败: {e}')
|
||||||
|
|
||||||
# 处理录制管理器返回的数据库更新信息
|
# 处理录制管理器返回的数据库更新信息
|
||||||
if recording_response and recording_response.get('success') and 'database_updates' in recording_response:
|
if recording_response and recording_response.get('success') and 'database_updates' in recording_response:
|
||||||
@ -1456,7 +1480,10 @@ class AppServer:
|
|||||||
# 保存检测视频记录(映射到 detection_video 表字段)
|
# 保存检测视频记录(映射到 detection_video 表字段)
|
||||||
video_paths = db_updates.get('video_paths', {})
|
video_paths = db_updates.get('video_paths', {})
|
||||||
video_record = {
|
video_record = {
|
||||||
'screen_video_path': video_paths.get('screen_video_path')
|
'screen_video_path': video_paths.get('screen_video_path'),
|
||||||
|
'body_video_path': None,
|
||||||
|
'foot_video1_path': None,
|
||||||
|
'foot_video2_path': None
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -1495,6 +1522,76 @@ class AppServer:
|
|||||||
except Exception as rec_e:
|
except Exception as rec_e:
|
||||||
self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
|
self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
base_info = None
|
||||||
|
try:
|
||||||
|
base_info = self.recording_manager.get_active_video_base(session_id)
|
||||||
|
except Exception:
|
||||||
|
base_info = None
|
||||||
|
|
||||||
|
if base_info and base_info.get('base_path') and base_info.get('file_dir'):
|
||||||
|
base_path = base_info.get('base_path')
|
||||||
|
file_dir = base_info.get('file_dir')
|
||||||
|
|
||||||
|
cam1 = self.device_coordinator.devices.get('camera1') if self.device_coordinator and hasattr(self.device_coordinator, 'devices') else None
|
||||||
|
cam2 = self.device_coordinator.devices.get('camera2') if self.device_coordinator and hasattr(self.device_coordinator, 'devices') else None
|
||||||
|
|
||||||
|
if cam1:
|
||||||
|
try:
|
||||||
|
cam1_stop = cam1.stop_jpeg_recording(session_id=session_id)
|
||||||
|
cam1_frames_dir = cam1_stop.get('frames_dir')
|
||||||
|
cam1_fps = int(cam1_stop.get('fps') or 10)
|
||||||
|
if cam1_frames_dir and os.path.isdir(cam1_frames_dir):
|
||||||
|
out1 = os.path.join(base_path, 'foot1.mp4')
|
||||||
|
rel1 = os.path.relpath(out1, file_dir).replace('\\', '/')
|
||||||
|
def _done1(code: int):
|
||||||
|
if code == 0:
|
||||||
|
try:
|
||||||
|
self.db_manager.update_detection_video_latest(session_id, {'foot_video1_path': rel1})
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'更新foot_video1失败: {e}')
|
||||||
|
try:
|
||||||
|
shutil.rmtree(cam1_frames_dir, ignore_errors=True)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'删除camera1_frames失败: {e}')
|
||||||
|
self.recording_manager.transcode_jpeg_sequence_async(
|
||||||
|
name=f'{session_id}-camera1',
|
||||||
|
frames_dir=cam1_frames_dir,
|
||||||
|
output_mp4_path=out1,
|
||||||
|
fps=cam1_fps,
|
||||||
|
on_done=_done1
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'停止/转码camera1录制失败: {e}')
|
||||||
|
|
||||||
|
if cam2:
|
||||||
|
try:
|
||||||
|
cam2_stop = cam2.stop_jpeg_recording(session_id=session_id)
|
||||||
|
cam2_frames_dir = cam2_stop.get('frames_dir')
|
||||||
|
cam2_fps = int(cam2_stop.get('fps') or 10)
|
||||||
|
if cam2_frames_dir and os.path.isdir(cam2_frames_dir):
|
||||||
|
out2 = os.path.join(base_path, 'foot2.mp4')
|
||||||
|
rel2 = os.path.relpath(out2, file_dir).replace('\\', '/')
|
||||||
|
def _done2(code: int):
|
||||||
|
if code == 0:
|
||||||
|
try:
|
||||||
|
self.db_manager.update_detection_video_latest(session_id, {'foot_video2_path': rel2})
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'更新foot_video2失败: {e}')
|
||||||
|
try:
|
||||||
|
shutil.rmtree(cam2_frames_dir, ignore_errors=True)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'删除camera2_frames失败: {e}')
|
||||||
|
self.recording_manager.transcode_jpeg_sequence_async(
|
||||||
|
name=f'{session_id}-camera2',
|
||||||
|
frames_dir=cam2_frames_dir,
|
||||||
|
output_mp4_path=out2,
|
||||||
|
fps=cam2_fps,
|
||||||
|
on_done=_done2
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'停止/转码camera2录制失败: {e}')
|
||||||
|
|
||||||
return jsonify({'success': True,'msg': '停止录制成功'})
|
return jsonify({'success': True,'msg': '停止录制成功'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f'停止检测失败: {e}', exc_info=True)
|
self.logger.error(f'停止检测失败: {e}', exc_info=True)
|
||||||
|
|||||||
64
backend/tests/test_license_manager_unit.py
Normal file
64
backend/tests/test_license_manager_unit.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
class DummyConfigManager:
|
||||||
|
def __init__(self, values):
|
||||||
|
self._values = values
|
||||||
|
|
||||||
|
def get_config_value(self, section, key, fallback=None):
|
||||||
|
return self._values.get((section, key), fallback)
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_validity_accepts_candidate_machine_id(monkeypatch):
|
||||||
|
from devices.utils.license_manager import LicenseManager
|
||||||
|
|
||||||
|
lm = LicenseManager(config_manager=DummyConfigManager({}))
|
||||||
|
monkeypatch.setattr(lm, "get_machine_id_candidates", lambda: ["MID-PRIMARY", "MID-ALT"])
|
||||||
|
|
||||||
|
license_data = {
|
||||||
|
"product": "BodyBalanceEvaluation",
|
||||||
|
"license_id": "L1",
|
||||||
|
"license_type": "full",
|
||||||
|
"machine_id": "MID-ALT",
|
||||||
|
"issued_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"expires_at": (datetime.now(timezone.utc) + timedelta(days=1)).isoformat().replace("+00:00", "Z"),
|
||||||
|
"signature": "x",
|
||||||
|
"features": {"export": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, msg = lm.check_validity(license_data, machine_id="MID-PRIMARY", grace_days=0)
|
||||||
|
assert ok is True, msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_license_paths_falls_back_to_persistent(monkeypatch, tmp_path):
|
||||||
|
from devices.utils.license_manager import LicenseManager
|
||||||
|
|
||||||
|
programdata = tmp_path / "programdata"
|
||||||
|
persistent_dir = programdata / "BodyCheck" / "license"
|
||||||
|
persistent_dir.mkdir(parents=True)
|
||||||
|
(persistent_dir / "license.json").write_text('{"a":1}', encoding="utf-8")
|
||||||
|
(persistent_dir / "license_public_key.pem").write_text("PUB", encoding="utf-8")
|
||||||
|
|
||||||
|
cfg_dir = tmp_path / "cfg"
|
||||||
|
cfg_license_path = str(cfg_dir / "license.json")
|
||||||
|
cfg_pub_path = str(cfg_dir / "license_public_key.pem")
|
||||||
|
|
||||||
|
cfg = DummyConfigManager(
|
||||||
|
{
|
||||||
|
("LICENSE", "path"): cfg_license_path,
|
||||||
|
("LICENSE", "public_key"): cfg_pub_path,
|
||||||
|
("LICENSE", "grace_days"): "3",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lm = LicenseManager(config_manager=cfg)
|
||||||
|
|
||||||
|
monkeypatch.setenv("PROGRAMDATA", str(programdata))
|
||||||
|
monkeypatch.setattr("platform.system", lambda: "Windows")
|
||||||
|
|
||||||
|
license_path, pub_path, grace_days = lm._resolve_license_paths()
|
||||||
|
assert license_path == str(persistent_dir / "license.json")
|
||||||
|
assert pub_path == str(persistent_dir / "license_public_key.pem")
|
||||||
|
assert grace_days == 3
|
||||||
@ -3,7 +3,7 @@ const path = require('path');
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
const { spawn } = require('child_process');
|
const { spawn, exec, execSync } = require('child_process');
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
let localServer;
|
let localServer;
|
||||||
let backendProcess;
|
let backendProcess;
|
||||||
@ -11,6 +11,24 @@ let splashWindow;
|
|||||||
// app.disableHardwareAcceleration();
|
// app.disableHardwareAcceleration();
|
||||||
app.disableDomainBlockingFor3DAPIs();
|
app.disableDomainBlockingFor3DAPIs();
|
||||||
console.log('Electron version:', process.versions.electron);
|
console.log('Electron version:', process.versions.electron);
|
||||||
|
|
||||||
|
const gotSingleInstanceLock = app.requestSingleInstanceLock();
|
||||||
|
if (!gotSingleInstanceLock) {
|
||||||
|
app.quit();
|
||||||
|
} else {
|
||||||
|
app.on('second-instance', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isMinimized()) {
|
||||||
|
mainWindow.restore();
|
||||||
|
}
|
||||||
|
mainWindow.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (app.isReady()) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
ipcMain.handle('generate-report-pdf', async (event, payload) => {
|
ipcMain.handle('generate-report-pdf', async (event, payload) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
if (!win) throw new Error('窗口未找到');
|
if (!win) throw new Error('窗口未找到');
|
||||||
@ -134,6 +152,21 @@ ipcMain.handle('generate-report-pdf', async (event, payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function startBackendService() {
|
function startBackendService() {
|
||||||
|
if (backendProcess) {
|
||||||
|
console.log('Backend service already started (tracked).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tasklistOut = execSync('tasklist /FI "IMAGENAME eq BodyBalanceBackend.exe" /NH', { windowsHide: true }).toString();
|
||||||
|
if (tasklistOut && tasklistOut.toLowerCase().includes('bodybalancebackend.exe')) {
|
||||||
|
console.log('Backend service already running (tasklist). Skip spawning a new instance.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Backend process detection failed, continue to spawn:', e && e.message ? e.message : e);
|
||||||
|
}
|
||||||
|
|
||||||
// 在打包后的应用中,使用process.resourcesPath获取resources目录
|
// 在打包后的应用中,使用process.resourcesPath获取resources目录
|
||||||
const resourcesPath = process.resourcesPath || path.join(__dirname, '../..');
|
const resourcesPath = process.resourcesPath || path.join(__dirname, '../..');
|
||||||
const backendPath = path.join(resourcesPath, 'backend/BodyBalanceBackend/BodyBalanceBackend.exe');
|
const backendPath = path.join(resourcesPath, 'backend/BodyBalanceBackend/BodyBalanceBackend.exe');
|
||||||
@ -199,7 +232,6 @@ function stopBackendService() {
|
|||||||
|
|
||||||
// 强制杀死所有BodyBalanceBackend.exe进程
|
// 强制杀死所有BodyBalanceBackend.exe进程
|
||||||
try {
|
try {
|
||||||
const { exec } = require('child_process');
|
|
||||||
exec('taskkill /f /im BodyBalanceBackend.exe', (error, stdout, stderr) => {
|
exec('taskkill /f /im BodyBalanceBackend.exe', (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
// 如果没有找到进程,taskkill会返回错误,这是正常的
|
// 如果没有找到进程,taskkill会返回错误,这是正常的
|
||||||
@ -300,6 +332,11 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startLocalServer(callback) {
|
function startLocalServer(callback) {
|
||||||
|
if (localServer) {
|
||||||
|
console.log('Local server already started on http://localhost:3000');
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const staticPath = path.join(__dirname, '../dist/');
|
const staticPath = path.join(__dirname, '../dist/');
|
||||||
|
|
||||||
localServer = http.createServer((req, res) => {
|
localServer = http.createServer((req, res) => {
|
||||||
@ -360,26 +397,28 @@ function startLocalServer(callback) {
|
|||||||
// 应用事件处理
|
// 应用事件处理
|
||||||
// 关闭硬件加速以规避 GPU 进程异常导致的闪烁
|
// 关闭硬件加速以规避 GPU 进程异常导致的闪烁
|
||||||
// app.disableHardwareAcceleration();
|
// app.disableHardwareAcceleration();
|
||||||
app.whenReady().then(createWindow);
|
if (gotSingleInstanceLock) {
|
||||||
|
app.whenReady().then(createWindow);
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
if (localServer) {
|
if (localServer) {
|
||||||
localServer.close();
|
localServer.close();
|
||||||
|
}
|
||||||
|
// 关闭后端服务
|
||||||
|
stopBackendService();
|
||||||
|
app.quit();
|
||||||
}
|
}
|
||||||
// 关闭后端服务
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 应用退出前清理资源
|
||||||
|
app.on('before-quit', () => {
|
||||||
stopBackendService();
|
stopBackendService();
|
||||||
app.quit();
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 应用退出前清理资源
|
|
||||||
app.on('before-quit', () => {
|
|
||||||
stopBackendService();
|
|
||||||
});
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user