|
| 1 | +# Copyright (c) ModelScope Contributors. All rights reserved. |
| 2 | +import argparse |
| 3 | +import os |
| 4 | +import sys |
| 5 | +import threading |
| 6 | +import time |
| 7 | +import webbrowser |
| 8 | +from pathlib import Path |
| 9 | + |
| 10 | +from .base import CLICommand |
| 11 | + |
| 12 | + |
| 13 | +def subparser_func(args): |
| 14 | + """ Function which will be called for a specific sub parser. |
| 15 | + """ |
| 16 | + return UICMD(args) |
| 17 | + |
| 18 | + |
| 19 | +class UICMD(CLICommand): |
| 20 | + """The webui command class.""" |
| 21 | + |
| 22 | + name = 'ui' |
| 23 | + |
| 24 | + def __init__(self, args): |
| 25 | + self.args = args |
| 26 | + |
| 27 | + @staticmethod |
| 28 | + def define_args(parsers: argparse.ArgumentParser): |
| 29 | + """Define args for the ui command.""" |
| 30 | + parser: argparse.ArgumentParser = parsers.add_parser(UICMD.name) |
| 31 | + parser.add_argument( |
| 32 | + '--host', |
| 33 | + type=str, |
| 34 | + default='0.0.0.0', |
| 35 | + help='The server host to bind to.') |
| 36 | + parser.add_argument( |
| 37 | + '--port', |
| 38 | + type=int, |
| 39 | + default=7860, |
| 40 | + help='The server port to bind to.') |
| 41 | + parser.add_argument( |
| 42 | + '--reload', |
| 43 | + action='store_true', |
| 44 | + help='Enable auto-reload for development.') |
| 45 | + parser.add_argument( |
| 46 | + '--production', |
| 47 | + action='store_true', |
| 48 | + help='Run in production mode (serve built frontend).') |
| 49 | + parser.add_argument( |
| 50 | + '--no-browser', |
| 51 | + action='store_true', |
| 52 | + help='Do not automatically open browser.') |
| 53 | + parser.set_defaults(func=subparser_func) |
| 54 | + |
| 55 | + def execute(self): |
| 56 | + current_file = Path(__file__).resolve() |
| 57 | + project_root = current_file.parent.parent.parent.parent |
| 58 | + webui_dir = project_root / 'webui' |
| 59 | + |
| 60 | + if not webui_dir.exists(): |
| 61 | + import ms_agent |
| 62 | + ms_agent_path = Path(ms_agent.__file__).parent.parent |
| 63 | + webui_dir = ms_agent_path / 'webui' |
| 64 | + |
| 65 | + if not webui_dir.exists(): |
| 66 | + webui_dir = Path.cwd() / 'webui' |
| 67 | + |
| 68 | + backend_dir = webui_dir / 'backend' |
| 69 | + frontend_dir = webui_dir / 'frontend' |
| 70 | + |
| 71 | + if not webui_dir.exists() or not backend_dir.exists(): |
| 72 | + print('Error: WebUI directory not found.') |
| 73 | + sys.exit(1) |
| 74 | + |
| 75 | + frontend_dist = frontend_dir / 'dist' |
| 76 | + frontend_built = frontend_dist.exists() and (frontend_dist |
| 77 | + / 'index.html').exists() |
| 78 | + |
| 79 | + if self.args.production and not frontend_built: |
| 80 | + print( |
| 81 | + 'Error: Frontend not built. Please run "npm run build" in webui/frontend first.' |
| 82 | + ) |
| 83 | + sys.exit(1) |
| 84 | + |
| 85 | + if not self.args.production and not frontend_built: |
| 86 | + if self._build_frontend(frontend_dir): |
| 87 | + frontend_built = True |
| 88 | + |
| 89 | + browser_host = 'localhost' if self.args.host == '0.0.0.0' else self.args.host |
| 90 | + browser_url = f'http://{browser_host}:{self.args.port}' |
| 91 | + |
| 92 | + backend_str = str(backend_dir) |
| 93 | + if backend_str not in sys.path: |
| 94 | + sys.path.insert(0, backend_str) |
| 95 | + |
| 96 | + original_argv = sys.argv |
| 97 | + original_cwd = os.getcwd() |
| 98 | + try: |
| 99 | + os.chdir(backend_dir) |
| 100 | + from main import main |
| 101 | + |
| 102 | + sys.argv = [ |
| 103 | + 'main.py', |
| 104 | + '--host', |
| 105 | + self.args.host, |
| 106 | + '--port', |
| 107 | + str(self.args.port), |
| 108 | + ] |
| 109 | + if self.args.reload: |
| 110 | + sys.argv.append('--reload') |
| 111 | + |
| 112 | + if not self.args.no_browser and frontend_built: |
| 113 | + |
| 114 | + def open_browser(): |
| 115 | + time.sleep(1.5) |
| 116 | + webbrowser.open(browser_url) |
| 117 | + |
| 118 | + browser_thread = threading.Thread( |
| 119 | + target=open_browser, daemon=True) |
| 120 | + browser_thread.start() |
| 121 | + |
| 122 | + main() |
| 123 | + except KeyboardInterrupt: |
| 124 | + print('\nShutting down...') |
| 125 | + sys.exit(0) |
| 126 | + except Exception as e: |
| 127 | + print(f'Error starting WebUI: {e}') |
| 128 | + import traceback |
| 129 | + traceback.print_exc() |
| 130 | + sys.exit(1) |
| 131 | + finally: |
| 132 | + sys.argv = original_argv |
| 133 | + os.chdir(original_cwd) |
| 134 | + |
| 135 | + def _build_frontend(self, frontend_dir: Path) -> bool: |
| 136 | + import subprocess |
| 137 | + |
| 138 | + try: |
| 139 | + subprocess.run(['npm', '--version'], |
| 140 | + capture_output=True, |
| 141 | + check=True, |
| 142 | + timeout=5) |
| 143 | + except (subprocess.TimeoutExpired, subprocess.CalledProcessError, |
| 144 | + FileNotFoundError): |
| 145 | + return False |
| 146 | + |
| 147 | + node_modules = frontend_dir / 'node_modules' |
| 148 | + if not node_modules.exists(): |
| 149 | + try: |
| 150 | + subprocess.run(['npm', 'install'], |
| 151 | + cwd=frontend_dir, |
| 152 | + check=True, |
| 153 | + timeout=300, |
| 154 | + stdout=subprocess.PIPE, |
| 155 | + stderr=subprocess.PIPE) |
| 156 | + except (subprocess.TimeoutExpired, subprocess.CalledProcessError): |
| 157 | + return False |
| 158 | + |
| 159 | + try: |
| 160 | + subprocess.run(['npm', 'run', 'build'], |
| 161 | + cwd=frontend_dir, |
| 162 | + check=True, |
| 163 | + timeout=300, |
| 164 | + stdout=subprocess.PIPE, |
| 165 | + stderr=subprocess.PIPE) |
| 166 | + return True |
| 167 | + except (subprocess.TimeoutExpired, subprocess.CalledProcessError): |
| 168 | + return False |
0 commit comments