Coverage for .claude/hooks/moai/lib/timeout.py: 0.00%

69 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-19 08:00 +0900

1#!/usr/bin/env python3 

2# # REMOVED_ORPHAN_CODE:TIMEOUT-001 | SPEC: SPEC-TIMEOUT-HANDLING-001 

3"""Cross-Platform Timeout Handler for Windows & Unix Compatibility 

4 

5Provides a unified timeout mechanism that works on both Windows (threading-based) 

6and Unix/POSIX systems (signal-based). 

7 

8Architecture: 

9 - Windows: threading.Timer with exception injection 

10 - Unix/POSIX: signal.SIGALRM (traditional approach) 

11 - Both: Context manager for clean cancellation 

12""" 

13 

14import platform 

15import signal 

16import threading 

17from contextlib import contextmanager 

18from typing import Callable, Optional 

19 

20 

21class TimeoutError(Exception): 

22 """Timeout exception raised when deadline exceeded""" 

23 pass 

24 

25 

26class CrossPlatformTimeout: 

27 """Cross-platform timeout handler supporting Windows and Unix. 

28 

29 Windows: Uses threading.Timer to schedule timeout exception 

30 Unix: Uses signal.SIGALRM for timeout handling 

31 

32 Usage: 

33 # Context manager (recommended) 

34 with CrossPlatformTimeout(5): 

35 long_running_operation() 

36 

37 # Manual control 

38 timeout = CrossPlatformTimeout(5) 

39 timeout.start() 

40 try: 

41 long_running_operation() 

42 finally: 

43 timeout.cancel() 

44 """ 

45 

46 def __init__(self, timeout_seconds: float, callback: Optional[Callable] = None): 

47 """Initialize timeout with duration in seconds. 

48 

49 Args: 

50 timeout_seconds: Timeout duration in seconds (float or int) 

51 callback: Optional callback to execute before raising TimeoutError 

52 """ 

53 # Convert float to int for signal.alarm() 

54 self.timeout_seconds = timeout_seconds 

55 self.timeout_seconds_int = max(1, int(timeout_seconds)) # signal.alarm requires >=1 

56 self.callback = callback 

57 self.timer: Optional[threading.Timer] = None 

58 self._is_windows = platform.system() == "Windows" 

59 self._old_handler = None 

60 

61 def start(self) -> None: 

62 """Start timeout countdown.""" 

63 # Handle zero/negative timeouts 

64 if self.timeout_seconds <= 0: 

65 if self.timeout_seconds == 0: 

66 # Zero timeout: raise immediately 

67 if self.callback: 

68 self.callback() 

69 raise TimeoutError("Timeout of 0 seconds exceeded immediately") 

70 else: 

71 # Negative timeout: don't timeout at all 

72 return 

73 

74 if self._is_windows: 

75 self._start_windows_timeout() 

76 else: 

77 self._start_unix_timeout() 

78 

79 def cancel(self) -> None: 

80 """Cancel timeout (must call before timeout expires).""" 

81 if self._is_windows: 

82 self._cancel_windows_timeout() 

83 else: 

84 self._cancel_unix_timeout() 

85 

86 def _start_windows_timeout(self) -> None: 

87 """Windows: Use threading.Timer to raise exception.""" 

88 def timeout_handler(): 

89 if self.callback: 

90 self.callback() 

91 raise TimeoutError( 

92 f"Operation exceeded {self.timeout_seconds}s timeout (Windows threading)" 

93 ) 

94 

95 self.timer = threading.Timer(self.timeout_seconds, timeout_handler) 

96 self.timer.daemon = True 

97 self.timer.start() 

98 

99 def _cancel_windows_timeout(self) -> None: 

100 """Windows: Cancel timer thread.""" 

101 if self.timer: 

102 self.timer.cancel() 

103 self.timer = None 

104 

105 def _start_unix_timeout(self) -> None: 

106 """Unix/POSIX: Use signal.SIGALRM for timeout.""" 

107 def signal_handler(signum, frame): 

108 if self.callback: 

109 try: 

110 self.callback() 

111 except Exception: 

112 # Ignore callback exceptions, timeout error takes precedence 

113 pass 

114 raise TimeoutError( 

115 f"Operation exceeded {self.timeout_seconds}s timeout (Unix signal)" 

116 ) 

117 

118 # Save old handler to restore later 

119 self._old_handler = signal.signal(signal.SIGALRM, signal_handler) 

120 # Use integer timeout_seconds_int for signal.alarm() 

121 signal.alarm(self.timeout_seconds_int) 

122 

123 def _cancel_unix_timeout(self) -> None: 

124 """Unix/POSIX: Cancel alarm and restore old handler.""" 

125 signal.alarm(0) # Cancel pending alarm 

126 if self._old_handler is not None: 

127 signal.signal(signal.SIGALRM, self._old_handler) 

128 self._old_handler = None 

129 

130 def __enter__(self): 

131 """Context manager entry.""" 

132 self.start() 

133 return self 

134 

135 def __exit__(self, exc_type, exc_val, exc_tb): 

136 """Context manager exit - always cancel.""" 

137 self.cancel() 

138 return False # Don't suppress exceptions 

139 

140 

141@contextmanager 

142def timeout_context(timeout_seconds: float, callback: Optional[Callable] = None): 

143 """Decorator/context manager for timeout. 

144 

145 Usage: 

146 with timeout_context(5): 

147 slow_function() 

148 

149 Args: 

150 timeout_seconds: Timeout duration in seconds (float or int) 

151 callback: Optional callback to execute before raising TimeoutError 

152 

153 Yields: 

154 CrossPlatformTimeout instance 

155 """ 

156 timeout = CrossPlatformTimeout(timeout_seconds, callback=callback) 

157 timeout.start() 

158 try: 

159 yield timeout 

160 finally: 

161 timeout.cancel()