1818allowing users to track resource creation progress, deployment status, and service health through
1919an interactive command-line interface with live updates and visual indicators.
2020
21- The data collection logic is handled by ServiceViewCollector, which parses AWS resources and
22- formats monitoring output. This module focuses on display and user interaction.
23-
2421The module implements two resource view modes:
2522- RESOURCE: Displays all resources associated with the service
2623- DEPLOYMENT: Shows resources that have changed in the most recent deployment
2724
25+ And two display modes:
26+ - INTERACTIVE: Real-time display with spinner and keyboard navigation (requires TTY)
27+ - TEXT-ONLY: Text output with timestamps and change detection (works without TTY)
28+
2829Key Features:
2930- Real-time progress monitoring with spinner animations
3031- Diff-based resource tracking for deployment changes
3637 ECSExpressGatewayServiceWatcher: Core monitoring logic and resource tracking
3738
3839Usage:
39- aws ecs monitor-express-gateway-service --service-arn <arn> [--resource-view RESOURCE|DEPLOYMENT]
40+ aws ecs monitor-express-gateway-service --service-arn <arn> [--resource-view RESOURCE|DEPLOYMENT] [--mode INTERACTIVE|TEXT-ONLY]
4041"""
4142
42- import asyncio
4343import sys
44- import threading
4544import time
4645
4746from botocore .exceptions import ClientError
4847
4948from awscli .customizations .commands import BasicCommand
5049from awscli .customizations .ecs .exceptions import MonitoringError
50+ from awscli .customizations .ecs .expressgateway .display_strategy import (
51+ DisplayStrategy ,
52+ InteractiveDisplayStrategy ,
53+ TextOnlyDisplayStrategy ,
54+ )
5155from awscli .customizations .ecs .prompt_toolkit_display import Display
5256from awscli .customizations .ecs .serviceviewcollector import ServiceViewCollector
5357from awscli .customizations .utils import uni_print
@@ -64,14 +68,16 @@ class ECSMonitorExpressGatewayService(BasicCommand):
6468
6569 DESCRIPTION = (
6670 "Monitors the progress of resource creation for an ECS Express Gateway Service. "
67- "This command provides real-time monitoring of service deployments with interactive "
68- "progress display, showing the status of load balancers, security groups, auto-scaling "
71+ "This command provides real-time monitoring of service deployments showing the status "
72+ "of load balancers, security groups, auto-scaling "
6973 "configurations, and other AWS resources as they are created or updated. "
7074 "Use ``--resource-view RESOURCE`` to view all service resources, or ``--resource-view DEPLOYMENT`` to track only "
7175 "resources that have changed in the most recent deployment. "
72- "The command requires a terminal (TTY) to run and the monitoring session continues "
73- "until manually stopped by the user or the specified timeout is reached. "
74- "Use keyboard shortcuts to navigate: up/down to scroll through resources, 'q' to quit monitoring."
76+ "Choose ``--mode INTERACTIVE`` for real-time display with keyboard navigation (requires TTY), "
77+ "or ``--mode text-only`` for text output with timestamps (works without TTY). "
78+ "The monitoring session continues until manually stopped by the user or the specified timeout is reached. "
79+ "In interactive mode, use keyboard shortcuts: up/down to scroll through resources, 'q' to quit. "
80+ "In TEXT-ONLY mode, press Ctrl+C to stop monitoring."
7581 )
7682
7783 ARG_TABLE = [
@@ -96,6 +102,16 @@ class ECSMonitorExpressGatewayService(BasicCommand):
96102 'default' : 'RESOURCE' ,
97103 'choices' : ['RESOURCE' , 'DEPLOYMENT' ],
98104 },
105+ {
106+ 'name' : 'mode' ,
107+ 'help_text' : (
108+ "Display mode for monitoring output. "
109+ "interactive (default if TTY available) - Real-time display with spinner and keyboard navigation. "
110+ "text-only - Text output with timestamps and change detection (works without TTY)."
111+ ),
112+ 'required' : False ,
113+ 'choices' : ['interactive' , 'text-only' ],
114+ },
99115 {
100116 'name' : 'timeout' ,
101117 'help_text' : (
@@ -126,15 +142,12 @@ def _run_main(self, parsed_args, parsed_globals):
126142 parsed_globals: Global CLI configuration including region and endpoint
127143 """
128144 try :
129- # Check if running in a TTY for interactive display
130- if not sys .stdout .isatty ():
131- uni_print (
132- "Error: This command requires a TTY. "
133- "Please run this command in a terminal." ,
134- sys .stderr ,
135- )
136- return 1
145+ display_mode = self ._determine_display_mode (parsed_args .mode )
146+ except ValueError as e :
147+ uni_print (str (e ), sys .stderr )
148+ return 1
137149
150+ try :
138151 self ._client = self ._session .create_client (
139152 'ecs' ,
140153 region_name = parsed_globals .region ,
@@ -149,14 +162,52 @@ def _run_main(self, parsed_args, parsed_globals):
149162 self ._client ,
150163 parsed_args .service_arn ,
151164 parsed_args .resource_view ,
165+ display_mode ,
152166 timeout_minutes = parsed_args .timeout ,
153167 use_color = use_color ,
154168 ).exec ()
155169 except MonitoringError as e :
156170 uni_print (f"Error monitoring service: { e } " , sys .stderr )
171+ return 1
172+
173+ def _determine_display_mode (self , requested_mode ):
174+ """Determine and validate the display mode.
175+
176+ Args:
177+ requested_mode: User-requested mode ('interactive', 'text-only', or None)
178+
179+ Returns:
180+ str: Validated display mode ('interactive' or 'text-only')
181+
182+ Raises:
183+ ValueError: If interactive mode is requested without TTY
184+ """
185+ # Determine display mode with auto-detection
186+ if requested_mode is None :
187+ # Auto-detect: interactive if TTY available, else text-only
188+ return 'interactive' if sys .stdout .isatty () else 'text-only'
189+
190+ # Validate requested mode
191+ if requested_mode == 'interactive' :
192+ if not sys .stdout .isatty ():
193+ raise ValueError (
194+ "Error: Interactive mode requires a TTY (terminal). "
195+ "Use --mode text-only for non-interactive environments."
196+ )
197+ return 'interactive'
198+
199+ # text-only mode doesn't require TTY
200+ return requested_mode
157201
158202 def _should_use_color (self , parsed_globals ):
159- """Determine if color output should be used based on global settings."""
203+ """Determine if color output should be used based on global settings.
204+
205+ Args:
206+ parsed_globals: Global CLI configuration
207+
208+ Returns:
209+ bool: True if color should be used
210+ """
160211 if parsed_globals .color == 'on' :
161212 return True
162213 elif parsed_globals .color == 'off' :
@@ -175,104 +226,51 @@ class ECSExpressGatewayServiceWatcher:
175226 Args:
176227 client: ECS client for API calls
177228 service_arn (str): ARN of the service to monitor
178- mode (str): Monitoring mode - 'RESOURCE' or 'DEPLOYMENT'
229+ resource_view (str): Resource view mode - 'RESOURCE' or 'DEPLOYMENT'
230+ display_mode (str): Display mode - 'INTERACTIVE' or 'TEXT-ONLY'
179231 timeout_minutes (int): Maximum monitoring time in minutes (default: 30)
180232 """
181233
182234 def __init__ (
183235 self ,
184236 client ,
185237 service_arn ,
186- mode ,
238+ resource_view ,
239+ display_mode ,
187240 timeout_minutes = 30 ,
188241 display = None ,
189242 use_color = True ,
190243 collector = None ,
191244 ):
192245 self ._client = client
193246 self .service_arn = service_arn
194- self .mode = mode
195- self .timeout_minutes = timeout_minutes
247+ self .display_mode = display_mode
196248 self .start_time = time .time ()
197- self .use_color = use_color
198- self .display = display or Display ()
249+ self .timeout_minutes = timeout_minutes
199250 self .collector = collector or ServiceViewCollector (
200- client , service_arn , mode , use_color
251+ client , service_arn , resource_view , use_color
201252 )
202-
203- @staticmethod
204- def is_monitoring_available ():
205- """Check if monitoring is available (requires TTY)."""
206- return sys .stdout .isatty ()
253+ self .display = display or Display ()
207254
208255 def exec (self ):
209- """Start monitoring the express gateway service with progress display."""
210-
211- def monitor_service (spinner_char ):
212- return self .collector .get_current_view (spinner_char )
213-
214- asyncio .run (self ._execute_with_progress_async (monitor_service , 100 ))
215-
216- async def _execute_with_progress_async (
217- self , execution , progress_refresh_millis , execution_refresh_millis = 5000
218- ):
219- """Execute monitoring loop with animated progress display."""
220- spinner_chars = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
221- spinner_index = 0
222-
223- current_output = "Waiting for initial data"
224-
225- async def update_data ():
226- nonlocal current_output
227- while True :
228- current_time = time .time ()
229- if current_time - self .start_time > self .timeout_minutes * 60 :
230- break
231- try :
232- loop = asyncio .get_event_loop ()
233- new_output = await loop .run_in_executor (
234- None , execution , "{SPINNER}"
235- )
236- current_output = new_output
237- except ClientError as e :
238- if (
239- e .response .get ('Error' , {}).get ('Code' )
240- == 'InvalidParameterException'
241- ):
242- error_message = e .response .get ('Error' , {}).get (
243- 'Message' , ''
244- )
245- if (
246- "Cannot call DescribeServiceRevisions for a service that is INACTIVE"
247- in error_message
248- ):
249- current_output = "Service is inactive"
250- else :
251- raise
252- else :
253- raise
254- await asyncio .sleep (execution_refresh_millis / 1000.0 )
255-
256- async def update_spinner ():
257- nonlocal spinner_index
258- while True :
259- spinner_char = spinner_chars [spinner_index ]
260- display_output = current_output .replace (
261- "{SPINNER}" , spinner_char
262- )
263- status_text = f"Getting updates... { spinner_char } | up/down to scroll, q to quit"
264- self .display .display (display_output , status_text )
265- spinner_index = (spinner_index + 1 ) % len (spinner_chars )
266- await asyncio .sleep (progress_refresh_millis / 1000.0 )
256+ """Execute monitoring using the appropriate display strategy."""
257+ strategy = self ._create_display_strategy ()
258+ strategy .execute (
259+ collector = self .collector ,
260+ start_time = self .start_time ,
261+ timeout_minutes = self .timeout_minutes ,
262+ )
267263
268- # Start both tasks
269- data_task = asyncio .create_task (update_data ())
270- spinner_task = asyncio .create_task (update_spinner ())
264+ def _create_display_strategy (self ):
265+ """Create display strategy based on display mode.
271266
272- try :
273- await self .display .run ()
274- finally :
275- data_task .cancel ()
276- spinner_task .cancel ()
277- final_output = current_output .replace ("{SPINNER}" , "" )
278- uni_print (final_output + "\n Monitoring Complete!\n " )
267+ Returns:
268+ DisplayStrategy: Appropriate strategy for the selected mode
269+ """
270+ if self .display_mode == 'text-only' :
271+ return TextOnlyDisplayStrategy (use_color = self .collector .use_color )
272+ else :
273+ return InteractiveDisplayStrategy (
274+ display = self .display ,
275+ use_color = self .collector .use_color ,
276+ )
0 commit comments