33"""
44
55import logging
6- import signal
76import socket
87import sys
98import time
1413from samcli .commands .local .cli_common .invoke_context import InvokeContext
1514from samcli .commands .local .lib .exceptions import NoFunctionUrlsDefined
1615from samcli .commands .local .lib .function_url_handler import FunctionUrlHandler
16+ from samcli .local .docker .utils import find_free_port
1717
1818LOG = logging .getLogger (__name__ )
1919
@@ -106,7 +106,7 @@ def _discover_function_urls(self):
106106
107107 def _allocate_port (self ) -> int :
108108 """
109- Allocate next available port in range
109+ Allocate next available port in range using existing find_free_port utility
110110
111111 Returns
112112 -------
@@ -118,36 +118,20 @@ def _allocate_port(self) -> int:
118118 PortExhaustedException
119119 When no ports are available in the specified range
120120 """
121- for port in range (self ._port_start , self ._port_end + 1 ):
122- if port not in self ._used_ports :
123- # Actually check if the port is available by trying to bind to it
124- if self ._is_port_available (port ):
125- self ._used_ports .add (port )
126- return port
127- raise PortExhaustedException (f"No available ports in range { self ._port_start } -{ self ._port_end } " )
128-
129- def _is_port_available (self , port : int ) -> bool :
130- """
131- Check if a port is available by attempting to bind to it
132-
133- Parameters
134- ----------
135- port : int
136- Port number to check
137-
138- Returns
139- -------
140- bool
141- True if port is available, False otherwise
142- """
121+ # Try to find a free port in the specified range
122+ # find_free_port signature: (network_interface: str, start: int, end: int)
123+ # find_free_port raises NoFreePortsError if no ports available
143124 try :
144- with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as sock :
145- sock .setsockopt (socket .SOL_SOCKET , socket .SO_REUSEADDR , 1 )
146- sock .bind ((self .host , port ))
147- return True
148- except OSError :
149- LOG .debug (f"Port { port } is already in use" )
150- return False
125+ port = find_free_port (network_interface = self .host , start = self ._port_start , end = self ._port_end )
126+ if port and port not in self ._used_ports :
127+ self ._used_ports .add (port )
128+ return port
129+ except Exception :
130+ # NoFreePortsError or any other exception
131+ raise PortExhaustedException (f"No available ports in range { self ._port_start } -{ self ._port_end } " )
132+
133+ # If port was None or already used, raise exception
134+ raise PortExhaustedException (f"No available ports in range { self ._port_start } -{ self ._port_end } " )
151135
152136 def _start_function_service (self , func_name : str , func_config : Dict , port : int ) -> FunctionUrlHandler :
153137 """Start individual function URL service"""
@@ -163,37 +147,45 @@ def _start_function_service(self, func_name: str, func_config: Dict, port: int)
163147 )
164148 return service
165149
166- def start (self ):
150+ def start (self , function_name : Optional [ str ] = None , port : Optional [ int ] = None ):
167151 """
168152 Start the Function URL services. This method will block until stopped.
153+
154+ Parameters
155+ ----------
156+ function_name : Optional[str]
157+ If specified, only start this function. If None, start all functions.
158+ port : Optional[int]
159+ If specified (with function_name), use this port. Otherwise auto-allocate.
169160 """
170161 if not self .function_urls :
171162 raise NoFunctionUrlsDefined ("No Function URLs found to start" )
172163
173- # Setup signal handlers
174- def signal_handler ( sig , frame ) :
175- LOG . info ( "Received interrupt signal. Shutting down..." )
176- self . _shutdown_event . set ( )
177-
178- signal . signal ( signal . SIGINT , signal_handler )
179- signal . signal ( signal . SIGTERM , signal_handler )
164+ # Determine which functions to start
165+ if function_name :
166+ if function_name not in self . function_urls :
167+ raise NoFunctionUrlsDefined ( f"Function { function_name } does not have a Function URL configured" )
168+ functions_to_start = { function_name : self . function_urls [ function_name ]}
169+ else :
170+ functions_to_start = self . function_urls
180171
181172 # Start services
182- self .executor = ThreadPoolExecutor (max_workers = len (self . function_urls ))
173+ self .executor = ThreadPoolExecutor (max_workers = len (functions_to_start ))
183174
184175 try :
185176 # Start each function service
186- for func_name , func_config in self .function_urls .items ():
187- port = self ._allocate_port ()
188- service = self ._start_function_service (func_name , func_config , port )
177+ for func_name , func_config in functions_to_start .items ():
178+ # Use specified port for single function, otherwise allocate
179+ service_port = port if function_name and port else self ._allocate_port ()
180+ service = self ._start_function_service (func_name , func_config , service_port )
189181 self .services [func_name ] = service
190182
191183 # Start the service (this runs Flask in a thread)
192184 service .start ()
193185
194186 # Wait for the service to be ready
195- if not self ._wait_for_service (port ):
196- LOG .warning (f"Service for { func_name } on port { port } did not start properly" )
187+ if not self ._wait_for_service (service_port ):
188+ LOG .warning (f"Service for { func_name } on port { service_port } did not start properly" )
197189
198190 # Print startup info
199191 self ._print_startup_info ()
@@ -208,61 +200,10 @@ def signal_handler(sig, frame):
208200
209201 def start_all (self ):
210202 """
211- Start all Function URL services. Alias for start() method .
203+ Start all Function URL services. Alias for start() without parameters .
212204 """
213205 return self .start ()
214206
215- def start_function (self , function_name : str , port : int ):
216- """
217- Start a specific function URL service on the given port.
218-
219- Args:
220- function_name: Name of the function to start
221- port: Port to bind the service to
222- """
223- if function_name not in self .function_urls :
224- raise NoFunctionUrlsDefined (f"Function { function_name } does not have a Function URL configured" )
225-
226- # Setup signal handlers
227- def signal_handler (sig , frame ):
228- LOG .info ("Received interrupt signal. Shutting down..." )
229- self ._shutdown_event .set ()
230-
231- signal .signal (signal .SIGINT , signal_handler )
232- signal .signal (signal .SIGTERM , signal_handler )
233-
234- function_url_config = self .function_urls [function_name ]
235- service = self ._start_function_service (function_name , function_url_config , port )
236- self .services [function_name ] = service
237-
238- # Start the service (this runs Flask in a thread)
239- service .start ()
240-
241- # Start service in thread
242- self .executor = ThreadPoolExecutor (max_workers = 1 )
243-
244- # Print startup info for single function
245- url = f"http://{ self .host } :{ port } /"
246- auth_type = function_url_config ["auth_type" ]
247- cors_enabled = bool (function_url_config .get ("cors" ))
248-
249- print ("\\ n" + "=" * 60 )
250- print ("SAM Local Function URL" )
251- print ("=" * 60 )
252- print (f"\\ n { function_name } :" )
253- print (f" URL: { url } " )
254- print (f" Auth: { auth_type } " )
255- print (f" CORS: { 'Enabled' if cors_enabled else 'Disabled' } " )
256- print ("\\ n" + "=" * 60 )
257-
258- try :
259- # Wait for shutdown signal
260- self ._shutdown_event .wait ()
261- except KeyboardInterrupt :
262- LOG .info ("Received keyboard interrupt" )
263- finally :
264- self ._shutdown_services ()
265-
266207 def _wait_for_service (self , port : int , timeout : int = 5 ) -> bool :
267208 """
268209 Wait for a service to be ready on the specified port
0 commit comments