1. 已基本完成功能,实现web server显示使用情况,实现排队的显示。实现lic的派发功能
2. 遗留有lic client的处理存在无法中断的问题。
This commit is contained in:
parent
552828ba21
commit
89e6c9439c
135
lic_info.json
Executable file
135
lic_info.json
Executable 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
475
license.py
Normal file → Executable 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
315
license_client.py
Executable 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
200
mock_lmutil.py
Executable 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
55
my_log.py
Executable 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
201
server.py
|
@ -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")
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
68
style.css
68
style.css
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user