11import argparse
22import asyncio
3- import base64
4- import json
5- import logging
6- import subprocess
7- import sys
8- import webbrowser
9- import zlib
3+ import inspect
104
11- from .graphserver import GraphService
5+ from ezmsg .util .perf .command import setup_perf_cmdline
6+
7+ from .commands import setup_core_cmdline
8+ from .commands .graphviz import handle_graphviz
9+ from .commands .mermaid import handle_mermaid , mermaid_url as mm
10+ from .commands .serve import handle_serve
11+ from .commands .shutdown import handle_shutdown
12+ from .commands .start import handle_start
1213from .netprotocol import (
1314 Address ,
1415 GRAPHSERVER_ADDR_ENV ,
1516 GRAPHSERVER_PORT_DEFAULT ,
1617 PUBLISHER_START_PORT_ENV ,
1718 PUBLISHER_START_PORT_DEFAULT ,
18- close_stream_writer ,
1919)
2020
21- logger = logging .getLogger ("ezmsg" )
22-
2321
24- def cmdline () -> None :
22+ def build_parser () -> argparse . ArgumentParser :
2523 """
26- Command-line interface for ezmsg core server management .
24+ Build the ezmsg core command-line parser .
2725
28- Provides commands for starting, stopping, and managing ezmsg server
29- processes including GraphServer and SHMServer, as well as utilities
30- for graph visualization.
26+ Each command gets its own subparser so command-specific options are not
27+ shared globally across unrelated commands.
3128 """
3229 parser = argparse .ArgumentParser (
3330 "ezmsg.core" ,
@@ -38,63 +35,27 @@ def cmdline() -> None:
3835 Publishers will be assigned available ports starting from { PUBLISHER_START_PORT_DEFAULT } . (Change with ${ PUBLISHER_START_PORT_ENV } )
3936 """ ,
4037 )
38+ subparsers = parser .add_subparsers (dest = "command" , required = True , help = "command for ezmsg" )
4139
42- parser .add_argument (
43- "command" ,
44- help = "command for ezmsg" ,
45- choices = ["serve" , "start" , "shutdown" , "graphviz" , "mermaid" ],
46- )
47-
48- parser .add_argument ("--address" , help = "Address for GraphServer" , default = None )
49-
50- parser .add_argument (
51- "--target" ,
52- help = "Target for mermaid output. Options are 'ink', 'live', and 'play'." ,
53- default = "live" ,
54- )
55-
56- parser .add_argument (
57- "-c" ,
58- "--compact" ,
59- help = """Use compact graph representation. Only used when `cmd` is 'mermaid' or 'graphviz'.
60- Removes the lowest level of detail (typically streams). Can be stacked (eg. '-cc').
61- Warning: this will also prune the graph of proxy topics (nodes that are both sources and targets).
62- """ ,
63- action = "count" ,
64- )
65-
66- parser .add_argument (
67- "-n" ,
68- "--nobrowser" ,
69- help = "Do not automatically open the browser for mermaid output. `--target` value will be ignored." ,
70- action = "store_true" ,
71- )
72-
73- class Args :
74- command : str
75- address : str | None
76- target : str
77- compact : int | None
78- nobrowser : bool
40+ setup_core_cmdline (subparsers )
41+ setup_perf_cmdline (subparsers )
42+ return parser
7943
80- args = parser .parse_args (namespace = Args )
8144
82- graph_address = Address ( "127.0.0.1" , GRAPHSERVER_PORT_DEFAULT )
83- if args . address is not None :
84- graph_address = Address . from_string ( args . address )
45+ def cmdline ( argv : list [ str ] | None = None ) -> None :
46+ """
47+ Command-line interface for ezmsg core server management.
8548
86- loop = asyncio .new_event_loop ()
87- asyncio .set_event_loop (loop )
49+ Provides commands for starting, stopping, and managing ezmsg server
50+ processes including GraphServer and SHMServer, as well as utilities
51+ for graph visualization.
52+ """
53+ parser = build_parser ()
54+ args = parser .parse_args (args = argv )
8855
89- loop .run_until_complete (
90- run_command (
91- args .command ,
92- graph_address ,
93- args .target ,
94- args .compact ,
95- args .nobrowser ,
96- )
97- )
56+ result = args ._handler (args )
57+ if inspect .isawaitable (result ):
58+ asyncio .run (result )
9859
9960
10061async def run_command (
@@ -104,122 +65,20 @@ async def run_command(
10465 compact : int | None = None ,
10566 nobrowser : bool = False ,
10667) -> None :
107- """
108- Run an ezmsg command with the specified parameters.
109-
110- This function handles various ezmsg commands like 'serve', 'start', 'shutdown', etc.
111- and manages the graph and shared memory services.
112-
113- :param cmd: The command to execute ('serve', 'start', 'shutdown', 'graphviz', 'mermaid')
114- :type cmd: str
115- :param graph_address: Address of the graph service
116- :type graph_address: Address
117- :param target: Target for visualization commands (default: 'live')
118- :type target: str
119- :param compact: Compactification level for visualization commands
120- :type compact: int | None
121- :param nobrowser: Whether to suppress browser opening for visualization
122- :type nobrowser: bool
123- """
124- graph_service = GraphService (graph_address )
125-
126- if cmd == "serve" :
127- logger .info (f"GraphServer Address: { graph_address } " )
128-
129- graph_server = graph_service .create_server ()
130-
131- try :
132- logger .info ("Servers running..." )
133- graph_server .join ()
134-
135- except KeyboardInterrupt :
136- logger .info ("Interrupt detected; shutting down servers" )
137-
138- finally :
139- if graph_server is not None :
140- graph_server .stop ()
141-
142- elif cmd == "start" :
143- popen = subprocess .Popen (
144- [sys .executable , "-m" , "ezmsg.core" , "serve" , f"--address={ graph_address } " ]
145- )
146-
147- while True :
148- try :
149- _ , writer = await graph_service .open_connection ()
150- await close_stream_writer (writer )
151- break
152- except ConnectionRefusedError :
153- await asyncio .sleep (0.1 )
154-
155- logger .info (f"Forked ezmsg servers in PID: { popen .pid } " )
156-
157- elif cmd == "shutdown" :
158- try :
159- await graph_service .shutdown ()
160- logger .info (
161- f"Issued shutdown command to GraphServer @ { graph_service .address } "
162- )
163-
164- except ConnectionRefusedError :
165- logger .warning (
166- f"Could not issue shutdown command to GraphServer @ { graph_service .address } ; server not running?"
167- )
168-
169- elif cmd in ["graphviz" , "mermaid" ]:
170- graph_out = await graph_service .get_formatted_graph (
171- fmt = cmd , compact_level = compact
172- )
173- print (graph_out )
174- if cmd == "mermaid" :
175- if not nobrowser :
176- if target == "live" :
177- print (
178- "%% If the graph does not render immediately, try toggling the 'Pan & Zoom' button."
179- )
180- webbrowser .open (mm (graph_out , target = target ))
181-
182-
183- def mm (graph : str , target = "live" ) -> str :
184- """
185- Generate a Mermaid visualization URL for the given graph.
186-
187- :param graph: Graph representation string to visualize.
188- :type graph: str
189- :param target: Target platform ('live' or 'ink').
190- :type target: str
191- :return: URL for graph visualization.
192- :rtype: str
193- """
194- if target != "ink" :
195- jdict = {
196- "code" : graph ,
197- "mermaid" : {"theme" : "default" },
198- "updateDiagram" : True ,
199- "autoSync" : True ,
200- "rough" : False ,
201- }
202- graph = json .dumps (jdict )
203- graphbytes : bytes = graph .encode ("utf8" )
204-
205- if target != "ink" :
206- compress = zlib .compressobj (9 , zlib .DEFLATED , 15 , 8 , zlib .Z_DEFAULT_STRATEGY )
207- graphbytes = compress .compress (graphbytes )
208- graphbytes += compress .flush ()
209-
210- base64_bytes = base64 .b64encode (graphbytes )
211- base64_string = base64_bytes .decode ("ascii" )
212-
213- if target == "ink" :
214- prefix = "https://mermaid.ink/img/"
215- elif target in ["live" , "play" ]:
216- type_str = "pako" # or "base64" if we skip compression above.
217- if target == "live" :
218- prefix = f"https://mermaid.live/edit#{ type_str } :"
219- else : # "play"
220- prefix = f"https://www.mermaidchart.com/play#{ type_str } :"
221- else :
222- raise ValueError (
223- f"Unknown mermaid target '{ target } '. Available options are 'ink', 'live', or 'play'."
224- )
225- return prefix + base64_string
68+ handlers = {
69+ "serve" : handle_serve ,
70+ "start" : handle_start ,
71+ "shutdown" : handle_shutdown ,
72+ "graphviz" : handle_graphviz ,
73+ "mermaid" : handle_mermaid ,
74+ }
75+ if cmd not in handlers :
76+ raise ValueError (f"Unknown ezmsg command '{ cmd } '" )
77+ args = argparse .Namespace (
78+ command = cmd ,
79+ address = str (graph_address ),
80+ target = target ,
81+ compact = compact ,
82+ nobrowser = nobrowser ,
83+ )
84+ await handlers [cmd ](args )
0 commit comments