From 89e6c9439ca22a55b9e85d4ecd5eb9e142349074 Mon Sep 17 00:00:00 2001 From: Begild Date: Mon, 13 Jan 2025 08:46:49 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E5=B7=B2=E5=9F=BA=E6=9C=AC=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=AE=9E=E7=8E=B0web=20ser?= =?UTF-8?q?ver=E6=98=BE=E7=A4=BA=E4=BD=BF=E7=94=A8=E6=83=85=E5=86=B5?= =?UTF-8?q?=EF=BC=8C=E5=AE=9E=E7=8E=B0=E6=8E=92=E9=98=9F=E7=9A=84=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E3=80=82=E5=AE=9E=E7=8E=B0lic=E7=9A=84=E6=B4=BE?= =?UTF-8?q?=E5=8F=91=E5=8A=9F=E8=83=BD=202.=20=E9=81=97=E7=95=99=E6=9C=89l?= =?UTF-8?q?ic=20client=E7=9A=84=E5=A4=84=E7=90=86=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E4=B8=AD=E6=96=AD=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lic_info.json | 135 +++++++++ license.py | 503 +++++++++++++++++++++++++++------ license_client.py | 315 +++++++++++++++++++++ mock_lmutil.py | 200 +++++++++++++ my_log.py | 55 ++++ server.py | 209 +++++--------- src/components/LicenseBar.js | 76 ++--- src/components/QueueDisplay.js | 12 +- src/services/queueService.js | 7 +- style.css | 68 +++-- 10 files changed, 1274 insertions(+), 306 deletions(-) create mode 100755 lic_info.json mode change 100644 => 100755 license.py create mode 100755 license_client.py create mode 100755 mock_lmutil.py create mode 100755 my_log.py diff --git a/lic_info.json b/lic_info.json new file mode 100755 index 0000000..bff5927 --- /dev/null +++ b/lic_info.json @@ -0,0 +1,135 @@ +{ + "27003@szmaslic03": { + "name": "27003@szmaslic03", + "total": 2, + "used": 2, + "used_info": [ + { + "user": "haoxiang.ran", + "host": "szl3bc12808", + "lic": "szmaslic03", + "login": "0.41h" + }, + { + "user": "ekko.bao", + "host": "qwerqwerq", + "lic": "27003@szmaslic03", + "login": "8.00h" + } + ] + }, + "27003@szmaslic04": { + "name": "27003@szmaslic04", + "total": 2, + "used": 2, + "used_info": [ + { + "user": "macro.yang", + "host": "szl3bc12804", + "lic": "szmaslic04", + "login": "52.28h" + }, + { + "user": "asdfsadf", + "host": "fdgh", + "lic": "27003@szmaslic04", + "login": "1.77h" + } + ] + }, + "27003@szmaslic06": { + "name": "27003@szmaslic06", + "total": 2, + "used": 2, + "used_info": [ + { + "user": "zhenhao.he", + "host": "szl3bc12804", + "lic": "szmaslic06", + "login": "0.20h" + }, + { + "user": "phillip.chan", + "host": "szl3bc12810", + "lic": "szmaslic06", + "login": "0.16h" + } + ] + }, + "27003@szmaslic07": { + "name": "27003@szmaslic07", + "total": 2, + "used": 2, + "used_info": [ + { + "user": "zhiyi.li", + "host": "szl3bc12806", + "lic": "szmaslic07", + "login": "2.96h" + }, + { + "user": "kkkkkkkkkk", + "host": "dddddd", + "lic": "27003@szmaslic07", + "login": "3.04h" + } + ] + }, + "27003@szmaslic08": { + "name": "27003@szmaslic08", + "total": 2, + "used": 2, + "used_info": [ + { + "user": "soo.liu", + "host": "szl3bc12809", + "lic": "szmaslic08", + "login": "6.55h" + }, + { + "user": "haichao.ou", + "host": "szl3bc12806", + "lic": "szmaslic08", + "login": "5.93h" + } + ] + }, + "27003@szmaslic09": { + "name": "27003@szmaslic09", + "total": 2, + "used": 2, + "used_info": [ + { + "user": "louie.liang", + "host": "szl3bc12810", + "lic": "szmaslic09", + "login": "8.31h" + }, + { + "user": "zabbix", + "host": "szl3bc06409", + "lic": "szmaslic09", + "login": "0.90h" + } + ] + }, + "27003@szmaslic10": { + "name": "27003@szmaslic10", + "total": 2, + "used": 2, + "used_info": [ + { + "user": "harvey.li", + "host": "szl3bc12810", + "lic": "szmaslic10", + "login": "57.38h" + }, + { + "user": "kw.hu", + "host": "szl3bc12809", + "lic": "szmaslic10", + "login": "1.41h" + } + ] + } +} \ No newline at end of file diff --git a/license.py b/license.py old mode 100644 new mode 100755 index d96a70e..d0e8c51 --- a/license.py +++ b/license.py @@ -1,25 +1,256 @@ #!/usr/bin/python3 import json -import queue import re -import sys import socket import asyncio +import subprocess import threading import time -from typing import Dict, List +from typing import Dict, List, Optional, Union from datetime import datetime -from utils import * -import logger -log = logger.get_logger() +from my_log import get_logger +import socket +import time +import threading +import select +log = get_logger() + + +MOCK_LMUTIL = True +if MOCK_LMUTIL: + from mock_lmutil import * + +DISPATCHER_SERVER_PORT = 8809 + +class LicenseDispatcher: + def __init__(self, host='0.0.0.0', port=DISPATCHER_SERVER_PORT): + self.host = host + self.port = port + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.server_socket.bind((self.host, self.port)) + self.server_socket.listen(5) + self.inputs = [self.server_socket] # all socket to listen + self.outputs = [] # all socket to write + self.message_queues = {} # store message to send + self.lock = threading.Lock() + self.wait_queue = Queue() + self.exit_event = threading.Event() + self.loop_thread = threading.Thread(target=self.loop) + self.loop_thread.start() + + def exit(self): + self.exit_event.set() + self.loop_thread.join() + + def loop(self): + log.info(f"dispatcher server start, listen {self.host}:{self.port}") + while not self.exit_event.is_set(): + # select listen all socket read/write event + readable, writable, exceptional = select.select(self.inputs, self.outputs, self.inputs, 1) + # handle readable socket + for sock in readable: + if sock is self.server_socket: + # new client connect + self._handle_new_connection() + else: + # handle client data + self._handle_client_data(sock) + # handle writable socket + for sock in writable: + self._handle_client_response(sock) + # handle exception socket + for sock in exceptional: + self._handle_socket_exception(sock) + # clean inactive client + self._cleanup_inactive_clients() + log.debug("LicenseDispatcher exit") + + def _handle_new_connection(self): + with self.lock: + client_socket, address = self.server_socket.accept() + log.info(f"new client connect: {address}") + client_socket.setblocking(0) + self.inputs.append(client_socket) + self.message_queues[client_socket] = [] + + def _handle_client_data(self, sock): + try: + data = sock.recv(1024) + if not data: + return + log.trace(f"recv data: {data}") + try: + req = json.loads(data.decode('utf-8')) + except json.JSONDecodeError: + log.error(f"Invalid JSON data received: {data}") + self._close_connection(sock) + return + req_type = req.get('type', None) + if req_type == 'heartbeat': + self._handle_heartbeat(sock, req) + elif req_type == 'lic_received_ack': + self._handle_lic_received_ack(req) + except Exception as e: + log.error(f"handle client data error: {e}") + self._close_connection(sock) + + def _handle_heartbeat(self, sock, req): + """ 心跳包内容 + { + "type":"heartbeat", + "user":"", + "host":"", + "heartbeat_time":datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + """ + for item in self.wait_queue: + if item['user'] == req['user']: + item['sock'] = sock + item['heartbeat_time'] = datetime.now() + break + else: + log.warning(f"user {req['user']} not in wait queue") + sock.close() + return + # 如果用户已经分配到license,则直接返回 + if item['lic'] != '': + self.send_lic_dispatch(item) + return + response = { + 'type':'heartbeat_ack', + 'status': 'ok', + 'heartbeat_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + with self.lock: + self.message_queues[sock].append(json.dumps(response)) + if sock not in self.outputs: + self.outputs.append(sock) + + def _handle_lic_received_ack(self, req): + self._remove_user(req['user']) + + def _handle_client_response(self, sock): + try: + with self.lock: + if sock in self.message_queues and self.message_queues[sock]: + next_msg = self.message_queues[sock].pop(0) + sock.send(next_msg.encode('utf-8')) + else: + self.outputs.remove(sock) + except Exception as e: + log.error(f"send response error: {e}") + self._close_connection(sock) + + def _handle_socket_exception(self, sock): + log.error(f"handle exception socket: {sock}") + self._close_connection(sock) + + def _remove_user(self, user): + with self.lock: + for idx, item in enumerate(self.wait_queue): + if item['user'] != user: + continue + self.wait_queue.remove(idx) + self._close_connection(item['sock']) + break + + def _close_connection(self, sock): + if not sock: + return + with self.lock: + # 先从所有列表中移除socket + if sock in self.outputs: + self.outputs.remove(sock) + if sock in self.inputs: + self.inputs.remove(sock) + if sock in self.message_queues: + del self.message_queues[sock] + try: + sock.shutdown(socket.SHUT_RDWR) # 先关闭连接 + except: + pass # 忽略已关闭socket的错误 + finally: + sock.close() # 最后关闭socket + + def _cleanup_inactive_clients(self, timeout=3): + """clean inactive client""" + current_time = datetime.now() + remove_list = [] + with self.lock: + for idx, item in enumerate(self.wait_queue): + if (current_time - item['heartbeat_time']).total_seconds() < timeout: + continue + log.info(f"clean inactive client: {item['user']}") + remove_list.append(item['user']) + for user in remove_list: + self._remove_user(user) + + def add_wait_queue(self, user, host): + data = { + "user":user, + "host":host, + 'status':'wait_dispatch', + "start_time":datetime.now(), + 'heartbeat_time':datetime.now(), + 'lic':'', + 'sock':None + } + with self.lock: + self.wait_queue.put(data) + + def dispatch_license(self, lic, available_cnt): + """ + 分配license存在几种情况: + 1. 该用户在等待队列中且没有分配到license,那么就直接分配 + 2. 该用户在等待队列中且已经分配到license,那么就跳过 + 3. 该lic已经被用户占用了,但是这个用户还没有连接,lic处于已经预分配状态,所以也不能再派发给别人 + """ + resp_list = [] + with self.lock: + for item in self.wait_queue: + if available_cnt <= 0: + break + if item['lic'] == lic: # 如果该lic已经被分配了,可用数量减1 + available_cnt -= 1 + continue + if item['status'] != 'wait_dispatch': # 如果该用户无需进行分配,则跳过 + continue + if item['lic'] != '': # 如果该用户已经分配了,则本次分配跳过 + continue + item['lic'] = lic + item['status'] = 'dispatched' + resp_list.append(item) + available_cnt -= 1 + for item in resp_list: + self.send_lic_dispatch(item) + + def send_lic_dispatch(self, item): + sock = item['sock'] #如果sock不存在说明该用户还没连接,等待其连接后再发送lic + if not sock: + return + response = { + 'user':item['user'], + 'host':item['host'], + 'lic':item['lic'], + 'type':'lic_dispatch', + 'status': 'ok', + 'msg':f'{item["user"]} get {item["lic"]} license' + } + with self.lock: + self.message_queues[sock].append(json.dumps(response)) + if sock not in self.outputs: + self.outputs.append(sock) + def _get_ip_address(domain): + if MOCK_LMUTIL: + return mock_ipaddress(domain) try: ip_address = socket.gethostbyname(domain) - # log.debug(f"get_ip_address {domain} {ip_address}") return ip_address except socket.gaierror as e: - # print(f"Error retrieving IP address: {e}") + log.error(f"get ip address {domain} error: {e}") return None def _get_valid_licenses(): @@ -38,20 +269,22 @@ class UsedInfo: self.lic = '' self.start_time:datetime = None def __str__(self): - time = datetime.now() - self.start_time - login_time = (time.days * 24 + time.seconds / 3600.0) - obj = { - "user":self.user, - "host":self.host, - 'lic':self.lic, - 'login':f'{login_time:.2f}h' - } + obj = self.as_dict() return str(obj) def __repr__(self): return self.__str__() + def as_dict(self): + time = datetime.now() - self.start_time + login_time = (time.days * 24 + time.seconds / 3600.0) + return { + "user":self.user, + "host":self.host, + "lic":self.lic, + "login":f'{login_time:.2f}h' + } class LicenseInfo: - def __init__(self, name='', total=0, used=0): + def __init__(self, name: str = '', total: int = 0, used: int = 0): self.name = name self.total = total self.used = used @@ -59,68 +292,150 @@ class LicenseInfo: def available(self): return self.total - self.used - + def __str__(self): - obj = { - "name":self.name, - "total":self.total, - "used":self.used, - "used_info":self.used_info - } + obj = self.as_dict() return str(obj) def __repr__(self): return self.__str__() - + + def as_dict(self): + info = [] + for used in self.used_info: + info.append(used.as_dict()) + return { + "name":self.name, + "total":self.total, + "used":self.used, + "used_info":info + } + +class Queue: + def __init__(self): + self.queue = [] # 使用列表作为底层存储 + self.lock = threading.Lock() # 创建一个锁 + + def put(self, item): + with self.lock: + self.queue.append(item) # 添加元素到队列 + + def pop(self): + with self.lock: + if not self.is_empty(): + return self.queue.pop(0) # 从队列头部移除并返回元素 + else: + return None # 如果队列为空,返回None + def get(self, idx): + with self.lock: + if not self.is_empty(): + return self.queue[idx] + else: + return None + def __getitem__(self, idx): + with self.lock: + return self.queue[idx] + def is_empty(self): + with self.lock: + return len(self.queue) == 0 # 检查队列是否为空 + + def qsize(self): + with self.lock: + return len(self.queue) # 返回队列大小 + + def remove(self, idx): + with self.lock: + if 0 <= idx < len(self.queue): + return self.queue.pop(idx) + return None class LicenseManager: def __init__(self): - self.wait_queue = queue.Queue() - self.ack_queue = queue.Queue() self.licenses_stat = LicenseStatistic() self.licenses:Dict[str,LicenseInfo] = {} - #启动定时获取 - self.licenses_stat.lic_info_updater() - - def lic_info_updater(self): - self.update() - if self.wait_queue.qsize() > 0: - for lic, available_cnt in self.licenses_stat.available(): - for i in range(available_cnt): - req = self.wait_queue.get() - if req == None: - return - req["lic"] = lic - self.ack_queue.put(req) - threading.Timer(10, self.lic_info_updater).start() #每间隔一段时间更新一次lic的使用情况 + self.dispatcher = LicenseDispatcher() + self.exit_event = threading.Event() + self.timer = None + self.start_updater() # 替换原来的线程启动 - def _in_wait_queue(self, user): - return any(user == _.user for _ in list(self.wait_queue)) + def start_updater(self): + if not self.exit_event.is_set(): + self.update_once() + # 设置下一次执行 + self.timer = threading.Timer(5.0, self.start_updater) + self.timer.start() - def request(self, user:str, host:str): + def update_once(self): + """单次更新操作""" + self.licenses_stat.update() + if self.dispatcher.wait_queue.qsize() > 0: + for lic, available_cnt, _ in self.licenses_stat.available(): + self.dispatcher.dispatch_license(lic, available_cnt) + + def request(self, user: str, host: str) -> Dict[str, Union[str, dict]]: #如果已经是在线的状态就不允许申请 - if any(user == _.user for _ in self.licenses_stat.online_users()): - log.warning(f"{user} is online, please try again later") - return False - #如果是已经在队列中则返回等待 - if self._in_wait_queue(user): - return None - - data = { - "user":user, - "host":host, - "start_time":time.now() + rep = { + "user":user, + "host":host, + "status":"refuse", + "lic":'', + "msg":f"unknown error" } - self.wait_queue.put(data) - return False - - def cancel(self): - pass - - def release(self): - pass - + if self.dispatcher.wait_queue.qsize() > 100: + rep["status"] = "refuse" + rep["msg"] = f"{user} queue is full!!" + return rep + if user in self.licenses_stat.online_users(): + rep["msg"] = f"{user} is online" + log.warning(rep["msg"]) + return rep + + #如果是已经在队列中则返回等待 + item = next((item for item in self.dispatcher.wait_queue if item['user'] == user), None) + if item: + rep["status"] = "queue" + rep["msg"] = f"{user} is in queue!!" + rep["start_time"] = item['start_time'].strftime("%Y-%m-%d %H:%M:%S") + return rep + log.info(f"add {user} to wait queue") + # 如果队列为空,则直接分配一个license + if self.dispatcher.wait_queue.qsize() == 0: + self.dispatcher.add_wait_queue(user, host) + lic, available_cnt, _ = next(self.licenses_stat.available(), ('', 0, 0)) + self.dispatcher.dispatch_license(lic, available_cnt) + else: + self.dispatcher.add_wait_queue(user, host) + rep["status"] = "wait_dispatch" + rep["msg"] = f"{user} add to queue" + rep["dispatcher_server"] = { + "port": DISPATCHER_SERVER_PORT + } + return rep + def get_wait_queue(self): - return [_ for _ in self.wait_queue.queue] + resp = [] + for item in self.dispatcher.wait_queue: + resp.append({ + "user":item['user'], + "host":item['host'], + "status":item['status'], + "wait_time":int((datetime.now() - item['start_time']).total_seconds()) + }) + return resp + + def get_licenses_info(self): + resp = {} + for lic, info in self.licenses_stat.licenses.items(): + info = info.as_dict() + resp[lic] = info + return resp + + def exit(self): + self.exit_event.set() + if self.timer: + self.timer.cancel() + self.timer = None + if self.dispatcher: + self.dispatcher.exit() class LicenseStatistic: def __init__(self): @@ -132,13 +447,23 @@ class LicenseStatistic: for lic in _get_valid_licenses(): log.trace(f'find valid lic: {lic}') tasks.append(get_license_used_info(lic)) - loop = asyncio.get_event_loop() - infos = loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) - for info in infos: - if info is not None: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + infos = loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) + for info in infos: + if info is None: + continue + if not isinstance(info, LicenseInfo): + log.error(f"get_license_used_info {info} is not LicenseInfo") + continue self.licenses[info.name] = info - if not self.licenses: - log.warning("not vcast lic found") + if not self.licenses: + log.warning("not vcast lic found") + except Exception as e: + log.error(f"update license info error: {e}") + loop.close() + def get_lic_list(self): # 获取所有的license名称 return self.licenses.keys() @@ -148,8 +473,8 @@ class LicenseStatistic: for _,lic in self.licenses.items(): value = lic.available() if value > 0: - yield (lic.name, value) #lic的name和个数 - + yield (lic.name, value, lic.total) #lic的name和个数 + def online_users(self): # 获取在线用户 for used_info in self.used(): @@ -160,7 +485,7 @@ class LicenseStatistic: for _,info in self.licenses.items(): for used in info.used_info: yield used #返回的是UsedInfo对象 - + def sorted(self): info = {} for used_info in self.used(): @@ -179,11 +504,30 @@ class LicenseStatistic: def __repr__(self): return self.__str__() + +def shell(cmd, checkret=True): + if MOCK_LMUTIL: + ret = mock_lmutil(cmd) + if not ret: + return 1, '', '' + return 0, ret, '' + process = subprocess.Popen(cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True) + out, err = process.communicate() + ret = process.returncode + if checkret and ret != 0: + raise subprocess.CalledProcessError(ret, cmd, output=out, stderr=err) + return ret, out, err + """ {"27003@szmaslic03": {"name": "27003@szmaslic03", "total": 2, "used": 1, "used_info": "[{"name": "haoxiang.ran", "host": "szl3bc12808", "lic": "szmaslic03", "login": "0.41"}]"}, "27003@szmaslic04": {"name": "27003@szmaslic04", "total": 2, "used": 1, "used_info": "[{"name": "macro.yang", "host": "szl3bc12804", "lic": "szmaslic04", "login": "52.28"}]"}, "27003@szmaslic06": {"name": "27003@szmaslic06", "total": 2, "used": 2, "used_info": "[{"name": "zhenhao.he", "host": "szl3bc12804", "lic": "szmaslic06", "login": "0.20"}, {"name": "phillip.chan", "host": "szl3bc12810", "lic": "szmaslic06", "login": "0.16"}]"}, "27003@szmaslic07": {"name": "27003@szmaslic07", "total": 2, "used": 1, "used_info": "[{"name": "zhiyi.li", "host": "szl3bc12806", "lic": "szmaslic07", "login": "2.96"}]"}, "27003@szmaslic08": {"name": "27003@szmaslic08", "total": 2, "used": 2, "used_info": "[{"name": "soo.liu", "host": "szl3bc12809", "lic": "szmaslic08", "login": "6.55"}, {"name": "haichao.ou", "host": "szl3bc12806", "lic": "szmaslic08", "login": "5.93"}]"}, "27003@szmaslic09": {"name": "27003@szmaslic09", "total": 2, "used": 2, "used_info": "[{"name": "louie.liang", "host": "szl3bc12810", "lic": "szmaslic09", "login": "8.31"}, {"name": "zabbix", "host": "szl3bc06409", "lic": "szmaslic09", "login": "0.90"}]"}, "27003@szmaslic10": {"name": "27003@szmaslic10", "total": 2, "used": 2, "used_info": "[{"name": "harvey.li", "host": "szl3bc12810", "lic": "szmaslic10", "login": "57.38"}, {"name": "kw.hu", "host": "szl3bc12809", "lic": "szmaslic10", "login": "1.41"}]"}} """ async def get_license_used_info(lic): - server_used_info = f"timeout 1 /tools/software/vcast/flexlm/lmutil lmstat -a -c {lic} -a" + # log.info(f"get_license_used_info {lic}") + server_used_info = f"timeout 1 /tools/software/vcast/flexlm/lmutil lmstat -a -c {lic}" ret,out,err = shell(server_used_info, checkret=False) # log.info(f"get_license_used_info {lic} {out} {ret} {err}") if ret != 0: @@ -218,7 +562,7 @@ async def get_license_used_info(lic): ret = re.findall(pattern, line) if not ret: log.warning(f"{lic} parser [{line}] fail") - return None + return info (total, used) = ret[0] info.total = int(total) info.used = int(used) @@ -258,10 +602,7 @@ if __name__ == '__main__': log.info("License status query...") lm = LicenseStatistic() log.info(lm) - available_lic = lm.available() - if not available_lic: - log.info("No license available") log.info(f"All licenses is: {[str(_) for _ in lm.get_lic_list()]}") - for lic,number in lm.available(): + for lic,number,total in lm.available(): log.info(f"{lic}: {number} lic available") - draw_time_grpah(lm) \ No newline at end of file + draw_time_grpah(lm) diff --git a/license_client.py b/license_client.py new file mode 100755 index 0000000..2c7e6e7 --- /dev/null +++ b/license_client.py @@ -0,0 +1,315 @@ +#!/usr/bin/python3 +import json +import os +import socket +import threading +import time +from datetime import datetime +import http.client +import sys +from my_log import get_logger +from mock_lmutil import mock_license_add_user, mock_license_remove_user, _read_sample_file, _write_sample_file +import queue +import select # 添加 select 模块导入 +import signal + + +log = get_logger() + +server = os.environ.get('SSTAR_VCAST_LICENSE_SERVER_HOST') # Use get() to avoid KeyError +SERVER_HOST = server if server else '127.0.0.1' +SERVER_PORT = 8088 +SERVER_URL = f'http://{SERVER_HOST}:{SERVER_PORT}' + +class LicenseDispatcherClient: + def __init__(self, user: str = '', host: str = '') -> None: + self.server_url = SERVER_URL # Use global configuration + self.user = user + self.host = host + self.dispatcher_socket: socket.socket = None + self.exit_event = threading.Event() + self.lic: queue.Queue = queue.Queue() + self.current_lic: str = None + self.dispatcher_timer: threading.Timer = None # Add missing attribute + self._lock = threading.Lock() # Add thread safety + self.heartbeat_thread: threading.Thread | None = None # Add thread reference + + def request(self) -> str: + """Request license, block until license is obtained or user interrupts""" + response = self._request_license() + if response['status'] == 'refuse': + log.error(response['msg']) + return None + elif response['status'] == 'wait_dispatch': + # Start heartbeat thread + log.info("Waiting for license allocation...") + self._start_dispatcher_client(response['dispatcher_server']['port']) + lic = self.get_lic() + return lic + + def get_lic(self) -> str: + """Get the assigned license""" + self.current_lic = self.lic.get() + return self.current_lic + + def _request_license(self) -> dict: + """Send license request to server""" + conn = None + try: + log.info(f"Requesting license: {self.user}@{self.host}") + conn = http.client.HTTPConnection(SERVER_HOST, SERVER_PORT, timeout=10) # Add timeout + headers = {'Content-Type': 'application/json'} + data = { + "user": self.user, + "host": self.host + } + conn.request('GET', '/vcast/request_lic', + body=json.dumps(data), + headers=headers) + response = conn.getresponse() + response_data = response.read().decode() + return json.loads(response_data) + except (socket.timeout, ConnectionRefusedError) as e: + log.error(f"Connection error: {e}") + return {"status": "refuse", "msg": str(e)} + except Exception as e: + log.error(f"Requesting license error: {e}") + return {"status": "error", "msg": str(e)} + finally: + if conn: + conn.close() + + def _start_dispatcher_client(self, port: int) -> None: + """Start dispatcher connection with server""" + log.info(f"Starting dispatcher: {port}") + self.dispatcher_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.dispatcher_socket.connect((SERVER_HOST, port)) + start_time = time.time() + + def heartbeat_loop(): + while not self.exit_event.is_set(): + try: + # Send heartbeat + heartbeat = { + "type": "heartbeat", + "user": self.user, + "host": self.host, + "heartbeat_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + self.dispatcher_socket.send(json.dumps(heartbeat).encode('utf-8')) + + # Wait for response with timeout + readable, _, _ = select.select([self.dispatcher_socket], [], [], 0.5) + + if self.dispatcher_socket in readable: + data = self.dispatcher_socket.recv(1024) + if not data: + log.warning("Connection closed by server") + break + + response = json.loads(data.decode()) + if response.get('type') == 'lic_dispatch': + self.lic.put(response['lic']) + log.info(f"Received license: {response['lic']}") + # Send acknowledgment + ack = { + "type": "lic_received_ack", + "user": self.user, + "host": self.host + } + self.dispatcher_socket.send(json.dumps(ack).encode('utf-8')) + break + elif response.get('type') == 'heartbeat_ack': + wait_time = int(time.time() - start_time) + print(f"\rwaiting... {wait_time}s", end='', flush=True) + elif response.get('type') == 'queue': + wait_time = int(time.time() - start_time) + log.info(f"waiting... {wait_time}s") + + time.sleep(0.5) # Small sleep to prevent CPU spinning + except Exception as e: + log.error(f"Error in heartbeat loop: {e}") + break + + self.lic.put(None) # Signal end of heartbeat loop + + # Start heartbeat thread + self.heartbeat_thread = threading.Thread(target=heartbeat_loop, daemon=True) + self.heartbeat_thread.start() + + def release(self) -> None: + """Release the current license""" + log.info("License released") + + def exit(self) -> None: + """Exit the client and cleanup resources""" + try: + self.exit_event.set() + + # Wait for heartbeat thread to finish with timeout + if self.heartbeat_thread and self.heartbeat_thread.is_alive(): + self.heartbeat_thread.join(timeout=1.0) + + with self._lock: + if self.dispatcher_socket: + try: + self.dispatcher_socket.shutdown(socket.SHUT_RDWR) + except Exception: + pass + self.dispatcher_socket.close() + self.dispatcher_socket = None + + self.release() + self.lic.put(None) + except Exception as e: + log.error(f"Error during exit: {e}") + +# Global flag for exit control +is_exiting = False +clients: dict[str, LicenseDispatcherClient] = {} + +def signal_handler(signum, frame): + """Handle interrupt signals""" + global is_exiting + if is_exiting: + log.info("\nForce exiting...") + sys.exit(1) + + log.info("\nReceived interrupt signal, cleaning up...") + is_exiting = True + cleanup_and_exit() + +def cleanup_and_exit(): + """Cleanup all resources and exit""" + for client_key, client in list(clients.items()): + try: + log.info(f"Cleaning up client: {client_key}") + client.exit() + del clients[client_key] + except Exception as e: + log.error(f"Error cleaning up client {client_key}: {e}") + log.info("\nProgram exited") + sys.exit(0) + +def main() -> None: + # Register signal handlers + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + # Make sure SIGINT is not ignored + if os.name == 'posix': + signal.siginterrupt(signal.SIGINT, True) + + while not is_exiting: + show_menu() + # Set timeout for input operations + cmd = input("Please select an operation: ").strip() + + if is_exiting: # Check if exit flag was set during input + break + + if cmd == '1': + create_client() + elif cmd == '2': + release_client() + elif cmd == '3': + show_clients() + elif cmd == '4': + break + else: + print("Invalid command") + if not is_exiting: + cleanup_and_exit() + +def create_client(): + """Create new license client""" + global is_exiting + username = input("Please enter username: ").strip() + hostname = input("Please enter hostname: ").strip() + if is_exiting: + return + + if not username or not hostname: + print("Username and hostname cannot be empty") + return + + client_key = f"{username}@{hostname}" + if client_key in clients: + print(f"Client {client_key} already exists") + return + + client = LicenseDispatcherClient(user=username, host=hostname) + lic = client.request() + if lic: + clients[client_key] = client + # Add mock license here + mock_license_add_user(lic, username, hostname) + print(f"{client_key} successfully created and requested license: {lic}") + else: + print(f"{client_key} License request failed") + +def release_client(): + """Release existing license""" + lic_use_info = _read_sample_file() + if not lic_use_info: + print("No license usage record currently") + return + + print("\nCurrent license usage:") + users_list = [] + + # Iterate through all license server information + for server_info in lic_use_info.values(): + for used_info in server_info.get("used_info", []): + user = used_info["user"] + host = used_info["host"] + lic = used_info["lic"] + users_list.append((user, host, lic)) + print(f"{len(users_list)}: user: {user}, host: {host}, lic: {lic}") + + if not users_list: + print("No valid user records found") + return + + try: + choice = int(input("\nPlease select the license number to release (0 to cancel): ")) + except ValueError: + print("Please enter a valid number") + if choice == 0: + return + if 1 <= choice <= len(users_list): + user, host, lic = users_list[choice-1] + client_key = f"{user}@{host}" + lic_use_info = _read_sample_file() + + # If the client exists in the clients dictionary, release it + if client_key in clients: + client = clients[client_key] + client.release() + client.exit() + del clients[client_key] + + # Remove mock license record + mock_license_remove_user(user, host) + print(f"Released license: user: {user}, host: {host}, lic: {lic}") + else: + print("Invalid selection") + +def show_clients(): + if not clients: + print("No active clients currently") + return + + print("\nCurrent active clients:") + for key, client in clients.items(): + lic_status = "License obtained" if client.current_lic else "Waiting" + print(f"- {key}: {lic_status}") + +def show_menu() -> None: + print("\n=== License Management System ===") + print("1: Create new license request") + print("2: Release license") + print("3: Show current clients") + print("4: Exit") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/mock_lmutil.py b/mock_lmutil.py new file mode 100755 index 0000000..a0b244e --- /dev/null +++ b/mock_lmutil.py @@ -0,0 +1,200 @@ +from datetime import datetime +import json +import time +import random + +global MOCK_LIC_USE_INFO +MOCK_LIC_USE_INFO = { + "27003@szmaslic03": { + "name": "27003@szmaslic03", + "total": 2, + "used": 1, + "used_info": [ + { + "user": "haoxiang.ran", + "host": "szl3bc12808", + "lic": "szmaslic03", + "login": "0.41h" + } + ] + }, + "27003@szmaslic04": { + "name": "27003@szmaslic04", + "total": 2, + "used": 1, + "used_info": [ + { + "user": "macro.yang", + "host": "szl3bc12804", + "lic": "szmaslic04", + "login": "52.28h" + } + ] + }, + "27003@szmaslic06": { + "name": "27003@szmaslic06", + "total": 2, + "used": 2, + "used_info": [ + { + "user": "zhenhao.he", + "host": "szl3bc12804", + "lic": "szmaslic06", + "login": "0.20h" + }, + { + "user": "phillip.chan", + "host": "szl3bc12810", + "lic": "szmaslic06", + "login": "0.16h" + } + ] + }, + "27003@szmaslic07": { + "name": "27003@szmaslic07", + "total": 2, + "used": 1, + "used_info": [ + { + "user": "zhiyi.li", + "host": "szl3bc12806", + "lic": "szmaslic07", + "login": "2.96h" + } + ] + }, + "27003@szmaslic08": { + "name": "27003@szmaslic08", + "total": 2, + "used": 2, + "used_info": [ + { + "user": "soo.liu", + "host": "szl3bc12809", + "lic": "szmaslic08", + "login": "6.55h" + }, + { + "user": "haichao.ou", + "host": "szl3bc12806", + "lic": "szmaslic08", + "login": "5.93h" + } + ] + }, + "27003@szmaslic09": { + "name": "27003@szmaslic09", + "total": 2, + "used": 2, + "used_info": [ + { + "user": "louie.liang", + "host": "szl3bc12810", + "lic": "szmaslic09", + "login": "8.31h" + }, + { + "user": "zabbix", + "host": "szl3bc06409", + "lic": "szmaslic09", + "login": "0.90h" + } + ] + }, + "27003@szmaslic10": { + "name": "27003@szmaslic10", + "total": 2, + "used": 2, + "used_info": [ + { + "user": "harvey.li", + "host": "szl3bc12810", + "lic": "szmaslic10", + "login": "57.38h" + }, + { + "user": "kw.hu", + "host": "szl3bc12809", + "lic": "szmaslic10", + "login": "1.41h" + } + ] + } +} + +def _read_sample_file(file_name:str='lic_info.json'): + with open(file_name, 'r') as f: + return json.load(f) + +def _write_sample_file(lic_use_info, file_name:str='lic_info.json'): + with open(file_name, 'w') as f: + json.dump(lic_use_info, f, indent=4) + +def mock_ipaddress(domain): + id = int(domain[-2:]) + if id > 10: + return None + return f"192.168.1.{id}" + +def mock_lmutil(cmd): + lic_name = cmd.split(" ")[-1] + lic_use_info = _read_sample_file() + info = lic_use_info.get(lic_name, None) + if info is None: + return '' + used_template = " {user} {host} /dev/tty (v23) ({lic}/27003 337), start {start_time}\n" + template = """ +Users of VCAST_C_ENT_0800: (Total of {total} licenses issued; Total of {used} licenses in use) + +"VCAST_C_ENT_0800" v23, vendor: vector, expiry: permanent(no expiration date) +vendor_string: CUST:64925: +floating license + +{used_info} + +Users of CCAST_0801: (Total of 2 licenses issued; Total of 0 licenses in use) +""" + used_info = '' + for item in info.get("used_info", []): + start_time = time.time() - float(item.get("login")[:-1]) * 3600 + start_time = datetime.fromtimestamp(start_time) + start_time = start_time.strftime("%a %m/%d %H:%M") + used_info += used_template.format(user=item.get("user"), host=item.get("host"), lic=item.get("lic"), start_time=start_time) + ret = template.format(total=info.get("total"), used=info.get("used"), used_info=used_info) + return ret + + +def mock_license_add_user(lic, user, host): + key = lic + lic_use_info = _read_sample_file() + temp = lic_use_info.get(key, None) + used_info = { + "user": user, + "host": host, + "lic": lic, + "login": f"{random.randint(1, 1000)/ 100.0:.2f}h" + } + if temp is None: + lic_use_info[key] = { + "name": key, + "total": 2, + "used": 0, + "used_info": [used_info] + } + else: + lic_use_info[key]["used"] += 1 + lic_use_info[key]["used_info"].append(used_info) + _write_sample_file(lic_use_info) + +def mock_license_remove_user(user, host): + lic_use_info = _read_sample_file() + for _, info in lic_use_info.items(): + for item in info.get("used_info", []): + if item.get("user") == user and item.get("host") == host: + info["used"] -= 1 + info["used_info"].remove(item) + _write_sample_file(lic_use_info) + + +if __name__ == "__main__": + _write_sample_file(MOCK_LIC_USE_INFO) diff --git a/my_log.py b/my_log.py new file mode 100755 index 0000000..7bc436b --- /dev/null +++ b/my_log.py @@ -0,0 +1,55 @@ +import logging +import sys +from datetime import datetime + +# 定义日志级别 +TRACE = 5 # 自定义TRACE级别 +logging.addLevelName(TRACE, 'TRACE') + +class CustomLogger(logging.Logger): + def __init__(self, name): + super().__init__(name) + + def trace(self, msg, *args, **kwargs): + """添加TRACE级别的日志方法""" + if self.isEnabledFor(TRACE): + self._log(TRACE, msg, args, **kwargs) + +def get_logger(name='vcast_license'): + """获取自定义logger实例""" + # 注册自定义Logger类 + logging.setLoggerClass(CustomLogger) + + # 创建logger + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + + # 如果logger已经有处理器,直接返回 + if logger.handlers: + return logger + + # 创建控制台处理器 + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(logging.DEBUG) + + # 创建格式化器 + formatter = logging.Formatter( + '%(asctime)s [%(levelname)s] [%(name)s:%(lineno)d] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + console_handler.setFormatter(formatter) + + # 添加处理器到logger + logger.addHandler(console_handler) + + return logger + +# 测试代码 +if __name__ == '__main__': + log = get_logger() + log.trace('这是一条TRACE日志') + log.debug('这是一条DEBUG日志') + log.info('这是一条INFO日志') + log.warning('这是一条WARNING日志') + log.error('这是一条ERROR日志') + log.critical('这是一条CRITICAL日志') \ No newline at end of file diff --git a/server.py b/server.py index 44a441d..76db05b 100755 --- a/server.py +++ b/server.py @@ -1,139 +1,33 @@ #!/usr/bin/python3 -from http.server import BaseHTTPRequestHandler, HTTPServer +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import json import os from pathlib import Path +from license import LicenseManager +from my_log import get_logger +log = get_logger() -sample_data = { - "27003@szmaslic03": { - "name": "27003@szmaslic03", - "total": 2, - "used": 1, - "used_info": [ - { - "user": "haoxiang.ran", - "host": "szl3bc12808", - "lic": "szmaslic03", - "login": "0.41h" - } - ] - }, - "27003@szmaslic04": { - "name": "27003@szmaslic04", - "total": 2, - "used": 1, - "used_info": [ - { - "user": "macro.yang", - "host": "szl3bc12804", - "lic": "szmaslic04", - "login": "52.28h" - } - ] - }, - "27003@szmaslic06": { - "name": "27003@szmaslic06", - "total": 2, - "used": 2, - "used_info": [ - { - "user": "zhenhao.he", - "host": "szl3bc12804", - "lic": "szmaslic06", - "login": "0.20h" - }, - { - "user": "phillip.chan", - "host": "szl3bc12810", - "lic": "szmaslic06", - "login": "0.16h" - } - ] - }, - "27003@szmaslic07": { - "name": "27003@szmaslic07", - "total": 2, - "used": 1, - "used_info": [ - { - "user": "zhiyi.li", - "host": "szl3bc12806", - "lic": "szmaslic07", - "login": "2.96h" - } - ] - }, - "27003@szmaslic08": { - "name": "27003@szmaslic08", - "total": 2, - "used": 2, - "used_info": [ - { - "user": "soo.liu", - "host": "szl3bc12809", - "lic": "szmaslic08", - "login": "6.55h" - }, - { - "user": "haichao.ou", - "host": "szl3bc12806", - "lic": "szmaslic08", - "login": "5.93h" - } - ] - }, - "27003@szmaslic09": { - "name": "27003@szmaslic09", - "total": 2, - "used": 2, - "used_info": [ - { - "user": "louie.liang", - "host": "szl3bc12810", - "lic": "szmaslic09", - "login": "8.31h" - }, - { - "user": "zabbix", - "host": "szl3bc06409", - "lic": "szmaslic09", - "login": "0.90h" - } - ] - }, - "27003@szmaslic10": { - "name": "27003@szmaslic10", - "total": 2, - "used": 2, - "used_info": [ - { - "user": "harvey.li", - "host": "szl3bc12810", - "lic": "szmaslic10", - "login": "57.38h" - }, - { - "user": "kw.hu", - "host": "szl3bc12809", - "lic": "szmaslic10", - "login": "1.41h" - } - ] - } -} +global LIC_MANAGER +LIC_MANAGER = None class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): + def __init__(self, *args, **kwargs): + self.handler_map = { + '/vcast/lic_use_info': self.send_lic_info, + '/vcast/request_lic': self.send_request_lic, + '/vcast/queue_info': self.send_queue_info, + } + super().__init__(*args, **kwargs) + def log_message(self, format, *args): return # 重写该方法以禁用日志输出 def do_GET(self): - if self.path == '/vcast/lic_use_info': - self.send_response(200) - self.send_header('Content-type', 'text/json') - self.end_headers() - data = json.dumps(sample_data) - self.wfile.write(data.encode('utf-8')) + if self.path in self.handler_map: + self.handler_map[self.path]() + self.wfile.flush() + self.close_connection = True return path = self.path if path == '/': @@ -141,16 +35,60 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): path = path.strip('/') if not self.send_file(path): self.send_404() + self.wfile.flush() + self.close_connection = True + + def send_lic_info(self): + data = LIC_MANAGER.get_licenses_info() + resp_str = json.dumps(data).encode('utf-8') + self.send_response(200) + self.send_header('Content-type', 'text/json') + self.send_header('Content-Length', len(resp_str)) + self.end_headers() + self.wfile.write(resp_str) + self.wfile.flush() + self.close_connection = True + + def send_queue_info(self): + data = LIC_MANAGER.get_wait_queue() + resp_str = json.dumps(data).encode('utf-8') + self.send_response(200) + self.send_header('Content-type', 'text/json') + self.send_header('Content-Length', len(resp_str)) + self.end_headers() + self.wfile.write(resp_str) + self.wfile.flush() + self.close_connection = True + + def send_request_lic(self): + content_length = int(self.headers.get('Content-Length')) + data = self.rfile.read(content_length) + data = json.loads(data) + log.info(f"request license: {data['user']}@{data['host']}") + resp = LIC_MANAGER.request(data["user"], data["host"]) + resp_str = json.dumps(resp).encode('utf-8') + self.send_response(200) + self.send_header('Content-type', 'text/json') + self.send_header('Content-Length', len(resp_str)) + self.end_headers() + self.wfile.write(resp_str) + self.wfile.flush() + self.close_connection = True def send_404(self): self.send_response(404) self.send_header('Content-type', 'text/html') + resp_str = "

404 Not Found

The page you are looking for does not exist.

".encode('utf-8') + self.send_header('Content-Length', len(resp_str)) self.end_headers() - self.wfile.write(b"

404 Not Found

The page you are looking for does not exist.

") + self.wfile.write(resp_str) + self.wfile.flush() + self.close_connection = True + def send_file(self, file_path) -> bool: file_path =str(Path(os.path.abspath(__file__)).parent.joinpath(file_path)) - print(f"clinet request: [{file_path}]") + log.info(f"clinet request: [{file_path}]") if not os.path.isfile(file_path): return False self.send_response(200) @@ -177,14 +115,21 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): return 'text/css' # 添加更多 MIME 类型根据需要 return 'application/octet-stream' # 默认类型 -def run(server_class=HTTPServer, handler_class=SimpleHTTPRequestHandler, port=8088): +def run(server_class=ThreadingHTTPServer, handler_class=SimpleHTTPRequestHandler, port=8088): server_address = ('0.0.0.0', port) httpd = server_class(server_address, handler_class) - print(f'Starting server on {server_address[0]}:{server_address[1]} ...') + log.info(f'Starting threaded server on http://{server_address[0]}:{server_address[1]} ...') httpd.serve_forever() if __name__ == "__main__": - if len(os.sys.argv) > 1: - run(port=int(os.sys.argv[1])) - else: - run() + LIC_MANAGER = LicenseManager() + try: + if len(os.sys.argv) > 1: + run(port=int(os.sys.argv[1])) + else: + run() + except KeyboardInterrupt: + log.info("Keyboard interrupt received, shutting down...") + finally: + LIC_MANAGER.exit() + log.info("Server stopped") diff --git a/src/components/LicenseBar.js b/src/components/LicenseBar.js index 69e999d..3339155 100755 --- a/src/components/LicenseBar.js +++ b/src/components/LicenseBar.js @@ -16,12 +16,10 @@ export function createLicenseBar(license, max_duration) { // Create bars for each slot for (let i = 0; i < license.totalSlots; i++) { const usage = license.usageDetails[i]; - const bar = document.createElement('div'); - bar.className = 'bar-container'; - - const barContainer = document.createElement('div'); - barContainer.className = 'bar-wrapper'; + const barWrapper = document.createElement('div'); + barWrapper.className = 'bar-wrapper'; + console.log(usage) if (!usage) { const bar = document.createElement('div'); @@ -34,77 +32,51 @@ export function createLicenseBar(license, max_duration) { requestAnimationFrame(() => { label.style.transform = 'translate(0%)'; }); + barWrapper.appendChild(bar); } else { + const barContainer = document.createElement('div'); + barContainer.className = 'bar-container'; + const bar = document.createElement('div'); bar.className = 'bar'; const width = usage.duration / max_duration * 100; // 创建标签容器 - const labelContainer = document.createElement('div'); - labelContainer.className = 'label-container'; + // const labelContainer = document.createElement('div'); + // labelContainer.className = 'label-container'; // 用户信息标签 const userLabel = document.createElement('div'); userLabel.className = 'user-label'; userLabel.textContent = usage.userName; + const blankDiv = document.createElement('div'); + blankDiv.className = 'blank-div'; + blankDiv.textContent = ' '; + // 时长标签 const durationLabel = document.createElement('div'); durationLabel.className = 'duration-label'; durationLabel.textContent = `${usage.duration.toFixed(1)}h`; - if (width < 50) { - labelContainer.appendChild(durationLabel); - bar.appendChild(userLabel); - } else { - bar.appendChild(userLabel); - labelContainer.appendChild(durationLabel); - } - // const label = document.createElement('div'); - // label.className = 'bar-label'; - // label.textContent = `${usage.userName} (${usage.duration}h)`; - // // const hostLabel = document.createElement('div'); - // // hostLabel.className = 'user_info-label'; - // // hostLabel.textContent = ``; - // // // hostLabel.textContent = `${usage.userName}@${usage.hostName}`; + bar.appendChild(userLabel); + bar.appendChild(blankDiv); + bar.appendChild(durationLabel); - // // barElement.appendChild(hostLabel); - // barElement.appendChild(label); - // 将标签添加到bar容器中 - barContainer.appendChild(labelContainer); - barContainer.appendChild(bar); + barWrapper.appendChild(bar); requestAnimationFrame(() => { - const user_width = userLabel.offsetWidth; - const duration_width = labelContainer.offsetWidth - const duration_per = duration_width / bar.offsetWidth; bar.style.width = `${width}%`; - - // 根据宽度决定标签位置 - if (width + duration_per >= 100) { // 如果超出bar的范围则放在里面 - labelContainer.classList.add('inside'); - } else { - labelContainer.classList.remove('outside'); - labelContainer.classList.add('outside'); - // const per = user_width * 100 / bar.offsetWidth; - const rect1 = userLabel.getBoundingClientRect(); - const rect2 = durationLabel.getBoundingClientRect(); - - // 检查重叠 - const isOverlapping = !(rect1.right < rect2.left || - rect1.left > rect2.right); - - if (isOverlapping) { - // 如果重叠,将 element2 移动到 element1 的右侧 - console.log("trace", rect1.right) - labelContainer.style.left = `${userLabel.offsetLeft + userLabel.offsetWidth + 10}px`; // 10px 的间距 - } else { - labelContainer.style.left = `${width + max_duration / 200}%` - } + const barWidth = barWrapper.offsetWidth * width / 100; + const userLabelWidth = userLabel.offsetWidth; + const durationLabelWidth = durationLabel.offsetWidth; + if (userLabelWidth + durationLabelWidth > barWidth) { + const newMargin = Math.max(8, barWidth - userLabelWidth); + blankDiv.style.width = `${newMargin - 8}px`; } }); } - barsContainer.appendChild(barContainer); + barsContainer.appendChild(barWrapper); } container.appendChild(nameSection); diff --git a/src/components/QueueDisplay.js b/src/components/QueueDisplay.js index d8824d5..969a17f 100755 --- a/src/components/QueueDisplay.js +++ b/src/components/QueueDisplay.js @@ -20,10 +20,18 @@ export function createQueueDisplay(queueData) { queueData.forEach(item => { const queueItem = document.createElement('div'); + let wait_time = item.wait_time + if (wait_time < 60) { + wait_time = `${Math.round(wait_time)}秒` + } else if (wait_time < 3600) { + wait_time = `${Math.round(wait_time / 60)}分钟` + } else { + wait_time = `${Math.round(wait_time / 3600)}小时${Math.round(wait_time % 3600 / 60)}分钟` + } queueItem.className = 'queue-item'; queueItem.innerHTML = ` - ${item.userName} - ${item.waitTime} + ${item.user} + ${wait_time} `; queueList.appendChild(queueItem); }); diff --git a/src/services/queueService.js b/src/services/queueService.js index a29350a..3efc3a8 100755 --- a/src/services/queueService.js +++ b/src/services/queueService.js @@ -1,14 +1,15 @@ // 测试用的模拟队列数据 const mockQueueData = [ - { userName: "Alice Chen", waitTime: "15分钟" }, - { userName: "Bob Wang", waitTime: "8分钟" }, - { userName: "Charlie Liu", waitTime: "3分钟" } + { user: "Alice Chen", wait_time: "15" }, + { user: "Bob Wang", wait_time: "1300" }, + { user: "Charlie Liu", wait_time: "1200" } ]; export async function fetchQueueData() { try { const response = await fetch('/vcast/queue_info'); const data = await response.json(); + // console.log(data) return data; } catch (error) { console.error('Error fetching queue data:', error); diff --git a/style.css b/style.css index 62192f4..34baf3a 100755 --- a/style.css +++ b/style.css @@ -46,39 +46,28 @@ h1 { } .slot-info { - font-size: 12px; + font-size: 14px; color: #666; } .usage-bars { display: flex; flex-direction: column; - height: 60px; gap: 6px; flex-grow: 1; margin-right: 5px; - margin-top: 5px; - margin-bottom: 5px; -} - -.bar-wrapper { - position: relative; - height: 32px; - background-color: #f8f9fa; - border-radius: 4px; - overflow: visible; - /* 允许标签显示在外部 */ -} - -.bar-container { - flex: 1; - background-color: #eee; - border-radius: 4px; overflow: hidden; } +.bar-wrapper { + display: flex; + background-color: #f8f9fa; + border-radius: 4px; + overflow: visible; +} + .bar { - width: 100%; + width: 50%; height: 100%; background-color: #4caf50; display: flex; @@ -96,9 +85,7 @@ h1 { } .label-container { - position: absolute; display: flex; - align-items: center; gap: 8px; height: 100%; transition: all 0.3s ease; @@ -106,34 +93,42 @@ h1 { white-space: nowrap; } -.label-container.outside { - color: #2c3e50; -} - -.label-container.inside { - right: 8px; - color: #fff; +/* 一个空白的div */ +.blank-div { + height: 100%; + background-color: transparent; + display: inline-block; + flex-shrink: 0; + min-width: 0; + margin: 0px; } .bar-label { color: #212121; font-size: 16px; white-space: nowrap; - margin: 10px; - margin-left: 10%; + margin: 6px; + display: flex; + justify-content: center; + align-items: center; } .user-label { - margin: 10px; + margin: 8px; + text-align: center; color: rgba(33, 33, 33, 0.7); - font-size: 14px; - display: inline; + font-size: 16px; + display: flex; + align-items: center; } .duration-label { - font-size: 12px; + margin: 6px; + font-size: 16px; opacity: 0.9; - display: inline; + text-align: center; + align-items: center; + display: flex; } .free .bar-label { @@ -173,6 +168,7 @@ h1 { background-color: #f8f9fa; border-radius: 8px; padding: 15px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .queue-title {