3
3
"""
4
4
5
5
import logging
6
- import signal
7
6
import socket
8
7
import sys
9
8
import time
14
13
from samcli .commands .local .cli_common .invoke_context import InvokeContext
15
14
from samcli .commands .local .lib .exceptions import NoFunctionUrlsDefined
16
15
from samcli .commands .local .lib .function_url_handler import FunctionUrlHandler
16
+ from samcli .local .docker .utils import find_free_port
17
17
18
18
LOG = logging .getLogger (__name__ )
19
19
@@ -106,7 +106,7 @@ def _discover_function_urls(self):
106
106
107
107
def _allocate_port (self ) -> int :
108
108
"""
109
- Allocate next available port in range
109
+ Allocate next available port in range using existing find_free_port utility
110
110
111
111
Returns
112
112
-------
@@ -118,36 +118,20 @@ def _allocate_port(self) -> int:
118
118
PortExhaustedException
119
119
When no ports are available in the specified range
120
120
"""
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
143
124
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 } " )
151
135
152
136
def _start_function_service (self , func_name : str , func_config : Dict , port : int ) -> FunctionUrlHandler :
153
137
"""Start individual function URL service"""
@@ -163,37 +147,45 @@ def _start_function_service(self, func_name: str, func_config: Dict, port: int)
163
147
)
164
148
return service
165
149
166
- def start (self ):
150
+ def start (self , function_name : Optional [ str ] = None , port : Optional [ int ] = None ):
167
151
"""
168
152
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.
169
160
"""
170
161
if not self .function_urls :
171
162
raise NoFunctionUrlsDefined ("No Function URLs found to start" )
172
163
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
180
171
181
172
# Start services
182
- self .executor = ThreadPoolExecutor (max_workers = len (self . function_urls ))
173
+ self .executor = ThreadPoolExecutor (max_workers = len (functions_to_start ))
183
174
184
175
try :
185
176
# 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 )
189
181
self .services [func_name ] = service
190
182
191
183
# Start the service (this runs Flask in a thread)
192
184
service .start ()
193
185
194
186
# 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" )
197
189
198
190
# Print startup info
199
191
self ._print_startup_info ()
@@ -208,61 +200,10 @@ def signal_handler(sig, frame):
208
200
209
201
def start_all (self ):
210
202
"""
211
- Start all Function URL services. Alias for start() method .
203
+ Start all Function URL services. Alias for start() without parameters .
212
204
"""
213
205
return self .start ()
214
206
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
-
266
207
def _wait_for_service (self , port : int , timeout : int = 5 ) -> bool :
267
208
"""
268
209
Wait for a service to be ready on the specified port
0 commit comments