2025-01-13 08:46:49 +08:00
|
|
|
#!/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
|
2025-01-14 09:01:00 +08:00
|
|
|
import select
|
2025-01-13 08:46:49 +08:00
|
|
|
|
|
|
|
|
|
|
|
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.heartbeat_thread: threading.Thread | None = None # Add thread reference
|
|
|
|
|
|
|
|
def request(self) -> str:
|
2025-01-14 09:01:00 +08:00
|
|
|
"""Request license, block until license is obtained"""
|
2025-01-13 08:46:49 +08:00
|
|
|
response = self._request_license()
|
2025-01-14 09:01:00 +08:00
|
|
|
if response['status'] == 'wait_dispatch':
|
2025-01-13 08:46:49 +08:00
|
|
|
# Start heartbeat thread
|
|
|
|
log.info("Waiting for license allocation...")
|
|
|
|
self._start_dispatcher_client(response['dispatcher_server']['port'])
|
2025-01-14 09:01:00 +08:00
|
|
|
else:
|
|
|
|
log.warning(f"{response['status']}: {response['msg']}")
|
|
|
|
return response['status']
|
2025-01-13 08:46:49 +08:00
|
|
|
|
|
|
|
def get_lic(self) -> str:
|
|
|
|
"""Get the assigned license"""
|
2025-01-14 09:01:00 +08:00
|
|
|
while not self.exit_event.is_set():
|
|
|
|
try:
|
|
|
|
self.current_lic = self.lic.get(timeout=0.1)
|
|
|
|
return self.current_lic
|
|
|
|
except queue.Empty:
|
|
|
|
continue
|
2025-01-13 08:46:49 +08:00
|
|
|
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}")
|
2025-01-14 09:01:00 +08:00
|
|
|
return {"status": "connect_error", "msg": str(e)}
|
2025-01-13 08:46:49 +08:00
|
|
|
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)
|
2025-01-14 09:01:00 +08:00
|
|
|
try:
|
|
|
|
self.dispatcher_socket.connect((SERVER_HOST, port))
|
|
|
|
except Exception as e:
|
|
|
|
log.error(f"Failed to connect to dispatcher: {e}")
|
|
|
|
return
|
2025-01-13 08:46:49 +08:00
|
|
|
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)
|
2025-01-14 09:01:00 +08:00
|
|
|
if self.exit_event.is_set():
|
|
|
|
break
|
2025-01-13 08:46:49 +08:00
|
|
|
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)
|
2025-01-14 09:01:00 +08:00
|
|
|
if wait_time < 60:
|
|
|
|
print(f"\rwaiting... {wait_time}s", end='', flush=True)
|
|
|
|
elif wait_time < 3600:
|
|
|
|
print(f"\rwaiting... {wait_time / 60}m {wait_time}s", end='', flush=True)
|
|
|
|
else:
|
|
|
|
print(f"\rwaiting... {wait_time / 3600}h {wait_time / 60}m {wait_time}s", end='', flush=True)
|
2025-01-13 08:46:49 +08:00
|
|
|
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
|
2025-01-14 09:01:00 +08:00
|
|
|
except KeyboardInterrupt:
|
|
|
|
log.info("Keyboard interrupt received, shutting down...")
|
|
|
|
break
|
2025-01-13 08:46:49 +08:00
|
|
|
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
|
2025-01-14 09:01:00 +08:00
|
|
|
self.heartbeat_thread = threading.Thread(target=heartbeat_loop)
|
2025-01-13 08:46:49 +08:00
|
|
|
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)
|
|
|
|
|
2025-01-14 09:01:00 +08:00
|
|
|
if self.dispatcher_socket:
|
|
|
|
try:
|
|
|
|
self.dispatcher_socket.shutdown(socket.SHUT_RDWR)
|
|
|
|
except Exception:
|
|
|
|
pass
|
|
|
|
self.dispatcher_socket.close()
|
|
|
|
self.dispatcher_socket = None
|
2025-01-13 08:46:49 +08:00
|
|
|
|
|
|
|
self.release()
|
|
|
|
self.lic.put(None)
|
|
|
|
except Exception as e:
|
|
|
|
log.error(f"Error during exit: {e}")
|
|
|
|
|
|
|
|
clients: dict[str, LicenseDispatcherClient] = {}
|
2025-01-14 09:01:00 +08:00
|
|
|
def cleanup_clients():
|
|
|
|
"""清理所有客户端资源"""
|
2025-01-13 08:46:49 +08:00
|
|
|
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")
|
|
|
|
|
|
|
|
def main() -> None:
|
2025-01-14 09:01:00 +08:00
|
|
|
try:
|
|
|
|
while True:
|
|
|
|
show_menu()
|
|
|
|
cmd = input("Please select an operation: ").strip()
|
|
|
|
|
|
|
|
if cmd == '1':
|
|
|
|
create_client()
|
|
|
|
elif cmd == '2':
|
|
|
|
release_client()
|
|
|
|
elif cmd == '3':
|
|
|
|
show_clients()
|
|
|
|
elif cmd == '4':
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
print("Invalid command")
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
print("\nReceived keyboard interrupt, cleaning up...")
|
|
|
|
finally:
|
|
|
|
cleanup_clients()
|
2025-01-13 08:46:49 +08:00
|
|
|
|
|
|
|
def create_client():
|
|
|
|
"""Create new license client"""
|
2025-01-14 09:01:00 +08:00
|
|
|
try:
|
|
|
|
username = input("Please enter username: ").strip()
|
|
|
|
hostname = input("Please enter hostname: ").strip()
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
print("\nOperation cancelled")
|
2025-01-13 08:46:49 +08:00
|
|
|
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)
|
2025-01-14 09:01:00 +08:00
|
|
|
status = client.request()
|
|
|
|
if status == 'wait_dispatch':
|
|
|
|
lic = client.get_lic()
|
2025-01-13 08:46:49 +08:00
|
|
|
clients[client_key] = client
|
|
|
|
mock_license_add_user(lic, username, hostname)
|
|
|
|
print(f"{client_key} successfully created and requested license: {lic}")
|
|
|
|
else:
|
2025-01-14 09:01:00 +08:00
|
|
|
if status == 'connect_error':
|
|
|
|
print(f"{client_key} License Server connect error, You can try again later")
|
|
|
|
else:
|
|
|
|
print(f"{client_key} License request failed: {status}")
|
2025-01-13 08:46:49 +08:00
|
|
|
|
|
|
|
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()
|