1. 已基本完成功能,实现web server显示使用情况,实现排队的显示。实现lic的派发功能

2. 遗留有lic client的处理存在无法中断的问题。
This commit is contained in:
Ekko.bao 2025-01-13 08:46:49 +08:00
parent 552828ba21
commit 89e6c9439c
10 changed files with 1274 additions and 306 deletions

135
lic_info.json Executable file
View File

@ -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"
}
]
}
}

475
license.py Normal file → Executable file
View File

@ -1,25 +1,256 @@
#!/usr/bin/python3 #!/usr/bin/python3
import json import json
import queue
import re import re
import sys
import socket import socket
import asyncio import asyncio
import subprocess
import threading import threading
import time import time
from typing import Dict, List from typing import Dict, List, Optional, Union
from datetime import datetime from datetime import datetime
from utils import * from my_log import get_logger
import logger import socket
log = logger.get_logger() 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): def _get_ip_address(domain):
if MOCK_LMUTIL:
return mock_ipaddress(domain)
try: try:
ip_address = socket.gethostbyname(domain) ip_address = socket.gethostbyname(domain)
# log.debug(f"get_ip_address {domain} {ip_address}")
return ip_address return ip_address
except socket.gaierror as e: except socket.gaierror as e:
# print(f"Error retrieving IP address: {e}") log.error(f"get ip address {domain} error: {e}")
return None return None
def _get_valid_licenses(): def _get_valid_licenses():
@ -38,20 +269,22 @@ class UsedInfo:
self.lic = '' self.lic = ''
self.start_time:datetime = None self.start_time:datetime = None
def __str__(self): def __str__(self):
time = datetime.now() - self.start_time obj = self.as_dict()
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'
}
return str(obj) return str(obj)
def __repr__(self): def __repr__(self):
return self.__str__() 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: 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.name = name
self.total = total self.total = total
self.used = used self.used = used
@ -61,66 +294,148 @@ class LicenseInfo:
return self.total - self.used return self.total - self.used
def __str__(self): def __str__(self):
obj = { obj = self.as_dict()
"name":self.name,
"total":self.total,
"used":self.used,
"used_info":self.used_info
}
return str(obj) return str(obj)
def __repr__(self): def __repr__(self):
return self.__str__() 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: class LicenseManager:
def __init__(self): def __init__(self):
self.wait_queue = queue.Queue()
self.ack_queue = queue.Queue()
self.licenses_stat = LicenseStatistic() self.licenses_stat = LicenseStatistic()
self.licenses:Dict[str,LicenseInfo] = {} self.licenses:Dict[str,LicenseInfo] = {}
#启动定时获取 self.dispatcher = LicenseDispatcher()
self.licenses_stat.lic_info_updater() self.exit_event = threading.Event()
self.timer = None
self.start_updater() # 替换原来的线程启动
def lic_info_updater(self): def start_updater(self):
self.update() if not self.exit_event.is_set():
if self.wait_queue.qsize() > 0: self.update_once()
for lic, available_cnt in self.licenses_stat.available(): # 设置下一次执行
for i in range(available_cnt): self.timer = threading.Timer(5.0, self.start_updater)
req = self.wait_queue.get() self.timer.start()
if req == None:
return
req["lic"] = lic
self.ack_queue.put(req)
threading.Timer(10, self.lic_info_updater).start() #每间隔一段时间更新一次lic的使用情况
def _in_wait_queue(self, user): def update_once(self):
return any(user == _.user for _ in list(self.wait_queue)) """单次更新操作"""
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): def request(self, user: str, host: str) -> Dict[str, Union[str, dict]]:
#如果已经是在线的状态就不允许申请 #如果已经是在线的状态就不允许申请
if any(user == _.user for _ in self.licenses_stat.online_users()): rep = {
log.warning(f"{user} is online, please try again later")
return False
#如果是已经在队列中则返回等待
if self._in_wait_queue(user):
return None
data = {
"user":user, "user":user,
"host":host, "host":host,
"start_time":time.now() "status":"refuse",
"lic":'',
"msg":f"unknown error"
} }
self.wait_queue.put(data) if self.dispatcher.wait_queue.qsize() > 100:
return False 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
def cancel(self): #如果是已经在队列中则返回等待
pass item = next((item for item in self.dispatcher.wait_queue if item['user'] == user), None)
if item:
def release(self): rep["status"] = "queue"
pass 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): 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: class LicenseStatistic:
def __init__(self): def __init__(self):
@ -132,13 +447,23 @@ class LicenseStatistic:
for lic in _get_valid_licenses(): for lic in _get_valid_licenses():
log.trace(f'find valid lic: {lic}') log.trace(f'find valid lic: {lic}')
tasks.append(get_license_used_info(lic)) tasks.append(get_license_used_info(lic))
loop = asyncio.get_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
infos = loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) infos = loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
for info in infos: for info in infos:
if info is not None: 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 self.licenses[info.name] = info
if not self.licenses: if not self.licenses:
log.warning("not vcast lic found") 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): def get_lic_list(self):
# 获取所有的license名称 # 获取所有的license名称
return self.licenses.keys() return self.licenses.keys()
@ -148,7 +473,7 @@ class LicenseStatistic:
for _,lic in self.licenses.items(): for _,lic in self.licenses.items():
value = lic.available() value = lic.available()
if value > 0: if value > 0:
yield (lic.name, value) #lic的name和个数 yield (lic.name, value, lic.total) #lic的name和个数
def online_users(self): def online_users(self):
# 获取在线用户 # 获取在线用户
@ -179,11 +504,30 @@ class LicenseStatistic:
def __repr__(self): def __repr__(self):
return self.__str__() 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"}]"}} {"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): 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) ret,out,err = shell(server_used_info, checkret=False)
# log.info(f"get_license_used_info {lic} {out} {ret} {err}") # log.info(f"get_license_used_info {lic} {out} {ret} {err}")
if ret != 0: if ret != 0:
@ -218,7 +562,7 @@ async def get_license_used_info(lic):
ret = re.findall(pattern, line) ret = re.findall(pattern, line)
if not ret: if not ret:
log.warning(f"{lic} parser [{line}] fail") log.warning(f"{lic} parser [{line}] fail")
return None return info
(total, used) = ret[0] (total, used) = ret[0]
info.total = int(total) info.total = int(total)
info.used = int(used) info.used = int(used)
@ -258,10 +602,7 @@ if __name__ == '__main__':
log.info("License status query...") log.info("License status query...")
lm = LicenseStatistic() lm = LicenseStatistic()
log.info(lm) 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()]}") 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") log.info(f"{lic}: {number} lic available")
draw_time_grpah(lm) draw_time_grpah(lm)

315
license_client.py Executable file
View File

@ -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()

200
mock_lmutil.py Executable file
View File

@ -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)

55
my_log.py Executable file
View File

@ -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日志')

201
server.py
View File

@ -1,139 +1,33 @@
#!/usr/bin/python3 #!/usr/bin/python3
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json import json
import os import os
from pathlib import Path from pathlib import Path
from license import LicenseManager
from my_log import get_logger
log = get_logger()
sample_data = { global LIC_MANAGER
"27003@szmaslic03": { LIC_MANAGER = None
"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"
}
]
}
}
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): 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): def log_message(self, format, *args):
return # 重写该方法以禁用日志输出 return # 重写该方法以禁用日志输出
def do_GET(self): def do_GET(self):
if self.path == '/vcast/lic_use_info': if self.path in self.handler_map:
self.send_response(200) self.handler_map[self.path]()
self.send_header('Content-type', 'text/json') self.wfile.flush()
self.end_headers() self.close_connection = True
data = json.dumps(sample_data)
self.wfile.write(data.encode('utf-8'))
return return
path = self.path path = self.path
if path == '/': if path == '/':
@ -141,16 +35,60 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
path = path.strip('/') path = path.strip('/')
if not self.send_file(path): if not self.send_file(path):
self.send_404() 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): def send_404(self):
self.send_response(404) self.send_response(404)
self.send_header('Content-type', 'text/html') self.send_header('Content-type', 'text/html')
resp_str = "<h1>404 Not Found</h1><p>The page you are looking for does not exist.</p>".encode('utf-8')
self.send_header('Content-Length', len(resp_str))
self.end_headers() self.end_headers()
self.wfile.write(b"<h1>404 Not Found</h1><p>The page you are looking for does not exist.</p>") self.wfile.write(resp_str)
self.wfile.flush()
self.close_connection = True
def send_file(self, file_path) -> bool: def send_file(self, file_path) -> bool:
file_path =str(Path(os.path.abspath(__file__)).parent.joinpath(file_path)) 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): if not os.path.isfile(file_path):
return False return False
self.send_response(200) self.send_response(200)
@ -177,14 +115,21 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
return 'text/css' return 'text/css'
# 添加更多 MIME 类型根据需要 # 添加更多 MIME 类型根据需要
return 'application/octet-stream' # 默认类型 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) server_address = ('0.0.0.0', port)
httpd = server_class(server_address, handler_class) 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() httpd.serve_forever()
if __name__ == "__main__": if __name__ == "__main__":
LIC_MANAGER = LicenseManager()
try:
if len(os.sys.argv) > 1: if len(os.sys.argv) > 1:
run(port=int(os.sys.argv[1])) run(port=int(os.sys.argv[1]))
else: else:
run() run()
except KeyboardInterrupt:
log.info("Keyboard interrupt received, shutting down...")
finally:
LIC_MANAGER.exit()
log.info("Server stopped")

View File

@ -16,12 +16,10 @@ export function createLicenseBar(license, max_duration) {
// Create bars for each slot // Create bars for each slot
for (let i = 0; i < license.totalSlots; i++) { for (let i = 0; i < license.totalSlots; i++) {
const usage = license.usageDetails[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) { if (!usage) {
const bar = document.createElement('div'); const bar = document.createElement('div');
@ -34,77 +32,51 @@ export function createLicenseBar(license, max_duration) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
label.style.transform = 'translate(0%)'; label.style.transform = 'translate(0%)';
}); });
barWrapper.appendChild(bar);
} else { } else {
const barContainer = document.createElement('div');
barContainer.className = 'bar-container';
const bar = document.createElement('div'); const bar = document.createElement('div');
bar.className = 'bar'; bar.className = 'bar';
const width = usage.duration / max_duration * 100; const width = usage.duration / max_duration * 100;
// 创建标签容器 // 创建标签容器
const labelContainer = document.createElement('div'); // const labelContainer = document.createElement('div');
labelContainer.className = 'label-container'; // labelContainer.className = 'label-container';
// 用户信息标签 // 用户信息标签
const userLabel = document.createElement('div'); const userLabel = document.createElement('div');
userLabel.className = 'user-label'; userLabel.className = 'user-label';
userLabel.textContent = usage.userName; userLabel.textContent = usage.userName;
const blankDiv = document.createElement('div');
blankDiv.className = 'blank-div';
blankDiv.textContent = ' ';
// 时长标签 // 时长标签
const durationLabel = document.createElement('div'); const durationLabel = document.createElement('div');
durationLabel.className = 'duration-label'; durationLabel.className = 'duration-label';
durationLabel.textContent = `${usage.duration.toFixed(1)}h`; durationLabel.textContent = `${usage.duration.toFixed(1)}h`;
if (width < 50) {
labelContainer.appendChild(durationLabel);
bar.appendChild(userLabel); bar.appendChild(userLabel);
} else { bar.appendChild(blankDiv);
bar.appendChild(userLabel); bar.appendChild(durationLabel);
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}`;
// // barElement.appendChild(hostLabel); barWrapper.appendChild(bar);
// barElement.appendChild(label);
// 将标签添加到bar容器中
barContainer.appendChild(labelContainer);
barContainer.appendChild(bar);
requestAnimationFrame(() => { requestAnimationFrame(() => {
const user_width = userLabel.offsetWidth;
const duration_width = labelContainer.offsetWidth
const duration_per = duration_width / bar.offsetWidth;
bar.style.width = `${width}%`; bar.style.width = `${width}%`;
const barWidth = barWrapper.offsetWidth * width / 100;
// 根据宽度决定标签位置 const userLabelWidth = userLabel.offsetWidth;
if (width + duration_per >= 100) { // 如果超出bar的范围则放在里面 const durationLabelWidth = durationLabel.offsetWidth;
labelContainer.classList.add('inside'); if (userLabelWidth + durationLabelWidth > barWidth) {
} else { const newMargin = Math.max(8, barWidth - userLabelWidth);
labelContainer.classList.remove('outside'); blankDiv.style.width = `${newMargin - 8}px`;
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}%`
}
} }
}); });
} }
barsContainer.appendChild(barContainer); barsContainer.appendChild(barWrapper);
} }
container.appendChild(nameSection); container.appendChild(nameSection);

View File

@ -20,10 +20,18 @@ export function createQueueDisplay(queueData) {
queueData.forEach(item => { queueData.forEach(item => {
const queueItem = document.createElement('div'); 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.className = 'queue-item';
queueItem.innerHTML = ` queueItem.innerHTML = `
<span class="queue-user">${item.userName}</span> <span class="queue-user">${item.user}</span>
<span class="queue-time">${item.waitTime}</span> <span class="queue-time">${wait_time}</span>
`; `;
queueList.appendChild(queueItem); queueList.appendChild(queueItem);
}); });

View File

@ -1,14 +1,15 @@
// 测试用的模拟队列数据 // 测试用的模拟队列数据
const mockQueueData = [ const mockQueueData = [
{ userName: "Alice Chen", waitTime: "15分钟" }, { user: "Alice Chen", wait_time: "15" },
{ userName: "Bob Wang", waitTime: "8分钟" }, { user: "Bob Wang", wait_time: "1300" },
{ userName: "Charlie Liu", waitTime: "3分钟" } { user: "Charlie Liu", wait_time: "1200" }
]; ];
export async function fetchQueueData() { export async function fetchQueueData() {
try { try {
const response = await fetch('/vcast/queue_info'); const response = await fetch('/vcast/queue_info');
const data = await response.json(); const data = await response.json();
// console.log(data)
return data; return data;
} catch (error) { } catch (error) {
console.error('Error fetching queue data:', error); console.error('Error fetching queue data:', error);

View File

@ -46,39 +46,28 @@ h1 {
} }
.slot-info { .slot-info {
font-size: 12px; font-size: 14px;
color: #666; color: #666;
} }
.usage-bars { .usage-bars {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 60px;
gap: 6px; gap: 6px;
flex-grow: 1; flex-grow: 1;
margin-right: 5px; 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; overflow: hidden;
} }
.bar-wrapper {
display: flex;
background-color: #f8f9fa;
border-radius: 4px;
overflow: visible;
}
.bar { .bar {
width: 100%; width: 50%;
height: 100%; height: 100%;
background-color: #4caf50; background-color: #4caf50;
display: flex; display: flex;
@ -96,9 +85,7 @@ h1 {
} }
.label-container { .label-container {
position: absolute;
display: flex; display: flex;
align-items: center;
gap: 8px; gap: 8px;
height: 100%; height: 100%;
transition: all 0.3s ease; transition: all 0.3s ease;
@ -106,34 +93,42 @@ h1 {
white-space: nowrap; white-space: nowrap;
} }
.label-container.outside { /* 一个空白的div */
color: #2c3e50; .blank-div {
} height: 100%;
background-color: transparent;
.label-container.inside { display: inline-block;
right: 8px; flex-shrink: 0;
color: #fff; min-width: 0;
margin: 0px;
} }
.bar-label { .bar-label {
color: #212121; color: #212121;
font-size: 16px; font-size: 16px;
white-space: nowrap; white-space: nowrap;
margin: 10px; margin: 6px;
margin-left: 10%; display: flex;
justify-content: center;
align-items: center;
} }
.user-label { .user-label {
margin: 10px; margin: 8px;
text-align: center;
color: rgba(33, 33, 33, 0.7); color: rgba(33, 33, 33, 0.7);
font-size: 14px; font-size: 16px;
display: inline; display: flex;
align-items: center;
} }
.duration-label { .duration-label {
font-size: 12px; margin: 6px;
font-size: 16px;
opacity: 0.9; opacity: 0.9;
display: inline; text-align: center;
align-items: center;
display: flex;
} }
.free .bar-label { .free .bar-label {
@ -173,6 +168,7 @@ h1 {
background-color: #f8f9fa; background-color: #f8f9fa;
border-radius: 8px; border-radius: 8px;
padding: 15px; padding: 15px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
} }
.queue-title { .queue-title {