diff --git a/samcli/commands/local/cli_common/invoke_context.py b/samcli/commands/local/cli_common/invoke_context.py index ef3affcf296..39557e46f36 100644 --- a/samcli/commands/local/cli_common/invoke_context.py +++ b/samcli/commands/local/cli_common/invoke_context.py @@ -101,6 +101,7 @@ def __init__( invoke_images: Optional[str] = None, mount_symlinks: Optional[bool] = False, no_mem_limit: Optional[bool] = False, + no_watch: Optional[bool] = False, container_dns: Optional[Tuple[str]] = None, function_logical_ids: Optional[Tuple[str, ...]] = None, ) -> None: @@ -224,6 +225,7 @@ def __init__( self._mount_symlinks: Optional[bool] = mount_symlinks self._no_mem_limit = no_mem_limit + self._no_watch = no_watch # Note(xinhol): despite self._function_provider and self._stacks are initialized as None # they will be assigned with a non-None value in __enter__() and @@ -261,6 +263,15 @@ def __enter__(self) -> "InvokeContext": ContainersMode.COLD: [self._stacks], } + # --no-watch only applies when --warm-containers is set (RefreshableSamFunctionProvider). + # SamFunctionProvider (cold mode) has no file watcher, so the flag is meaningless there. + if self._no_watch and self._containers_mode == ContainersMode.COLD: + self._no_watch = False + LOG.warning( + "--no-watch was supplied without --warm-containers; the flag has no effect " + "without warm containers and will be ignored." + ) + # don't resolve the code URI immediately if we passed in docker vol by passing True for use_raw_codeuri # this way at the end the code URI will get resolved against the basedir option if self._docker_volume_basedir: @@ -271,6 +282,9 @@ def __enter__(self) -> "InvokeContext": if self._function_logical_ids: _function_providers_kwargs["function_logical_ids"] = self._function_logical_ids + if self._no_watch and self._containers_mode == ContainersMode.WARM: + _function_providers_kwargs["no_watch"] = True + self._function_provider = _function_providers_class[self._containers_mode]( *_function_providers_args[self._containers_mode], **_function_providers_kwargs ) @@ -580,6 +594,7 @@ def lambda_runtime(self) -> LambdaRuntime: image_builder, mount_symlinks=self._mount_symlinks, no_mem_limit=self._no_mem_limit, + no_watch=self._no_watch, ), ContainersMode.COLD: LambdaRuntime( self._container_manager, diff --git a/samcli/commands/local/cli_common/options.py b/samcli/commands/local/cli_common/options.py index 526f9ffd1f4..23e46668d7d 100644 --- a/samcli/commands/local/cli_common/options.py +++ b/samcli/commands/local/cli_common/options.py @@ -268,6 +268,16 @@ def warm_containers_common_options(f): type=click.STRING, multiple=False, ), + click.option( + "--no-watch", + is_flag=True, + default=False, + help="Disable file watching for hot reload. Only applies when --warm-containers is set. " + "When enabled, local code or template changes will not restart the running container; " + "stop and rerun the command to pick up changes. Useful when file watching causes " + "high CPU/IO (e.g. Microsoft Defender on Windows, large monorepos, or projects " + "using auto-generated directories like .sandbox).", + ), ] # Reverse the list to maintain ordering of options in help text printed with --help diff --git a/samcli/commands/local/start_api/cli.py b/samcli/commands/local/start_api/cli.py index 1d2aabea090..06e8c46b1db 100644 --- a/samcli/commands/local/start_api/cli.py +++ b/samcli/commands/local/start_api/cli.py @@ -145,6 +145,7 @@ def cli( terraform_plan_file, ssl_cert_file, ssl_key_file, + no_watch, no_memory_limit, container_dns, ): @@ -183,6 +184,7 @@ def cli( ssl_cert_file, ssl_key_file, no_memory_limit, + no_watch, container_dns, ) # pragma: no cover @@ -217,6 +219,7 @@ def do_cli( # pylint: disable=R0914 ssl_cert_file, ssl_key_file, no_mem_limit, + no_watch, container_dns, ): """ @@ -264,6 +267,7 @@ def do_cli( # pylint: disable=R0914 invoke_images=processed_invoke_images, add_host=add_host, no_mem_limit=no_mem_limit, + no_watch=no_watch, container_dns=container_dns, ) as invoke_context: ssl_context = (ssl_cert_file, ssl_key_file) if ssl_cert_file else None diff --git a/samcli/commands/local/start_api/core/options.py b/samcli/commands/local/start_api/core/options.py index 7f3a9894a2f..7770b42df60 100644 --- a/samcli/commands/local/start_api/core/options.py +++ b/samcli/commands/local/start_api/core/options.py @@ -44,6 +44,7 @@ "container_dns", "invoke_image", "disable_authorizer", + "no_watch", ] CONFIGURATION_OPTION_NAMES: List[str] = ["config_env", "config_file"] + SAVE_PARAMS_OPTIONS diff --git a/samcli/commands/local/start_lambda/cli.py b/samcli/commands/local/start_lambda/cli.py index e4250582700..216b3b7c309 100644 --- a/samcli/commands/local/start_lambda/cli.py +++ b/samcli/commands/local/start_lambda/cli.py @@ -107,6 +107,7 @@ def cli( hook_name, skip_prepare_infra, terraform_plan_file, + no_watch, no_memory_limit, container_dns, ): @@ -142,6 +143,7 @@ def cli( invoke_image, hook_name, no_memory_limit, + no_watch, container_dns, ) # pragma: no cover @@ -173,6 +175,7 @@ def do_cli( # pylint: disable=R0914 invoke_image, hook_name, no_mem_limit, + no_watch, container_dns, ): """ @@ -221,6 +224,7 @@ def do_cli( # pylint: disable=R0914 invoke_images=processed_invoke_images, function_logical_ids=function_logical_ids, no_mem_limit=no_mem_limit, + no_watch=no_watch, container_dns=container_dns, ) as invoke_context: service = LocalLambdaService(lambda_invoke_context=invoke_context, port=port, host=host) diff --git a/samcli/commands/local/start_lambda/core/options.py b/samcli/commands/local/start_lambda/core/options.py index 126af8dc172..2c740f58650 100644 --- a/samcli/commands/local/start_lambda/core/options.py +++ b/samcli/commands/local/start_lambda/core/options.py @@ -39,6 +39,7 @@ "add_host", "invoke_image", "no_memory_limit", + "no_watch", ] ARTIFACT_LOCATION_OPTIONS: List[str] = [ diff --git a/samcli/lib/providers/sam_function_provider.py b/samcli/lib/providers/sam_function_provider.py index 7cbe6b9db68..b1a9513b5c3 100644 --- a/samcli/lib/providers/sam_function_provider.py +++ b/samcli/lib/providers/sam_function_provider.py @@ -893,6 +893,7 @@ def __init__( use_raw_codeuri: bool = False, ignore_code_extraction_warnings: bool = False, function_logical_ids: Optional[Tuple[str, ...]] = None, + no_watch: Optional[bool] = False, ) -> None: """ Initialize the class with SAM template data. The SAM template passed to this provider is assumed @@ -909,6 +910,8 @@ def __init__( Note(xinhol): use_raw_codeuri is temporary to fix a bug, and will be removed for a permanent solution. :param bool ignore_code_extraction_warnings: Ignores Log warnings :param tuple function_logical_ids: Optional tuple of function logical IDs to filter by + :param bool no_watch: If True, skip creating the FileObserver entirely. The provider will not + detect template changes and stack/function refreshes will not be triggered. """ # Store function_logical_ids before calling super().__init__ @@ -930,9 +933,13 @@ def __init__( self.parent_templates_paths.append(stack.location) self.is_changed = False - self._observer = FileObserver(self._set_templates_changed) - self._observer.start() - self._watch_stack_templates(stacks) + + self._observer: Optional[FileObserver] = None + # Only initialize file watcher when --no-watch is not set + if not no_watch: + self._observer = FileObserver(self._set_templates_changed) + self._observer.start() + self._watch_stack_templates(stacks) @property def stacks(self) -> List[Stack]: @@ -995,15 +1002,17 @@ def _set_templates_changed(self, paths: List[str]) -> None: ", ".join(paths), ) self.is_changed = True - for stack in self._stacks: - self._observer.unwatch(stack.location) + if self._observer: + for stack in self._stacks: + self._observer.unwatch(stack.location) def _watch_stack_templates(self, stacks: List[Stack]) -> None: """ initialize the list of stack template watchers """ - for stack in stacks: - self._observer.watch(stack.location) + if self._observer: + for stack in stacks: + self._observer.watch(stack.location) def _refresh_loaded_functions(self) -> None: """ @@ -1039,4 +1048,5 @@ def stop_observer(self) -> None: """ Stop Observing. """ - self._observer.stop() + if self._observer: + self._observer.stop() diff --git a/samcli/lib/utils/file_observer.py b/samcli/lib/utils/file_observer.py index ba243bfee40..52f6d42f78a 100644 --- a/samcli/lib/utils/file_observer.py +++ b/samcli/lib/utils/file_observer.py @@ -182,11 +182,22 @@ def _on_change(self, resources: List[str], package_type: str) -> None: package_type: str determine if the changed resource is a source code path or an image name """ + LOG.debug( + "Acquiring lock for _on_change to process %s %s resource changes: %s", + len(resources), + package_type, + resources, + ) with self._watch_lock: changed_functions: List[FunctionConfig] = [] for resource in resources: if self._observed_functions[package_type].get(resource, None): changed_functions += self._observed_functions[package_type][resource] + LOG.debug( + "Acquired lock and processing %s changed functions from %s resources", + len(changed_functions), + len(resources), + ) self._input_on_change(changed_functions) def watch(self, function_config: FunctionConfig) -> None: @@ -203,9 +214,18 @@ def watch(self, function_config: FunctionConfig) -> None: ObserverException: if not able to observe the input function source path/image """ + LOG.debug( + "Acquiring lock for watch to observe %s function: %s", function_config.packagetype, function_config.name + ) with self._watch_lock: if self.get_resources.get(function_config.packagetype, None): resources = self.get_resources[function_config.packagetype](function_config) + LOG.debug( + "Acquired lock for watch, observing %s resources for function %s: %s", + len(resources), + function_config.name, + resources, + ) for resource in resources: functions = self._observed_functions[function_config.packagetype].get(resource, []) functions += [function_config] diff --git a/samcli/local/lambdafn/runtime.py b/samcli/local/lambdafn/runtime.py index 0473caa9b47..94efe75327b 100644 --- a/samcli/local/lambdafn/runtime.py +++ b/samcli/local/lambdafn/runtime.py @@ -526,7 +526,9 @@ class WarmLambdaRuntime(LambdaRuntime): warm containers life cycle. """ - def __init__(self, container_manager, image_builder, observer=None, mount_symlinks=False, no_mem_limit=False): + def __init__( + self, container_manager, image_builder, observer=None, mount_symlinks=False, no_mem_limit=False, no_watch=False + ): """ Initialize the Local Lambda runtime @@ -536,14 +538,24 @@ def __init__(self, container_manager, image_builder, observer=None, mount_symlin Instance of the ContainerManager class that can run a local Docker container image_builder samcli.local.docker.lambda_image.LambdaImage Instance of the LambdaImage class that can create am image - warm_containers bool - Determines if the warm containers is enabled or not. + observer + Optional observer for file watching + mount_symlinks bool + Optional. True if symlinks should be mounted in the container + no_mem_limit bool + Optional. True if memory limit should be disabled + no_watch bool + Optional. True if file watching should be disabled """ self._function_configs = {} self._containers = {} + self._no_watch = no_watch self._container_lock = threading.Lock() # Thread-safe container creation - self._observer = observer if observer else LambdaFunctionObserver(self._on_code_change) + if no_watch: + self._observer = None + else: + self._observer = observer if observer else LambdaFunctionObserver(self._on_code_change) super().__init__(container_manager, image_builder, mount_symlinks=mount_symlinks, no_mem_limit=no_mem_limit) @@ -604,7 +616,7 @@ def create( if container: self._container_manager.stop(container) self._containers.pop(function_path, None) - if exist_function_config: + if exist_function_config and self._observer is not None: self._observer.unwatch(exist_function_config) container = None @@ -612,9 +624,10 @@ def create( elif container and container.is_created(): return container - # Create new container - self._observer.watch(function_config) - self._observer.start() + # Create new container; only watch/start observer when file watching is enabled + if self._observer is not None: + self._observer.watch(function_config) + self._observer.start() container = super().create( function_config, @@ -702,7 +715,8 @@ def clean_running_containers_and_related_resources(self): self._function_configs.clear() self._clean_decompressed_paths() - self._observer.stop() + if self._observer is not None: + self._observer.stop() def _on_code_change(self, functions): """ @@ -723,7 +737,8 @@ def _on_code_change(self, functions): function_full_path, resource, ) - self._observer.unwatch(function_config) + if self._observer is not None: + self._observer.unwatch(function_config) self._function_configs.pop(function_full_path, None) container = self._containers.get(function_full_path, None) if container: diff --git a/schema/samcli.json b/schema/samcli.json index f2c93aa4492..303e8eabfdc 100644 --- a/schema/samcli.json +++ b/schema/samcli.json @@ -730,7 +730,7 @@ "properties": { "parameters": { "title": "Parameters for the local start api command", - "description": "Available parameters for the local start api command:\n* terraform_plan_file:\nUsed for passing a custom plan file when executing the Terraform hook.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* host:\nLocal hostname or IP address to bind to (default: '127.0.0.1')\n* port:\nLocal port number to listen on (default: '3000')\n* static_dir:\nAny static assets (e.g. CSS/Javascript/HTML) files located in this directory will be presented at /\n* disable_authorizer:\nDisable custom Lambda Authorizers from being parsed and invoked.\n* ssl_cert_file:\nPath to SSL certificate file (default: None)\n* ssl_key_file:\nPath to SSL key file (default: None)\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* env_vars:\nJSON file containing values for Lambda function's environment variables.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* debug_port:\nWhen specified, Lambda function container will start in debug mode and will expose this port on localhost.\n* debugger_path:\nHost path to a debugger that will be mounted into the Lambda container.\n* debug_args:\nAdditional arguments to be passed to the debugger.\n* container_env_vars:\nJSON file containing additional environment variables to be set within the container when used in a debugging session locally.\n* docker_volume_basedir:\nSpecify the location basedir where the SAM template exists. If Docker is running on a remote machine, Path of the SAM template must be mounted on the Docker machine and modified to match the remote machine.\n* log_file:\nFile to capture output logs.\n* layer_cache_basedir:\nSpecify the location basedir where the lambda layers used by the template will be downloaded to.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* force_image_build:\nForce rebuilding the image used for invoking functions with layers.\n* warm_containers:\nOptional. Specifies how AWS SAM CLI manages containers for each function.' \n\nTwo modes are available:\nEAGER: Containers for all functions are loaded at startup and persist between invocations.\nLAZY: Containers are only loaded when each function is first invoked. \n Those containers persist for additional invocations.\n* debug_function:\nOptional. Specifies the Lambda Function logicalId to apply debug options to when --warm-containers is specified. This parameter applies to --debug-port, --debugger-path, and --debug-args.\n* shutdown:\nEmulate a shutdown event after invoke completes, to test extension handling of shutdown behavior.\n* container_host:\nHost of locally emulated Lambda container. This option is useful when the container runs on a different host than AWS SAM CLI. For example, if one wants to run AWS SAM CLI in a Docker container on macOS, this option could specify `host.docker.internal`\n* container_host_interface:\nIP address of the host network interface that container ports should bind to. Use 0.0.0.0 to bind to all interfaces.\n* add_host:\nPasses a hostname to IP address mapping to the Docker container's host file. This parameter can be passed multiple times.Example:--add-host example.com:127.0.0.1\n* container_dns:\nSet custom DNS servers for the Lambda container. This parameter can be passed multiple times to specify multiple DNS servers. Example: --container-dns 8.8.8.8 --container-dns 1.1.1.1\n* invoke_image:\nContainer image URIs for invoking functions or starting api and function. One can specify the image URI used for the local function invocation (--invoke-image public.ecr.aws/sam/build-nodejs20.x:latest). One can also specify for each individual function with (--invoke-image Function1=public.ecr.aws/sam/build-nodejs20.x:latest). If a function does not have invoke image specified, the default AWS SAM CLI emulation image will be used.\n* no_memory_limit:\nRemoves the Memory limit during emulation. With this parameter, the underlying container will run without a --memory parameter\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "description": "Available parameters for the local start api command:\n* terraform_plan_file:\nUsed for passing a custom plan file when executing the Terraform hook.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* host:\nLocal hostname or IP address to bind to (default: '127.0.0.1')\n* port:\nLocal port number to listen on (default: '3000')\n* static_dir:\nAny static assets (e.g. CSS/Javascript/HTML) files located in this directory will be presented at /\n* disable_authorizer:\nDisable custom Lambda Authorizers from being parsed and invoked.\n* ssl_cert_file:\nPath to SSL certificate file (default: None)\n* ssl_key_file:\nPath to SSL key file (default: None)\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* env_vars:\nJSON file containing values for Lambda function's environment variables.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* debug_port:\nWhen specified, Lambda function container will start in debug mode and will expose this port on localhost.\n* debugger_path:\nHost path to a debugger that will be mounted into the Lambda container.\n* debug_args:\nAdditional arguments to be passed to the debugger.\n* container_env_vars:\nJSON file containing additional environment variables to be set within the container when used in a debugging session locally.\n* docker_volume_basedir:\nSpecify the location basedir where the SAM template exists. If Docker is running on a remote machine, Path of the SAM template must be mounted on the Docker machine and modified to match the remote machine.\n* log_file:\nFile to capture output logs.\n* layer_cache_basedir:\nSpecify the location basedir where the lambda layers used by the template will be downloaded to.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* force_image_build:\nForce rebuilding the image used for invoking functions with layers.\n* warm_containers:\nOptional. Specifies how AWS SAM CLI manages containers for each function.' \n\nTwo modes are available:\nEAGER: Containers for all functions are loaded at startup and persist between invocations.\nLAZY: Containers are only loaded when each function is first invoked. \n Those containers persist for additional invocations.\n* debug_function:\nOptional. Specifies the Lambda Function logicalId to apply debug options to when --warm-containers is specified. This parameter applies to --debug-port, --debugger-path, and --debug-args.\n* no_watch:\nDisable file watching for hot reload. Only applies when --warm-containers is set. When enabled, local code or template changes will not restart the running container; stop and rerun the command to pick up changes. Useful when file watching causes high CPU/IO (e.g. Microsoft Defender on Windows, large monorepos, or projects using auto-generated directories like .sandbox).\n* shutdown:\nEmulate a shutdown event after invoke completes, to test extension handling of shutdown behavior.\n* container_host:\nHost of locally emulated Lambda container. This option is useful when the container runs on a different host than AWS SAM CLI. For example, if one wants to run AWS SAM CLI in a Docker container on macOS, this option could specify `host.docker.internal`\n* container_host_interface:\nIP address of the host network interface that container ports should bind to. Use 0.0.0.0 to bind to all interfaces.\n* add_host:\nPasses a hostname to IP address mapping to the Docker container's host file. This parameter can be passed multiple times.Example:--add-host example.com:127.0.0.1\n* container_dns:\nSet custom DNS servers for the Lambda container. This parameter can be passed multiple times to specify multiple DNS servers. Example: --container-dns 8.8.8.8 --container-dns 1.1.1.1\n* invoke_image:\nContainer image URIs for invoking functions or starting api and function. One can specify the image URI used for the local function invocation (--invoke-image public.ecr.aws/sam/build-nodejs20.x:latest). One can also specify for each individual function with (--invoke-image Function1=public.ecr.aws/sam/build-nodejs20.x:latest). If a function does not have invoke image specified, the default AWS SAM CLI emulation image will be used.\n* no_memory_limit:\nRemoves the Memory limit during emulation. With this parameter, the underlying container will run without a --memory parameter\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "terraform_plan_file": { @@ -868,6 +868,11 @@ "type": "string", "description": "Optional. Specifies the Lambda Function logicalId to apply debug options to when --warm-containers is specified. This parameter applies to --debug-port, --debugger-path, and --debug-args." }, + "no_watch": { + "title": "no_watch", + "type": "boolean", + "description": "Disable file watching for hot reload. Only applies when --warm-containers is set. When enabled, local code or template changes will not restart the running container; stop and rerun the command to pick up changes. Useful when file watching causes high CPU/IO (e.g. Microsoft Defender on Windows, large monorepos, or projects using auto-generated directories like .sandbox)." + }, "shutdown": { "title": "shutdown", "type": "boolean", @@ -946,7 +951,7 @@ "properties": { "parameters": { "title": "Parameters for the local start lambda command", - "description": "Available parameters for the local start lambda command:\n* terraform_plan_file:\nUsed for passing a custom plan file when executing the Terraform hook.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* host:\nLocal hostname or IP address to bind to (default: '127.0.0.1')\n* port:\nLocal port number to listen on (default: '3001')\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* env_vars:\nJSON file containing values for Lambda function's environment variables.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* debug_port:\nWhen specified, Lambda function container will start in debug mode and will expose this port on localhost.\n* debugger_path:\nHost path to a debugger that will be mounted into the Lambda container.\n* debug_args:\nAdditional arguments to be passed to the debugger.\n* container_env_vars:\nJSON file containing additional environment variables to be set within the container when used in a debugging session locally.\n* docker_volume_basedir:\nSpecify the location basedir where the SAM template exists. If Docker is running on a remote machine, Path of the SAM template must be mounted on the Docker machine and modified to match the remote machine.\n* log_file:\nFile to capture output logs.\n* layer_cache_basedir:\nSpecify the location basedir where the lambda layers used by the template will be downloaded to.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* force_image_build:\nForce rebuilding the image used for invoking functions with layers.\n* warm_containers:\nOptional. Specifies how AWS SAM CLI manages containers for each function.' \n\nTwo modes are available:\nEAGER: Containers for all functions are loaded at startup and persist between invocations.\nLAZY: Containers are only loaded when each function is first invoked. \n Those containers persist for additional invocations.\n* debug_function:\nOptional. Specifies the Lambda Function logicalId to apply debug options to when --warm-containers is specified. This parameter applies to --debug-port, --debugger-path, and --debug-args.\n* shutdown:\nEmulate a shutdown event after invoke completes, to test extension handling of shutdown behavior.\n* container_host:\nHost of locally emulated Lambda container. This option is useful when the container runs on a different host than AWS SAM CLI. For example, if one wants to run AWS SAM CLI in a Docker container on macOS, this option could specify `host.docker.internal`\n* container_host_interface:\nIP address of the host network interface that container ports should bind to. Use 0.0.0.0 to bind to all interfaces.\n* add_host:\nPasses a hostname to IP address mapping to the Docker container's host file. This parameter can be passed multiple times.Example:--add-host example.com:127.0.0.1\n* container_dns:\nSet custom DNS servers for the Lambda container. This parameter can be passed multiple times to specify multiple DNS servers. Example: --container-dns 8.8.8.8 --container-dns 1.1.1.1\n* invoke_image:\nContainer image URIs for invoking functions or starting api and function. One can specify the image URI used for the local function invocation (--invoke-image public.ecr.aws/sam/build-nodejs20.x:latest). One can also specify for each individual function with (--invoke-image Function1=public.ecr.aws/sam/build-nodejs20.x:latest). If a function does not have invoke image specified, the default AWS SAM CLI emulation image will be used.\n* no_memory_limit:\nRemoves the Memory limit during emulation. With this parameter, the underlying container will run without a --memory parameter\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "description": "Available parameters for the local start lambda command:\n* terraform_plan_file:\nUsed for passing a custom plan file when executing the Terraform hook.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* host:\nLocal hostname or IP address to bind to (default: '127.0.0.1')\n* port:\nLocal port number to listen on (default: '3001')\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* env_vars:\nJSON file containing values for Lambda function's environment variables.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* debug_port:\nWhen specified, Lambda function container will start in debug mode and will expose this port on localhost.\n* debugger_path:\nHost path to a debugger that will be mounted into the Lambda container.\n* debug_args:\nAdditional arguments to be passed to the debugger.\n* container_env_vars:\nJSON file containing additional environment variables to be set within the container when used in a debugging session locally.\n* docker_volume_basedir:\nSpecify the location basedir where the SAM template exists. If Docker is running on a remote machine, Path of the SAM template must be mounted on the Docker machine and modified to match the remote machine.\n* log_file:\nFile to capture output logs.\n* layer_cache_basedir:\nSpecify the location basedir where the lambda layers used by the template will be downloaded to.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* force_image_build:\nForce rebuilding the image used for invoking functions with layers.\n* warm_containers:\nOptional. Specifies how AWS SAM CLI manages containers for each function.' \n\nTwo modes are available:\nEAGER: Containers for all functions are loaded at startup and persist between invocations.\nLAZY: Containers are only loaded when each function is first invoked. \n Those containers persist for additional invocations.\n* debug_function:\nOptional. Specifies the Lambda Function logicalId to apply debug options to when --warm-containers is specified. This parameter applies to --debug-port, --debugger-path, and --debug-args.\n* no_watch:\nDisable file watching for hot reload. Only applies when --warm-containers is set. When enabled, local code or template changes will not restart the running container; stop and rerun the command to pick up changes. Useful when file watching causes high CPU/IO (e.g. Microsoft Defender on Windows, large monorepos, or projects using auto-generated directories like .sandbox).\n* shutdown:\nEmulate a shutdown event after invoke completes, to test extension handling of shutdown behavior.\n* container_host:\nHost of locally emulated Lambda container. This option is useful when the container runs on a different host than AWS SAM CLI. For example, if one wants to run AWS SAM CLI in a Docker container on macOS, this option could specify `host.docker.internal`\n* container_host_interface:\nIP address of the host network interface that container ports should bind to. Use 0.0.0.0 to bind to all interfaces.\n* add_host:\nPasses a hostname to IP address mapping to the Docker container's host file. This parameter can be passed multiple times.Example:--add-host example.com:127.0.0.1\n* container_dns:\nSet custom DNS servers for the Lambda container. This parameter can be passed multiple times to specify multiple DNS servers. Example: --container-dns 8.8.8.8 --container-dns 1.1.1.1\n* invoke_image:\nContainer image URIs for invoking functions or starting api and function. One can specify the image URI used for the local function invocation (--invoke-image public.ecr.aws/sam/build-nodejs20.x:latest). One can also specify for each individual function with (--invoke-image Function1=public.ecr.aws/sam/build-nodejs20.x:latest). If a function does not have invoke image specified, the default AWS SAM CLI emulation image will be used.\n* no_memory_limit:\nRemoves the Memory limit during emulation. With this parameter, the underlying container will run without a --memory parameter\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "terraform_plan_file": { @@ -1063,6 +1068,11 @@ "type": "string", "description": "Optional. Specifies the Lambda Function logicalId to apply debug options to when --warm-containers is specified. This parameter applies to --debug-port, --debugger-path, and --debug-args." }, + "no_watch": { + "title": "no_watch", + "type": "boolean", + "description": "Disable file watching for hot reload. Only applies when --warm-containers is set. When enabled, local code or template changes will not restart the running container; stop and rerun the command to pick up changes. Useful when file watching causes high CPU/IO (e.g. Microsoft Defender on Windows, large monorepos, or projects using auto-generated directories like .sandbox)." + }, "shutdown": { "title": "shutdown", "type": "boolean", diff --git a/tests/unit/commands/local/cli_common/test_invoke_context.py b/tests/unit/commands/local/cli_common/test_invoke_context.py index fd2ea8846d6..71a59d3712b 100644 --- a/tests/unit/commands/local/cli_common/test_invoke_context.py +++ b/tests/unit/commands/local/cli_common/test_invoke_context.py @@ -522,6 +522,67 @@ def test_docker_volume_basedir_set_use_raw_codeuri( extract_func_mock.assert_called_with([], expected, False, False, None) + @patch("samcli.commands.local.cli_common.invoke_context.ContainerManager") + @patch("samcli.commands.local.cli_common.invoke_context.RefreshableSamFunctionProvider") + @patch("samcli.commands.local.cli_common.invoke_context.InvokeContext._add_account_id_to_global") + def test_no_watch_passed_through_to_provider_when_warm_containers_enabled( + self, _add_account_id_to_global_mock, RefreshableSamFunctionProviderMock, ContainerManagerMock + ): + function_provider = Mock() + function_provider.get_all.return_value = [] + function_provider.functions = {} + RefreshableSamFunctionProviderMock.return_value = function_provider + + invoke_context = InvokeContext( + template_file="template_file", + warm_container_initialization_mode=ContainersInitializationMode.LAZY.value, + no_watch=True, + ) + + invoke_context._get_stacks = Mock(return_value=[]) + invoke_context._get_env_vars_value = Mock() + invoke_context._setup_log_file = Mock() + invoke_context._get_debug_context = Mock() + + invoke_context.__enter__() + + # no_watch should be forwarded as a kwarg only when warm containers is set + _, kwargs = RefreshableSamFunctionProviderMock.call_args + self.assertEqual(kwargs.get("no_watch"), True) + self.assertTrue(invoke_context._no_watch) + + @patch("samcli.commands.local.cli_common.invoke_context.ContainerManager") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + @patch("samcli.commands.local.cli_common.invoke_context.InvokeContext._add_account_id_to_global") + def test_no_watch_ignored_with_warning_when_warm_containers_disabled( + self, _add_account_id_to_global_mock, SamFunctionProviderMock, ContainerManagerMock + ): + function_provider = Mock() + function_provider.get_all.return_value = [] + function_provider.functions = {} + SamFunctionProviderMock.return_value = function_provider + + invoke_context = InvokeContext( + template_file="template_file", + no_watch=True, # without warm containers => should be ignored + ) + + invoke_context._get_stacks = Mock(return_value=[]) + invoke_context._get_env_vars_value = Mock() + invoke_context._setup_log_file = Mock() + invoke_context._get_debug_context = Mock() + + with self.assertLogs("samcli.commands.local.cli_common.invoke_context", level="WARNING") as log_ctx: + invoke_context.__enter__() + + # The cold-mode SamFunctionProvider must NOT receive no_watch (it doesn't accept it) + _, kwargs = SamFunctionProviderMock.call_args + self.assertNotIn("no_watch", kwargs) + # Internal flag is reset to avoid downstream effects + self.assertFalse(invoke_context._no_watch) + # And we surface a warning to the user + self.assertTrue(any("--no-watch" in msg and "warm-containers" in msg for msg in log_ctx.output)) + class TestInvokeContext__exit__(TestCase): def test_must_close_opened_logfile(self): @@ -780,7 +841,7 @@ def test_must_create_runner_using_warm_containers( self.assertEqual(result, runner_mock) WarmLambdaRuntimeMock.assert_called_with( - container_manager_mock, image_mock, mount_symlinks=False, no_mem_limit=False + container_manager_mock, image_mock, mount_symlinks=False, no_mem_limit=False, no_watch=False ) lambda_image_patch.assert_called_once_with(download_mock, True, True, invoke_images=None) LocalLambdaMock.assert_called_with( diff --git a/tests/unit/commands/local/lib/test_sam_function_provider.py b/tests/unit/commands/local/lib/test_sam_function_provider.py index c62c1e7d860..95831d2caa4 100644 --- a/tests/unit/commands/local/lib/test_sam_function_provider.py +++ b/tests/unit/commands/local/lib/test_sam_function_provider.py @@ -2297,6 +2297,29 @@ def test_reload_flag_set_to_true_incase_any_template_got_changed( self.assertTrue(provider.is_changed) self.file_observer.unwatch.assert_has_calls([call("template.yaml"), call("child/template.yaml")]) + @patch("samcli.lib.providers.sam_function_provider.FileObserver") + @patch.object(SamFunctionProvider, "_extract_functions") + @patch("samcli.lib.providers.provider.SamBaseProvider.get_template") + def test_no_watch_skips_file_observer_creation(self, get_template_mock, extract_mock, FileObserverMock): + extract_mock.return_value = {"foo": "bar"} + template = {"Resources": {"a": "b"}} + get_template_mock.return_value = template + stack = make_root_stack(template, self.parameter_overrides) + stack2 = Stack("", "childStack", "child/template.yaml", self.parameter_overrides, template) + + provider = RefreshableSamFunctionProvider( + [stack, stack2], self.parameter_overrides, self.global_parameter_overrides, no_watch=True + ) + + # Observer must not be constructed at all when no_watch is set + FileObserverMock.assert_not_called() + self.assertIsNone(provider._observer) + + # Subsequent lifecycle methods must be no-ops, not crash + provider._set_templates_changed(["child/template.yaml"]) + provider.stop_observer() + self.assertTrue(provider.is_changed) + @patch("samcli.lib.providers.sam_function_provider.SamLocalStackProvider.get_stacks") @patch("samcli.lib.providers.sam_function_provider.FileObserver") @patch.object(SamFunctionProvider, "_extract_functions") diff --git a/tests/unit/commands/local/start_api/test_cli.py b/tests/unit/commands/local/start_api/test_cli.py index 3eec1eb12a7..99cf52c9d8c 100644 --- a/tests/unit/commands/local/start_api/test_cli.py +++ b/tests/unit/commands/local/start_api/test_cli.py @@ -57,6 +57,7 @@ def setUp(self): self.container_host_interface = "127.0.0.1" self.invoke_image = () self.no_mem_limit = False + self.no_watch = False self.container_dns = None @patch("samcli.commands.local.cli_common.invoke_context.InvokeContext") @@ -100,6 +101,7 @@ def test_cli_must_setup_context_and_start_service(self, local_api_service_mock, add_host=self.add_host, invoke_images={}, no_mem_limit=self.no_mem_limit, + no_watch=self.no_watch, container_dns=self.container_dns, ) @@ -114,6 +116,56 @@ def test_cli_must_setup_context_and_start_service(self, local_api_service_mock, service_mock.start.assert_called_with() + @patch("samcli.commands.local.cli_common.invoke_context.InvokeContext") + @patch("samcli.commands.local.lib.local_api_service.LocalApiService") + def test_cli_must_pass_no_watch_flag_when_set(self, local_api_service_mock, invoke_context_mock): + # Mock the __enter__ method to return a object inside a context manager + context_mock = Mock() + invoke_context_mock.return_value.__enter__.return_value = context_mock + + service_mock = Mock() + local_api_service_mock.return_value = service_mock + + # Set no_watch to True + self.no_watch = True + self.warm_containers = None + self.debug_function = None + self.disable_authorizer = False + + self.call_cli() + + # Verify that no_watch=True was passed to InvokeContext + invoke_context_mock.assert_called_with( + template_file=self.template, + function_identifier=None, + env_vars_file=self.env_vars, + docker_volume_basedir=self.docker_volume_basedir, + docker_network=self.docker_network, + log_file=self.log_file, + skip_pull_image=self.skip_pull_image, + debug_ports=self.debug_ports, + debug_args=self.debug_args, + debugger_path=self.debugger_path, + container_env_vars_file=self.container_env_vars, + parameter_overrides=self.parameter_overrides, + layer_cache_basedir=self.layer_cache_basedir, + force_image_build=self.force_image_build, + aws_region=self.region_name, + aws_profile=self.profile, + warm_container_initialization_mode=self.warm_containers, + debug_function=self.debug_function, + shutdown=self.shutdown, + container_host=self.container_host, + container_host_interface=self.container_host_interface, + add_host=self.add_host, + invoke_images={}, + no_mem_limit=self.no_mem_limit, + no_watch=True, # Verify this is True + container_dns=self.container_dns, + ) + + service_mock.start.assert_called_with() + @patch("samcli.commands.local.cli_common.invoke_context.InvokeContext") @patch("samcli.commands.local.lib.local_api_service.LocalApiService") def test_must_raise_if_no_api_defined(self, local_api_service_mock, invoke_context_mock): @@ -233,5 +285,6 @@ def call_cli(self): disable_authorizer=self.disable_authorizer, add_host=self.add_host, no_mem_limit=self.no_mem_limit, + no_watch=self.no_watch, container_dns=self.container_dns, ) diff --git a/tests/unit/commands/local/start_lambda/test_cli.py b/tests/unit/commands/local/start_lambda/test_cli.py index f4662622966..2808d01c68f 100644 --- a/tests/unit/commands/local/start_lambda/test_cli.py +++ b/tests/unit/commands/local/start_lambda/test_cli.py @@ -46,6 +46,7 @@ def setUp(self): self.invoke_image = () self.hook_name = None self.no_mem_limit = False + self.no_watch = False self.container_dns = None @patch("samcli.commands.local.cli_common.invoke_context.InvokeContext") @@ -88,6 +89,7 @@ def test_cli_must_setup_context_and_start_service(self, local_lambda_service_moc invoke_images={}, function_logical_ids=(), no_mem_limit=self.no_mem_limit, + no_watch=self.no_watch, container_dns=self.container_dns, ) @@ -95,6 +97,56 @@ def test_cli_must_setup_context_and_start_service(self, local_lambda_service_moc service_mock.start.assert_called_with() + @patch("samcli.commands.local.cli_common.invoke_context.InvokeContext") + @patch("samcli.commands.local.lib.local_lambda_service.LocalLambdaService") + def test_cli_must_pass_no_watch_flag_when_set(self, local_lambda_service_mock, invoke_context_mock): + # Mock the __enter__ method to return a object inside a context manager + context_mock = Mock() + invoke_context_mock.return_value.__enter__.return_value = context_mock + + service_mock = Mock() + local_lambda_service_mock.return_value = service_mock + + # Set no_watch to True + self.no_watch = True + self.warm_containers = None + self.debug_function = None + + self.call_cli() + + # Verify that no_watch=True was passed to InvokeContext + invoke_context_mock.assert_called_with( + template_file=self.template, + function_identifier=None, + env_vars_file=self.env_vars, + container_env_vars_file=self.container_env_vars, + docker_volume_basedir=self.docker_volume_basedir, + docker_network=self.docker_network, + log_file=self.log_file, + skip_pull_image=self.skip_pull_image, + debug_ports=self.debug_ports, + debug_args=self.debug_args, + debugger_path=self.debugger_path, + parameter_overrides=self.parameter_overrides, + layer_cache_basedir=self.layer_cache_basedir, + force_image_build=self.force_image_build, + aws_region=self.region_name, + aws_profile=self.profile, + warm_container_initialization_mode=self.warm_containers, + debug_function=self.debug_function, + shutdown=self.shutdown, + container_host=self.container_host, + container_host_interface=self.container_host_interface, + add_host=self.add_host, + invoke_images={}, + function_logical_ids=(), + no_mem_limit=self.no_mem_limit, + no_watch=True, # Verify this is True + container_dns=self.container_dns, + ) + + service_mock.start.assert_called_with() + @parameterized.expand( [ (InvalidSamDocumentException("bad template"), "bad template"), @@ -192,5 +244,6 @@ def call_cli(self): invoke_image=self.invoke_image, hook_name=self.hook_name, no_mem_limit=self.no_mem_limit, + no_watch=self.no_watch, container_dns=self.container_dns, ) diff --git a/tests/unit/commands/samconfig/test_samconfig.py b/tests/unit/commands/samconfig/test_samconfig.py index 99152dd0928..f582bc6cbeb 100644 --- a/tests/unit/commands/samconfig/test_samconfig.py +++ b/tests/unit/commands/samconfig/test_samconfig.py @@ -739,6 +739,7 @@ def test_local_start_api(self, do_cli_mock): None, None, False, + False, (), ) @@ -806,6 +807,7 @@ def test_local_start_lambda(self, do_cli_mock): ("image",), None, False, + False, (), ) @@ -1453,6 +1455,7 @@ def test_override_with_cli_params(self, do_cli_mock): ("image",), None, False, + False, (), ) @@ -1556,6 +1559,7 @@ def test_override_with_cli_params_and_envvars(self, do_cli_mock): ("image",), None, True, + False, (), ) diff --git a/tests/unit/local/lambdafn/test_runtime.py b/tests/unit/local/lambdafn/test_runtime.py index 10658887341..b85b5a5f0fe 100644 --- a/tests/unit/local/lambdafn/test_runtime.py +++ b/tests/unit/local/lambdafn/test_runtime.py @@ -1231,6 +1231,54 @@ def test_must_ignore_debug_options_if_function_name_is_not_debug_function( self.assertEqual(self.runtime._containers[self.full_path], container) +class TestWarmLambdaRuntime_no_watch(TestCase): + """Tests that verify --no-watch suppresses observer setup and lifecycle calls.""" + + def setUp(self): + self.manager_mock = Mock() + self.lambda_image_mock = Mock() + self.name = "name" + self.full_path = "stack/name" + self.func_config = FunctionConfig( + self.name, + self.full_path, + "runtime", + "handler", + None, + None, + ZIP, + "code-path", + [], + "arm64", + ) + self.func_config.env_vars = Mock() + self.func_config.env_vars.resolve.return_value = {} + + @patch("samcli.local.lambdafn.runtime.LambdaFunctionObserver") + def test_observer_is_not_created_when_no_watch_is_true(self, LambdaFunctionObserverMock): + runtime = WarmLambdaRuntime(self.manager_mock, self.lambda_image_mock, no_watch=True) + + LambdaFunctionObserverMock.assert_not_called() + self.assertIsNone(runtime._observer) + + @patch("samcli.local.lambdafn.runtime.LambdaContainer") + def test_create_does_not_call_watch_or_start_when_no_watch_is_true(self, LambdaContainerMock): + runtime = WarmLambdaRuntime(self.manager_mock, self.lambda_image_mock, no_watch=True) + runtime._get_code_dir = MagicMock(return_value="code-dir") + LambdaContainerMock.return_value = Mock() + + # Should not raise even though _observer is None + runtime.create(self.func_config, debug_context=None) + # Container is still created and tracked + self.assertIn(self.full_path, runtime._containers) + + def test_clean_warm_containers_does_not_call_observer_stop_when_no_watch_is_true(self): + runtime = WarmLambdaRuntime(self.manager_mock, self.lambda_image_mock, no_watch=True) + runtime._containers = {} + # Should be a no-op; should not raise + runtime.clean_running_containers_and_related_resources() + + class TestWarmLambdaRuntime_get_code_dir(TestCase): def setUp(self): self.manager_mock = Mock()