88"""
99
1010import time
11- from collections import namedtuple
1211from queue import Queue
12+ from typing import NamedTuple
1313from uuid import uuid4
1414
1515from flask import Flask , Response , request
1616
1717__version__ = "1.1.0"
1818
19+ CLIENT_LOOP_WAIT_SECONDS = 5
20+ STALE_OUTPUT_TTL_SECONDS = 3600
21+
22+
1923# Global command queue
20- output_object = namedtuple ("OutputObject" , ["output" , "rc" ])
21- command_object = namedtuple ("CommandObject" , ["command_id" , "str" ])
24+ class OutputObject (NamedTuple ):
25+ """Store command output together with its return code."""
26+
27+ output : str
28+ rc : int
29+
30+
31+ class CommandObject (NamedTuple ):
32+ """Store queued command metadata sent to a specific client."""
33+
34+ command_id : str
35+ str : str
2236
23- output_queue : dict [str , output_object ] = dict ()
37+
38+ output_object = OutputObject
39+ command_object = CommandObject
40+
41+ output_queue : dict [str , OutputObject ] = dict ()
42+ output_queue_timestamps : dict [str , float ] = dict ()
2443command_dict_queue : dict [str , Queue ] = dict ()
2544clients : list = []
2645
2746app = Flask (__name__ )
2847
2948
30- def get_output (command_id : str , timeout : float = 600 ) -> output_object :
49+ def _cleanup_stale_outputs (now : float | None = None , ttl : int = STALE_OUTPUT_TTL_SECONDS ) -> None :
50+ """Remove orphaned command outputs that have been kept longer than the configured TTL."""
51+ current_time = time .monotonic () if now is None else now
52+ stale_command_ids = [
53+ command_id for command_id , created_at in output_queue_timestamps .items () if current_time - created_at >= ttl
54+ ]
55+ for command_id in stale_command_ids :
56+ output_queue .pop (command_id , None )
57+ output_queue_timestamps .pop (command_id , None )
58+
59+
60+ def _store_output (command_id : str , output : str , rc : int ) -> None :
61+ """Persist command output together with its insertion timestamp."""
62+ _cleanup_stale_outputs ()
63+ output_queue [command_id ] = output_object (output = output , rc = rc )
64+ output_queue_timestamps [command_id ] = time .time ()
65+
66+
67+ def get_output (command_id : str , timeout : float = 600 ) -> OutputObject :
3168 """
3269 Retrieve the output for a given command ID.
3370
@@ -38,11 +75,13 @@ def get_output(command_id: str, timeout: float = 600) -> output_object:
3875 """
3976 print ("Getting output for command ID:" , command_id )
4077 print (f"Waiting for output { timeout } seconds" )
41- timeout = timeout + 5 # add time for client loop waiting
78+ timeout = timeout + CLIENT_LOOP_WAIT_SECONDS # add time for client loop waiting
4279 while timeout > 0 :
43- result = output_queue .get (command_id , None )
80+ result = output_queue .pop (command_id , None )
4481 if result is not None :
82+ output_queue_timestamps .pop (command_id , None )
4583 return result
84+ _cleanup_stale_outputs ()
4685 time .sleep (1 )
4786 timeout -= 1
4887 raise TimeoutError ("Command timed out" )
@@ -110,7 +149,7 @@ def post_exception() -> Response:
110149 command_id = str (request .headers .get ("CommandID" ))
111150 print ("CommandID: " , command_id )
112151 print (str (read_data , encoding = "utf-8" ))
113- output_queue [ command_id ] = output_object ( output = str (read_data , encoding = "utf-8" ), rc = - 1 )
152+ _store_output ( command_id , str (read_data , encoding = "utf-8" ), rc = - 1 )
114153 return Response ("Exception received" , status = 200 )
115154
116155
@@ -140,7 +179,7 @@ def execute_command() -> Response:
140179 headers = {
141180 "Content-type" : "text/plain" ,
142181 "CommandID" : _id ,
143- "rc" : process .rc ,
182+ "rc" : str ( process .rc ) ,
144183 },
145184 )
146185 else :
@@ -156,6 +195,7 @@ def disconnect_client(ip_address: str) -> Response:
156195 """
157196 if ip_address in clients :
158197 clients .remove (ip_address )
198+ command_dict_queue .pop (ip_address , None )
159199 print (f"Client disconnected: { ip_address } " )
160200 return Response ("Client disconnected" , status = 200 )
161201
@@ -168,7 +208,7 @@ def post_result() -> Response:
168208 rc = int (request .headers .get ("rc" , - 1 ))
169209 print ("CommandID: " , command_id )
170210 print (str (read_data , encoding = "utf-8" ))
171- output_queue [ command_id ] = output_object ( output = str (read_data , encoding = "utf-8" ), rc = rc )
211+ _store_output ( command_id , str (read_data , encoding = "utf-8" ), rc = rc )
172212 return Response ("Results received" , status = 200 )
173213
174214
0 commit comments