99import re
1010import subprocess
1111import sys
12+ import time
13+
1214
1315import click
1416from sonic_py_common import device_info
3941STATE_KEY = "state"
4042PID_KEY = "pid"
4143START_TIME_KEY = "start_time"
44+ OPER_STATE_KEY = "oper_state"
45+ LAST_STATE_CHANGE_KEY = "last_state_change"
4246
4347BUSY_FLAG = "busy"
4448IDLE_FLAG = "idle"
4953
5054UDEV_PREFIX_CONF_FILENAME = "udevprefix.conf"
5155
56+ PTY_SYMLINK_SUFFIX = "-PTS"
57+
5258TIMEOUT_SEC = 0.2
5359
5460class ConsolePortProvider (object ):
@@ -176,6 +182,48 @@ def busy(self):
176182 def session_pid (self ):
177183 return self .cur_state [PID_KEY ] if PID_KEY in self .cur_state else None
178184
185+ @property
186+ def oper_state (self ):
187+ return self .cur_state [OPER_STATE_KEY ] if OPER_STATE_KEY in self .cur_state else None
188+
189+ @property
190+ def last_state_change (self ):
191+ return self .cur_state [LAST_STATE_CHANGE_KEY ] if LAST_STATE_CHANGE_KEY in self .cur_state else None
192+
193+ @property
194+ def state_duration (self ):
195+ """Calculate and format the duration since last state change.
196+ Format: XdXhXmXs (only shows non-zero parts)
197+ """
198+ if not self .last_state_change :
199+ return None
200+ try :
201+ ts = int (self .last_state_change )
202+ now = int (time .time ())
203+ diff = now - ts
204+ if diff < 0 :
205+ return None
206+
207+ # Calculate time components
208+ days , remainder = divmod (diff , 24 * 3600 )
209+ hours , remainder = divmod (remainder , 3600 )
210+ minutes , seconds = divmod (remainder , 60 )
211+
212+ # Build formatted string, only include non-zero parts for d/h/m, always show seconds
213+ parts = []
214+ if days > 0 :
215+ parts .append (f"{ days } d" )
216+ if hours > 0 or days > 0 :
217+ parts .append (f"{ hours } h" )
218+ if minutes > 0 or hours > 0 or days > 0 :
219+ parts .append (f"{ minutes } m" )
220+
221+ parts .append (f"{ seconds } s" ) # Always show seconds
222+
223+ return "" .join (parts )
224+ except (ValueError , OSError ):
225+ return None
226+
179227 @property
180228 def session_start_date (self ):
181229 return self .cur_state [START_TIME_KEY ] if START_TIME_KEY in self .cur_state else None
@@ -201,8 +249,9 @@ def connect(self):
201249 # build and start picocom command
202250 flow_cmd = "h" if self .flow_control else "n"
203251 escape_cmd = "-e {}" .format (self .escape_char ) if self .escape_char else ""
204- cmd = "picocom {} -b {} -f {} {}{}" .format (escape_cmd , self .baud , flow_cmd ,
205- SysInfoProvider .DEVICE_PREFIX , self .line_num )
252+ cmd = "picocom {} -b {} -f {} {}{}{}" .format (
253+ escape_cmd , self .baud , flow_cmd ,
254+ SysInfoProvider .DEVICE_PREFIX , self .line_num , PTY_SYMLINK_SUFFIX )
206255
207256 # start connection
208257 try :
@@ -312,7 +361,7 @@ def list_console_ttys():
312361 cmd = ["bash" , "-c" , "ls " + SysInfoProvider .DEVICE_PREFIX + "*" ]
313362 output , _ = SysInfoProvider .run_command (cmd , abort = False )
314363 ttys = output .split ('\n ' )
315- ttys = list ([dev for dev in ttys if re .match (SysInfoProvider .DEVICE_PREFIX + r"\d+" , dev ) != None ])
364+ ttys = list ([dev for dev in ttys if re .match (SysInfoProvider .DEVICE_PREFIX + r"\d+$ " , dev )])
316365 return ttys
317366
318367 @staticmethod
@@ -345,8 +394,8 @@ def _parse_processes_info(output):
345394 regex_date = r"([A-Z][a-z]{2} [A-Z][a-z]{2} [\d ]\d \d{2}:\d{2}:\d{2} \d{4})"
346395 # matches any characters ending in minicom or picocom,
347396 # then a space and any chars followed by /dev/ttyUSB<any digits>,
348- # then a space and any chars
349- regex_cmd = r".*(?:(?:mini)|(?:pico))com .*" + SysInfoProvider .DEVICE_PREFIX + r"(\d+)(?: .*)? "
397+ # then any chars
398+ regex_cmd = r".*(?:(?:mini)|(?:pico))com .*" + SysInfoProvider .DEVICE_PREFIX + r"(\d+).* "
350399 regex_process = re .compile (r"^" + regex_pid + r" " + regex_date + r" " + regex_cmd + r"$" )
351400
352401 console_processes = {}
@@ -378,10 +427,18 @@ def update_state(self, line_num, state, pid="", date=""):
378427 self ._state_db .set (self ._state_db .STATE_DB , key , STATE_KEY , state )
379428 self ._state_db .set (self ._state_db .STATE_DB , key , PID_KEY , pid )
380429 self ._state_db .set (self ._state_db .STATE_DB , key , START_TIME_KEY , date )
430+
431+ # Read existing oper_state and last_state_change from STATE_DB
432+ existing_data = self ._state_db .get_all (self ._state_db .STATE_DB , key )
433+ oper_state = existing_data .get (OPER_STATE_KEY , "" ) if existing_data else ""
434+ last_state_change = existing_data .get (LAST_STATE_CHANGE_KEY , "" ) if existing_data else ""
435+
381436 return {
382437 STATE_KEY : state ,
383438 PID_KEY : pid ,
384- START_TIME_KEY : date
439+ START_TIME_KEY : date ,
440+ OPER_STATE_KEY : oper_state ,
441+ LAST_STATE_CHANGE_KEY : last_state_change
385442 }
386443
387444class InvalidConfigurationError (Exception ):
0 commit comments