From 9d29b6ce3d85cbd792f0167570397f8e28159bc1 Mon Sep 17 00:00:00 2001 From: tianheil3 Date: Mon, 17 Mar 2025 04:25:18 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=94=A7=20chore(.idea)=EF=BC=9A?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0.gitignore=E6=96=87=E4=BB=B6=E4=BB=A5?= =?UTF-8?q?=E5=BF=BD=E7=95=A5IDE=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 ++++++++ .idea/vcs.xml | 4 ++++ 2 files changed, 12 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..35410cacd --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..d843f340d --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From 9b5ee30ef6ab838e0c37c170c8523a98d33e10c6 Mon Sep 17 00:00:00 2001 From: tianheil3 Date: Mon, 17 Mar 2025 04:26:35 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=94=A7=20chore(=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E9=85=8D=E7=BD=AE)=EF=BC=9A=E6=B7=BB=E5=8A=A0IntelliJ=20IDEA?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=20?= =?UTF-8?q?=F0=9F=8C=90=20feat(=E7=8A=B6=E6=80=81=E9=A1=B5=E9=9D=A2)?= =?UTF-8?q?=EF=BC=9A=E6=9B=B4=E6=96=B0Supervisor=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E7=9A=84=E5=9B=BD=E9=99=85=E5=8C=96=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20=E2=9C=A8=20feat(web.py)=EF=BC=9A=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E7=BB=84=E5=90=AF=E5=8A=A8=E5=92=8C=E5=81=9C?= =?UTF-8?q?=E6=AD=A2=E5=8A=9F=E8=83=BD=20=E2=99=BB=EF=B8=8F=20refactor(web?= =?UTF-8?q?.py)=EF=BC=9A=E9=87=8D=E6=9E=84=E8=BF=9B=E7=A8=8B=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E7=9A=84=E6=94=B6=E9=9B=86=E5=92=8C=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E9=80=BB=E8=BE=91=20=F0=9F=92=84=20style(=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2)=EF=BC=9A=E6=9B=B4=E6=96=B0Supervisor?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E9=A1=B5=E9=9D=A2=E7=9A=84=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E4=BB=A5=E5=AE=9E=E7=8E=B0=E7=8E=B0=E4=BB=A3=E5=8C=96=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/inspectionProfiles/Project_Default.xml | 24 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/supervisor.iml | 14 + .idea/vcs.xml | 4 +- supervisor/ui/status.html | 61 ++- supervisor/ui/stylesheets/supervisor.css | 442 ++++++++++++------ supervisor/web.py | 285 ++++++++--- 9 files changed, 624 insertions(+), 224 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/supervisor.iml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..c4379ba70 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 000000000..105ce2da2 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..6a4ddcc51 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..e623d0283 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/supervisor.iml b/.idea/supervisor.iml new file mode 100644 index 000000000..f0e2c75c0 --- /dev/null +++ b/.idea/supervisor.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index d843f340d..35eb1ddfb 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,4 +1,6 @@ - + + + \ No newline at end of file diff --git a/supervisor/ui/status.html b/supervisor/ui/status.html index 166e3e32f..8347be08c 100644 --- a/supervisor/ui/status.html +++ b/supervisor/ui/status.html @@ -3,15 +3,19 @@ - Supervisor Status + + + Supervisor 状态监控 +
@@ -19,30 +23,35 @@
- +
+ +
+ + +
- - - - + + + + - - - + + + @@ -62,8 +71,30 @@ Supervisor# + + diff --git a/supervisor/ui/stylesheets/supervisor.css b/supervisor/ui/stylesheets/supervisor.css index 12aba9288..9c67455b8 100644 --- a/supervisor/ui/stylesheets/supervisor.css +++ b/supervisor/ui/stylesheets/supervisor.css @@ -1,215 +1,355 @@ -/* =ORDER - 1. display - 2. float and position - 3. width and height - 4. Specific element properties - 5. margin - 6. border - 7. padding - 8. background - 9. color -10. font related properties ------------------------------------------------ */ - -/* =MAIN ------------------------------------------------ */ -body, td, input, select, textarea, a { - font: 12px/1.5em arial, helvetica, verdana, sans-serif; - color: #333; -} -html, body, form, fieldset, h1, h2, h3, h4, h5, h6, -p, pre, blockquote, ul, ol, dl, address { +/* Supervisor Status Page Modern Style */ +/* Reset and Base Styles */ +* { + box-sizing: border-box; margin: 0; padding: 0; } -form label { - cursor: pointer; -} -fieldset { - border: none; -} -img, table { - border-width: 0; -} -/* =COLORS ------------------------------------------------ */ -body { - background-color: #FFFFF3; - color: #333; -} -a:link, -a:visited { +html, body { + height: 100%; + font-family: 'Segoe UI', Arial, Helvetica, sans-serif; + font-size: 16px; + line-height: 1.5; color: #333; -} -a:hover { - color: #000; + background-color: #f5f5f5; } -/* =FLOATS ------------------------------------------------ */ -.left { - float: left; -} -.right { - text-align: right; - float: right; -} -/* clear float */ -.clr:after { - content: "."; - display: block; - height: 0; - clear: both; - visibility: hidden; +a { + color: #2c7be5; + text-decoration: none; + transition: color 0.2s, background-color 0.2s; } -.clr {display: inline-block;} -/* Hides from IE-mac \*/ -* html .clr {height: 1%;} -.clr {display: block;} -/* End hide from IE-mac */ -/* =LAYOUT ------------------------------------------------ */ -html, body { - height: 100%; +a:hover { + color: #1a56a8; } + +/* Layout */ #wrapper { min-height: 100%; - height: auto !important; - height: 100%; - width: 850px; - margin: 0 auto -31px; + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 20px; + background-color: #fff; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); +} + +#header { + margin-bottom: 30px; + padding: 20px 0; + border-bottom: 2px solid #eaeaea; + display: flex; + align-items: center; } -#footer, -.push { - height: 30px; + +#header img { + max-height: 50px; } .hidden { display: none; } -/* =STATUS ------------------------------------------------ */ -#header { - margin-bottom: 13px; - padding: 10px 0 13px 0; - background: url("../images/rule.gif") left bottom repeat-x; -} -.status_msg { - padding: 5px 10px; - border: 1px solid #919191; - background-color: #FBFBFB; - color: #000000; +/* Status Message */ +#statusmessage { + padding: 12px 15px; + margin-bottom: 20px; + background-color: #e8f4fd; + border-left: 4px solid #2c7be5; + border-radius: 4px; + color: #1a56a8; } +/* Action Buttons */ #buttons { - margin: 13px 0; -} -#buttons li { - float: left; - display: block; - margin: 0 7px 0 0; -} -#buttons a { - float: left; - display: block; - padding: 1px 0 0 0; -} -#buttons a, #buttons a:link { - text-decoration: none; + display: flex; + gap: 10px; + margin: 20px 0; + list-style: none; } .action-button { - border: 1px solid #919191; - text-transform: uppercase; - padding: 0 5px; + display: inline-block; +} + +.action-button a { + display: inline-block; + padding: 8px 16px; + background-color: #2c7be5; + color: white; border-radius: 4px; - color: #50504d; - font-size: 12px; - background: #fbfbfb; - font-weight: 600; + font-weight: 500; + text-transform: uppercase; + font-size: 14px; + letter-spacing: 0.5px; + border: none; + transition: all 0.2s; } -.action-button:hover { - border: 1px solid #88b0f2; - background: #ffffff; +.action-button a:hover { + background-color: #1a56a8; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } +/* Tables */ table { width: 100%; - border: 1px solid #919191; + border-collapse: collapse; + margin: 20px 0; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } + th { - background-color: #919191; - color: #fff; + background-color: #2c7be5; + color: white; text-align: left; + padding: 12px 15px; + font-weight: 500; } + th.state { + width: 80px; text-align: center; - width: 44px; } + th.desc { - width: 200px; + width: 30%; } + th.name { - width: 200px; + width: 25%; } + th.action { + width: 20%; } -td, th { - padding: 4px 8px; - border-bottom: 1px solid #fff; -} -tr td { - background-color: #FBFBFB; + +td { + padding: 12px 15px; + border-bottom: 1px solid #eaeaea; } -tr.shade td { - background-color: #F0F0F0; + +tr:nth-child(even) td { + background-color: #f9f9f9; } -.action ul { - list-style: none; - display: inline; + +tr:hover td { + background-color: #f0f7ff; } -.action li { - margin-right: 10px; - display: inline; + +tr:last-child td { + border-bottom: none; } -/* status message */ +/* Status Indicators */ .status span { - display: block; - width: 60px; - height: 16px; - border: 1px solid #fff; + display: inline-block; + padding: 4px 8px; + border-radius: 50px; + font-size: 12px; + font-weight: 600; text-align: center; - font-size: 95%; - line-height: 1.4em; + width: auto; + min-width: 80px; } + .statusnominal { - background-image: url("../images/state0.gif"); + background-color: #0abb87; + color: white; } + .statusrunning { - background-image: url("../images/state2.gif"); + background-color: #ffb822; + color: #333; } + .statuserror { - background-image: url("../images/state3.gif"); + background-color: #fd397a; + color: white; +} + +/* Action Links */ +.action ul { + list-style: none; + display: flex; + gap: 8px; +} + +.action li { + display: inline-block; +} + +.action a { + display: inline-block; + padding: 4px 10px; + background-color: #f5f5f5; + border: 1px solid #ddd; + border-radius: 4px; + color: #333; + font-size: 13px; + transition: all 0.2s; +} + +.action a:hover { + background-color: #2c7be5; + color: white; + border-color: #2c7be5; } +/* Footer */ #footer { - width: 760px; - margin: 0 auto; - padding: 0 10px; - line-height: 30px; - border: 1px solid #C8C8C2; - border-bottom-width: 0; - background-color: #FBFBFB; + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 40px; + padding: 15px 0; + border-top: 1px solid #eaeaea; + color: #777; + font-size: 14px; +} + +/* Utilities */ +.left { + float: left; +} + +.right { + float: right; +} + +.clr:after { + content: ""; + display: table; + clear: both; +} + +/* 分组样式 */ +.process-group { + margin-bottom: 20px; + border-radius: 8px; overflow: hidden; - opacity: 0.7; - color: #000; - font-size: 95%; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + background-color: #fff; +} + +.group-header { + background-color: #f0f7ff; + padding: 12px 15px; + display: flex; + align-items: center; + cursor: pointer; + border-bottom: 1px solid #ddd; + transition: background-color 0.2s; +} + +.group-header:hover { + background-color: #e0f0ff; +} + +.group-name { + font-weight: 600; + font-size: 18px; + color: #2c7be5; + margin: 0 10px; + flex-grow: 1; +} + +.group-summary { + display: flex; + gap: 10px; +} + +.group-status { + font-size: 14px; + padding: 4px 10px; + border-radius: 20px; + background-color: #f5f5f5; +} + +.group-status.running { + background-color: #e1f8f0; + color: #0abb87; +} + +.group-status.error { + background-color: #ffe8ef; + color: #fd397a; +} + +.group-status.partial { + background-color: #fff4de; + color: #ffb822; +} + +.group-icon { + margin-right: 5px; + transition: transform 0.2s; +} + +.group-actions { + display: flex; + gap: 8px; +} + +.group-actions a { + display: flex; + align-items: center; + justify-content: center; + padding: 4px 10px; + background-color: #f5f5f5; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 13px; + transition: all 0.2s; +} + +.group-actions a:hover { + background-color: #2c7be5; + color: white; + border-color: #2c7be5; } -#footer a { - font-size: inherit; + +.group-content { + width: 100%; +} + +/* Responsive Design */ +@media (max-width: 768px) { + #wrapper { + padding: 15px; + } + + #buttons { + flex-direction: column; + } + + .action-button { + width: 100%; + } + + .action-button a { + display: block; + text-align: center; + } + + table { + display: block; + overflow-x: auto; + } + + .group-header { + flex-direction: column; + align-items: flex-start; + } + + .group-summary { + width: 100%; + margin-top: 8px; + justify-content: space-between; + } } diff --git a/supervisor/web.py b/supervisor/web.py index 926e8d43f..e6d8a97d5 100644 --- a/supervisor/web.py +++ b/supervisor/web.py @@ -327,6 +327,40 @@ def restartall(): return 'All restarted at %s' % time.ctime() restartall.delay = 0.05 return restartall + + elif action == 'startProcessGroup': + try: + callback = rpcinterface.supervisor.startProcessGroup(namespec) + except RPCError as e: + msg = 'unexpected rpc fault [%d] %s' % (e.code, e.text) + def startgrperr(): + return msg + startgrperr.delay = 0.05 + return startgrperr + + def startgroup(): + if callback() is NOT_DONE_YET: + return NOT_DONE_YET + return '组 %s 的所有进程已启动' % namespec + startgroup.delay = 0.05 + return startgroup + + elif action == 'stopProcessGroup': + try: + callback = rpcinterface.supervisor.stopProcessGroup(namespec) + except RPCError as e: + msg = 'unexpected rpc fault [%d] %s' % (e.code, e.text) + def stopgrperr(): + return msg + stopgrperr.delay = 0.05 + return stopgrperr + + def stopgroup(): + if callback() is NOT_DONE_YET: + return NOT_DONE_YET + return '组 %s 的所有进程已停止' % namespec + stopgroup.delay = 0.05 + return stopgroup elif namespec: def wrong(): @@ -488,27 +522,26 @@ def render(self): SupervisorNamespaceRPCInterface(supervisord))] ) - processnames = [] - for group in supervisord.process_groups.values(): - for gprocname in group.processes.keys(): - processnames.append((group.config.name, gprocname)) - - processnames.sort() - - data = [] - for groupname, processname in processnames: - actions = self.actions_for_process( - supervisord.process_groups[groupname].processes[processname]) - sent_name = make_namespec(groupname, processname) - info = rpcinterface.supervisor.getProcessInfo(sent_name) - data.append({ - 'status':info['statename'], - 'name':processname, - 'group':groupname, - 'actions':actions, - 'state':info['state'], - 'description':info['description'], + # 按组收集进程 + groups = {} + for groupname, group in supervisord.process_groups.items(): + groups[groupname] = [] + for process_name in group.processes.keys(): + sent_name = make_namespec(groupname, process_name) + info = rpcinterface.supervisor.getProcessInfo(sent_name) + process = group.processes[process_name] + actions = self.actions_for_process(process) + groups[groupname].append({ + 'status': info['statename'], + 'name': process_name, + 'group': groupname, + 'actions': actions, + 'state': info['state'], + 'description': info['description'], }) + + # 按照组名称排序 + sorted_groups = sorted(groups.items()) root = self.clone() @@ -517,44 +550,182 @@ def render(self): statusarea.attrib['class'] = 'status_msg' statusarea.content(message) - if data: - iterator = root.findmeld('tr').repeat(data) - shaded_tr = False - - for tr_element, item in iterator: - status_text = tr_element.findmeld('status_text') - status_text.content(item['status'].lower()) - status_text.attrib['class'] = self.css_class_for_state( - item['state']) - - info_text = tr_element.findmeld('info_text') - info_text.content(item['description']) - - anchor = tr_element.findmeld('name_anchor') - processname = make_namespec(item['group'], item['name']) - anchor.attributes(href='tail.html?processname=%s' % - urllib.quote(processname)) - anchor.content(processname) - - actions = item['actions'] - actionitem_td = tr_element.findmeld('actionitem_td') - - for li_element, actionitem in actionitem_td.repeat(actions): - anchor = li_element.findmeld('actionitem_anchor') - if actionitem is None: - anchor.attrib['class'] = 'hidden' - else: - anchor.attributes(href=actionitem['href'], - name=actionitem['name']) - anchor.content(actionitem['name']) - if actionitem['target']: - anchor.attributes(target=actionitem['target']) - if shaded_tr: - tr_element.attrib['class'] = 'shade' - shaded_tr = not shaded_tr + # 处理分组显示 + if sorted_groups: + process_groups_div = root.findmeld('process-groups') + + for groupname, processes in sorted_groups: + # 计算组的整体状态 + running_count = sum(1 for p in processes if p['state'] == ProcessStates.RUNNING) + error_count = sum(1 for p in processes if p['state'] in (ProcessStates.FATAL, ProcessStates.BACKOFF)) + total_count = len(processes) + + # 创建组容器 + group_div = templating.Element('div') + group_div.attrib['class'] = 'process-group' + + # 创建组标题栏 + header_div = templating.Element('div') + header_div.attrib['class'] = 'group-header' + + # 添加折叠图标 + icon = templating.Element('i') + icon.attrib['class'] = 'fas fa-angle-down group-icon' + header_div.append(icon) + + # 添加组名称 + group_name = templating.Element('div') + group_name.attrib['class'] = 'group-name' + group_name.content(groupname) + header_div.append(group_name) + + # 添加组状态摘要 + summary_div = templating.Element('div') + summary_div.attrib['class'] = 'group-summary' + + # 状态标签 + status_span = templating.Element('div') + if error_count > 0: + status_class = 'group-status error' + status_text = '%d/%d 错误' % (error_count, total_count) + elif running_count == total_count: + status_class = 'group-status running' + status_text = '全部运行 (%d)' % total_count + elif running_count == 0: + status_class = 'group-status' + status_text = '全部停止 (%d)' % total_count + else: + status_class = 'group-status partial' + status_text = '%d/%d 运行中' % (running_count, total_count) + + status_span.attrib['class'] = status_class + status_span.content(status_text) + summary_div.append(status_span) + + # 组操作按钮 + actions_div = templating.Element('div') + actions_div.attrib['class'] = 'group-actions' + + # 启动全部按钮 + start_a = templating.Element('a') + start_a.attrib['href'] = 'index.html?action=startProcessGroup&processname=%s' % urllib.quote(groupname) + start_a.content('启动全部') + actions_div.append(start_a) + + # 停止全部按钮 + stop_a = templating.Element('a') + stop_a.attrib['href'] = 'index.html?action=stopProcessGroup&processname=%s' % urllib.quote(groupname) + stop_a.content('停止全部') + actions_div.append(stop_a) + + summary_div.append(actions_div) + header_div.append(summary_div) + + group_div.append(header_div) + + # 创建组内容区域 + content_div = templating.Element('div') + content_div.attrib['class'] = 'group-content' + + # 创建进程表格 + table = templating.Element('table') + + # 表头 + thead = templating.Element('thead') + tr = templating.Element('tr') + + th_state = templating.Element('th') + th_state.attrib['class'] = 'state' + th_state.content('状态') + tr.append(th_state) + + th_desc = templating.Element('th') + th_desc.attrib['class'] = 'desc' + th_desc.content('描述') + tr.append(th_desc) + + th_name = templating.Element('th') + th_name.attrib['class'] = 'name' + th_name.content('名称') + tr.append(th_name) + + th_action = templating.Element('th') + th_action.attrib['class'] = 'action' + th_action.content('操作') + tr.append(th_action) + + thead.append(tr) + table.append(thead) + + # 表内容 + tbody = templating.Element('tbody') + + for i, process in enumerate(processes): + tr = templating.Element('tr') + if i % 2: + tr.attrib['class'] = 'shade' + + # 状态列 + td_status = templating.Element('td') + td_status.attrib['class'] = 'status' + + status_span = templating.Element('span') + status_span.attrib['class'] = self.css_class_for_state(process['state']) + status_span.content(process['status'].lower()) + td_status.append(status_span) + tr.append(td_status) + + # 描述列 + td_info = templating.Element('td') + info_span = templating.Element('span') + info_span.content(process['description']) + td_info.append(info_span) + tr.append(td_info) + + # 名称列 + td_name = templating.Element('td') + name_a = templating.Element('a') + processname = make_namespec(process['group'], process['name']) + name_a.attrib['href'] = 'tail.html?processname=%s' % urllib.quote(processname) + name_a.attrib['target'] = '_blank' + name_a.content(processname) + td_name.append(name_a) + tr.append(td_name) + + # 操作列 + td_action = templating.Element('td') + td_action.attrib['class'] = 'action' + ul = templating.Element('ul') + + for action in process['actions']: + li = templating.Element('li') + if action is None: + li.attrib['class'] = 'hidden' + a = templating.Element('a') + a.attrib['href'] = '#' + li.append(a) + else: + a = templating.Element('a') + a.attrib['href'] = action['href'] + a.content(action['name']) + if action['target']: + a.attrib['target'] = action['target'] + li.append(a) + ul.append(li) + + td_action.append(ul) + tr.append(td_action) + + tbody.append(tr) + + table.append(tbody) + content_div.append(table) + group_div.append(content_div) + + process_groups_div.append(group_div) else: - table = root.findmeld('statustable') - table.replace('No programs to manage') + process_groups_div = root.findmeld('process-groups') + process_groups_div.content('没有程序可以管理') root.findmeld('supervisor_version').content(VERSION) copyright_year = str(datetime.date.today().year) From e6aae488ecc130211181c0b28d032585b153f5b4 Mon Sep 17 00:00:00 2001 From: feiwentao <505735413@qq.com> Date: Mon, 17 Mar 2025 07:24:55 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=8A=9F=E8=83=BD:=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BB=84=E6=93=8D=E4=BD=9C=E6=8C=89=E9=92=AE?= =?UTF-8?q?,=E7=BE=8E=E5=8C=96=E6=97=A5=E5=BF=97=E7=95=8C=E9=9D=A2,?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- INSTALL_CN.md | 73 +++ PACKAGING_GUIDE_CN.md | 83 +++ README_CN.md | 40 ++ RELEASE_NOTES_CN.md | 48 ++ SUPERVISOR_USAGE_CN.md | 163 +++++ build_package.sh | 19 + scripts/counter.py | 9 + scripts/cpu_monitor.py | 10 + scripts/memory_monitor.py | 10 + scripts/time_printer.py | 9 + supervisor/http.py | 215 ++++++- supervisor/ui/status.html | 120 ++-- supervisor/ui/stylesheets/supervisor.css | 351 ++++++----- supervisor/ui/tail.html | 255 +++++++- supervisor/version.txt | 2 +- supervisor/web.py | 738 +++++++++++++++-------- supervisor_cmd.sh | 22 + 17 files changed, 1663 insertions(+), 504 deletions(-) create mode 100644 INSTALL_CN.md create mode 100644 PACKAGING_GUIDE_CN.md create mode 100644 README_CN.md create mode 100644 RELEASE_NOTES_CN.md create mode 100644 SUPERVISOR_USAGE_CN.md create mode 100755 build_package.sh create mode 100644 scripts/counter.py create mode 100644 scripts/cpu_monitor.py create mode 100644 scripts/memory_monitor.py create mode 100644 scripts/time_printer.py create mode 100755 supervisor_cmd.sh diff --git a/INSTALL_CN.md b/INSTALL_CN.md new file mode 100644 index 000000000..3f66bcfe4 --- /dev/null +++ b/INSTALL_CN.md @@ -0,0 +1,73 @@ +# Supervisor 安装指南 + +## 安装方法 + +### 1. 使用 pip 安装(推荐) + +```bash +pip install supervisor +``` + +### 2. 从源码安装 + +```bash +# 克隆仓库 +git clone https://github.com/你的用户名/supervisor.git +cd supervisor + +# 安装 +pip install -e . +``` + +## 基本配置 + +1. 生成默认配置文件: + +```bash +echo_supervisord_conf > supervisord.conf +``` + +2. 编辑配置文件,根据需要修改: + +```bash +# 设置HTTP服务器端口 +[inet_http_server] +port=0.0.0.0:9001 +username=admin +password=123456 + +# 添加您的程序 +[program:yourapp] +command=/path/to/your/program +``` + +## 启动 Supervisor + +```bash +# 启动 Supervisor +supervisord -c supervisord.conf + +# 使用 supervisorctl 控制进程 +supervisorctl status +supervisorctl start all +supervisorctl stop all +supervisorctl restart all +``` + +## Web 界面 + +安装成功后,访问 http://localhost:9001 即可打开 Supervisor 的 Web 界面。 + +用户名:admin +密码:123456 (根据您的配置) + +## 组操作 + +本版本支持对进程组进行操作: + +- 重启组:点击组名旁边的"重启组"按钮 +- 停止组:点击组名旁边的"停止组"按钮 + +## 日志查看 + +点击进程名后的"查看日志"按钮,可以查看该进程的日志输出。 \ No newline at end of file diff --git a/PACKAGING_GUIDE_CN.md b/PACKAGING_GUIDE_CN.md new file mode 100644 index 000000000..d5061697c --- /dev/null +++ b/PACKAGING_GUIDE_CN.md @@ -0,0 +1,83 @@ +# Supervisor 打包与发布指南 + +## 打包过程 + +### 1. 环境准备 + +确保安装了必要的打包工具: + +```bash +pip install setuptools wheel twine +``` + +### 2. 修改版本号 + +1. 编辑 `supervisor/version.txt` 文件,设置正确的版本号 +2. 修改配置文件中的端口设置(如需要) + +### 3. 构建包 + +运行打包脚本生成分发包: + +```bash +./build_package.sh +``` + +打包完成后,会在 `dist/` 目录下生成以下文件: +- `supervisor-4.3.0.tar.gz` - 源码分发包 +- `supervisor-4.3.0-py2.py3-none-any.whl` - Python wheel 包 + +### 4. 测试安装包 + +在测试环境中测试生成的包: + +```bash +# 从源码包安装 +pip install dist/supervisor-4.3.0.tar.gz + +# 或从 wheel 包安装 +pip install dist/supervisor-4.3.0-py2.py3-none-any.whl +``` + +### 5. 上传到 PyPI(可选) + +如果你有 PyPI 帐号并想公开发布,可以使用: + +```bash +python -m twine upload dist/* +``` + +## 文档结构 + +发布版本应包含以下文档: + +- `README_CN.md` - 中文项目说明 +- `INSTALL_CN.md` - 中文安装指南 +- `RELEASE_NOTES_CN.md` - 中文发布说明 +- `PACKAGING_GUIDE_CN.md` - 中文打包指南(本文档) + +## 本地部署 + +如果只需要本地部署,可以将打包好的分发包复制到目标机器并安装: + +```bash +# 在目标机器上 +pip install supervisor-4.3.0.tar.gz +``` + +## 打包脚本说明 + +`build_package.sh` 脚本执行以下操作: + +1. 清理之前的构建文件 +2. 构建源码分发包和 wheel 包 +3. 显示生成的包文件列表 +4. 提供上传到 PyPI 的命令提示 + +## 故障排除 + +如果在打包过程中遇到问题: + +1. 检查 Python 版本是否兼容 +2. 确认所有依赖已正确安装 +3. 检查文件权限和路径是否正确 \ No newline at end of file diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 000000000..ff0eab824 --- /dev/null +++ b/README_CN.md @@ -0,0 +1,40 @@ +# Supervisor - 进程控制系统 + +Supervisor 是一个客户端/服务器系统,允许用户在类 UNIX 操作系统上控制多个进程。 + +## 特性 + +- **进程管理**:启动、停止、重启进程 +- **自动重启**:进程崩溃时自动重启 +- **状态监控**:监控进程状态 +- **日志管理**:收集和管理进程输出 +- **Web界面**:通过Web界面管理进程 +- **组操作**:对进程组进行批量操作 + +## 增强功能 + +本版本在原始 Supervisor 基础上增加了以下功能: + +1. **组操作按钮**:在Web界面中直接操作进程组 + - 重启组:一键重启整个组中的所有进程 + - 停止组:一键停止整个组中的所有进程 + +2. **日志查看增强**:美化了日志查看页面 + - 语法高亮 + - 行号显示 + - 自动滚动 + - 搜索功能 + - 复制功能 + +## 安装 + +详细安装说明请参考 [INSTALL_CN.md](INSTALL_CN.md)。 + +## 许可证 + +Supervisor 是根据类 BSD 许可证发布的,详见 [LICENSE.txt](LICENSES.txt)。 + +## 更多信息 + +- 官方文档:http://supervisord.org/ +- 源代码:https://github.com/Supervisor/supervisor \ No newline at end of file diff --git a/RELEASE_NOTES_CN.md b/RELEASE_NOTES_CN.md new file mode 100644 index 000000000..0c96cb2f1 --- /dev/null +++ b/RELEASE_NOTES_CN.md @@ -0,0 +1,48 @@ +# Supervisor 4.3.0 发布说明 + +## 版本亮点 + +Supervisor 4.3.0 版本是一个重要的增强版本,在标准 Supervisor 功能基础上增加了更多实用功能。 + +### 主要新特性 + +1. **进程组操作按钮** + - 在 Web 界面中直接添加了"重启组"和"停止组"按钮 + - 可以一键操作整个进程组,提高管理效率 + - 优化了按钮布局和交互体验 + +2. **增强的日志查看界面** + - 全新设计的日志查看页面 + - 添加了语法高亮显示 + - 支持行号显示 + - 提供自动滚动功能 + - 增加日志搜索功能 + - 一键复制日志内容 + +3. **用户界面改进** + - 优化了组和进程的显示层次结构 + - 改进了按钮布局和样式 + - 增强了整体视觉体验 + +### 错误修复 + +- 修复了组操作功能中的异步回调处理问题 +- 解决了多个界面显示和布局问题 +- 修复了 RPC 接口调用相关的错误 + +## 安装指南 + +详细安装说明请参阅 [INSTALL_CN.md](INSTALL_CN.md)。 + +## 升级说明 + +如果您正在从之前的版本升级,只需按照安装指南重新安装即可。配置文件格式保持兼容。 + +## 兼容性 + +- 支持 Python 2.7 及 Python 3.4+ +- 兼容所有主流 UNIX/Linux 系统及 macOS + +## 致谢 + +特别感谢所有为此版本贡献代码、测试和反馈的开发者。 \ No newline at end of file diff --git a/SUPERVISOR_USAGE_CN.md b/SUPERVISOR_USAGE_CN.md new file mode 100644 index 000000000..5e616258f --- /dev/null +++ b/SUPERVISOR_USAGE_CN.md @@ -0,0 +1,163 @@ +# Supervisor 使用说明 + +## 配置文件位置 + +当前项目使用的配置文件位于: +``` +/Users/feiwentao/tianhei_projects/python_projests/supervisor/supervisord.conf +``` + +## 常用命令 + +为了更方便地操作 Supervisor,我们创建了一个命令辅助脚本 `supervisor_cmd.sh`。使用此脚本可以避免认证错误和路径问题。 + +### 基本命令 + +```bash +# 查看所有进程状态 +./supervisor_cmd.sh status + +# 启动特定进程 +./supervisor_cmd.sh start <进程名> + +# 停止特定进程 +./supervisor_cmd.sh stop <进程名> + +# 重启特定进程 +./supervisor_cmd.sh restart <进程名> + +# 关闭 Supervisor +./supervisor_cmd.sh shutdown +``` + +### 组操作命令 + +```bash +# 启动整个组 +./supervisor_cmd.sh start <组名>:* + +# 停止整个组 +./supervisor_cmd.sh stop <组名>:* + +# 重启整个组 +./supervisor_cmd.sh restart <组名>:* +``` + +### 配置管理命令 + +```bash +# 重新读取配置文件 +./supervisor_cmd.sh reread + +# 更新配置(应用新的配置) +./supervisor_cmd.sh update + +# 显示所有命令帮助 +./supervisor_cmd.sh help +``` + +## 修改配置文件 + +使用文本编辑器打开配置文件: + +```bash +vim supervisord.conf +# 或者 +open -a TextEdit supervisord.conf +``` + +### 配置文件主要部分 + +1. **HTTP 服务器设置**: + ```ini + [inet_http_server] + port=0.0.0.0:9001 # 端口设置 + username=admin # 登录用户名 + password=123456 # 登录密码 + ``` + +2. **进程配置**: + ```ini + [program:程序名称] + command=要执行的命令 # 必填,程序启动命令 + directory=工作目录 # 程序的工作目录 + autostart=true # 是否自动启动 + autorestart=true # 是否自动重启 + redirect_stderr=true # 是否重定向错误输出 + stdout_logfile=日志路径 # 日志文件位置 + ``` + +3. **进程组配置**: + ```ini + [group:组名] + programs=程序1,程序2 # 组内的程序列表 + priority=999 # 启动优先级 + ``` + +### 添加新程序 + +1. 在配置文件末尾添加新的程序段: + ```ini + [program:新程序名] + command=python /path/to/your/script.py + directory=/path/to/working/dir + autostart=true + autorestart=true + redirect_stderr=true + stdout_logfile=./logs/新程序名.log + environment=变量1="值1",变量2="值2" + ``` + +2. 如果要将程序添加到组中,先添加程序配置,然后添加或修改组配置: + ```ini + [group:组名] + programs=现有程序1,现有程序2,新程序名 + priority=999 + ``` + +### 修改现有程序配置 + +找到对应的 `[program:xxx]` 部分,修改相应的参数。常用参数包括: + +- **command**: 启动命令 +- **directory**: 工作目录 +- **autostart**: 是否自动启动(true/false) +- **autorestart**: 自动重启(true/false/unexpected) +- **redirect_stderr**: 是否将错误输出重定向到标准输出(true/false) +- **stdout_logfile**: 标准输出日志文件路径 +- **environment**: 环境变量设置 + +## Web 界面 + +Supervisor 提供了一个 Web 界面来管理您的进程: + +- 地址:http://localhost:9001 +- 用户名:admin +- 密码:123456 (根据配置文件设置) + +通过 Web 界面,您可以: +- 查看所有进程的状态 +- 启动/停止/重启单个进程 +- 启动/停止/重启整个进程组 +- 查看进程日志 + +## 常见问题解决 + +### 1. 认证错误 +问题:`Server requires authentication: error: 401 Unauthorized` +解决:使用 `./supervisor_cmd.sh` 脚本执行命令,或指定配置文件路径: +```bash +supervisorctl -c $(pwd)/supervisord.conf -u admin -p 123456 status +``` + +### 2. 无法连接到 Supervisor +问题:`unix:///tmp/supervisor.sock no such file` +解决:确保 Supervisor 已启动,并且 socket 文件路径正确。 + +### 3. 进程启动后立即退出 +问题:进程状态显示为 `FATAL` 或 `BACKOFF` +解决: +- 检查命令是否正确 +- 查看进程日志了解详细错误信息 +- 确保工作目录正确 +- 检查环境变量设置 \ No newline at end of file diff --git a/build_package.sh b/build_package.sh new file mode 100755 index 000000000..66a83606d --- /dev/null +++ b/build_package.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# 打包和发布Supervisor +set -e + +# 清理之前的构建 +echo "清理之前的构建..." +rm -rf build/ dist/ *.egg-info/ + +# 构建源码包和wheel包 +echo "构建源码包和wheel包..." +python setup.py sdist bdist_wheel + +echo "构建完成!" +echo "生成的包在dist/目录下" +ls -la dist/ + +echo "" +echo "如需上传到PyPI,请运行:" +echo "python -m twine upload dist/*" \ No newline at end of file diff --git a/scripts/counter.py b/scripts/counter.py new file mode 100644 index 000000000..356bbd1b5 --- /dev/null +++ b/scripts/counter.py @@ -0,0 +1,9 @@ +import time + +print("计数器脚本已启动") + +count = 0 +while True: + count += 1 + print(f"当前计数: {count}") + time.sleep(2) \ No newline at end of file diff --git a/scripts/cpu_monitor.py b/scripts/cpu_monitor.py new file mode 100644 index 000000000..28d5938f9 --- /dev/null +++ b/scripts/cpu_monitor.py @@ -0,0 +1,10 @@ +import time +import psutil + +print("CPU监控脚本已启动") + +while True: + print(f"CPU使用率: {psutil.cpu_percent(interval=1)}%") + for i, percentage in enumerate(psutil.cpu_percent(interval=1, percpu=True)): + print(f"CPU {i}: {percentage}%") + time.sleep(8) \ No newline at end of file diff --git a/scripts/memory_monitor.py b/scripts/memory_monitor.py new file mode 100644 index 000000000..c283c07f7 --- /dev/null +++ b/scripts/memory_monitor.py @@ -0,0 +1,10 @@ +import time +import psutil + +print("内存监控脚本已启动") + +while True: + memory = psutil.virtual_memory() + print(f"内存使用率: {memory.percent}%") + print(f"可用内存: {memory.available / (1024 * 1024):.2f} MB") + time.sleep(10) \ No newline at end of file diff --git a/scripts/time_printer.py b/scripts/time_printer.py new file mode 100644 index 000000000..c3fc2e15f --- /dev/null +++ b/scripts/time_printer.py @@ -0,0 +1,9 @@ +import time +import datetime + +print("时间打印脚本已启动") + +while True: + now = datetime.datetime.now() + print(f"当前时间: {now.strftime('%Y-%m-%d %H:%M:%S')}") + time.sleep(5) \ No newline at end of file diff --git a/supervisor/http.py b/supervisor/http.py index af3e3da87..2e0b2f715 100644 --- a/supervisor/http.py +++ b/supervisor/http.py @@ -6,6 +6,7 @@ import errno import weakref import traceback +import supervisor.options try: import pwd @@ -635,10 +636,11 @@ def checkused(self, socketname): return True class tail_f_producer: - def __init__(self, request, filename, head): + def __init__(self, request, filename, head, is_html=False): self.request = weakref.ref(request) self.filename = filename self.delay = 0.1 + self.is_html = is_html self._open() sz = self._fsize() @@ -658,14 +660,45 @@ def more(self): bytes_added = newsz - self.sz if bytes_added < 0: self.sz = 0 - return "==> File truncated <==\n" + return "==> File truncated <==\n" if not self.is_html else "==> File truncated <==\n" if bytes_added > 0: self.file.seek(-bytes_added, 2) - bytes = self.file.read(bytes_added) + data = self.file.read(bytes_added) self.sz = newsz - return bytes + + if self.is_html and data: + # HTML 转义,防止破坏 HTML 结构 + data = data.replace(b'&', b'&').replace(b'<', b'<').replace(b'>', b'>') + # 高亮日志级别 + data = self._highlight_log_levels(data) + + return data return NOT_DONE_YET + def _highlight_log_levels(self, data): + """高亮显示不同级别的日志""" + import re + + # 将字节数据转换为字符串以便使用正则表达式 + if isinstance(data, bytes): + data_str = data.decode('utf-8', errors='replace') + else: + data_str = data + + # 定义日志级别的正则表达式和对应的 CSS 类 + patterns = [ + (r'\b(ERROR|CRITICAL|FATAL)\b', 'log-error'), + (r'\b(WARN|WARNING)\b', 'log-warn'), + (r'\b(INFO|NOTICE)\b', 'log-info'), + (r'\b(DEBUG|TRACE)\b', 'log-debug') + ] + + # 为每个匹配项添加 span 标签 + for pattern, css_class in patterns: + data_str = re.sub(pattern, r'\1' % css_class, data_str) + + return data_str.encode('utf-8') + def _open(self): self.file = open(self.filename, 'rb') self.ino = os.fstat(self.file.fileno())[stat.ST_INO] @@ -680,7 +713,7 @@ def _follow(self): except (OSError, ValueError): # file was unlinked return - + if self.ino != ino: # log rotation occurred self._close() self._open() @@ -744,18 +777,166 @@ def handle_request(self, request): request.error(404) # not found return - mtime = os.stat(logfile)[stat.ST_MTIME] - request['Last-Modified'] = http_date.build_http_date(mtime) - request['Content-Type'] = 'text/plain;charset=utf-8' - # the lack of a Content-Length header makes the outputter - # send a 'Transfer-Encoding: chunked' response - request['X-Accel-Buffering'] = 'no' - # tell reverse proxy server (e.g., nginx) to disable proxy buffering - # (see also http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering) - - request.push(tail_f_producer(request, logfile, 1024)) - - request.done() + # 获取请求的 format 参数,决定返回 HTML 还是纯文本 + is_html = True + if query: + import cgi + parsed_query = cgi.parse_qs(query) + is_html = parsed_query.get('format', ['html'])[0] != 'plain' + + if is_html: + # 返回美化的 HTML 页面 + mtime = os.stat(logfile)[stat.ST_MTIME] + request['Last-Modified'] = http_date.build_http_date(mtime) + request['Content-Type'] = 'text/html;charset=utf-8' + request['X-Accel-Buffering'] = 'no' + + # 创建进程的完整名称 + full_process_name = process_name + if group_name != process_name: + full_process_name = "%s:%s" % (group_name, process_name) + + # 构建 HTML 头部 + html_head = ''' + + + + 进程 %s 的日志 + + + +
+
+

进程 %s 的日志

+
+ +
+
+ +
+
''' % (full_process_name, full_process_name, request.uri)
+
+            html_foot = '''
+
+ + +
+ +''' % (supervisor.options.VERSION) + + # 发送带有美化页面的响应 + request.push(html_head) + request.push(tail_f_producer(request, logfile, 1024, is_html=True)) + request.push(html_foot) + request.done() + + else: + # 原始的纯文本响应 + mtime = os.stat(logfile)[stat.ST_MTIME] + request['Last-Modified'] = http_date.build_http_date(mtime) + request['Content-Type'] = 'text/plain;charset=utf-8' + request['X-Accel-Buffering'] = 'no' + + request.push(tail_f_producer(request, logfile, 1024, is_html=False)) + request.done() class mainlogtail_handler: IDENT = 'Main Logtail HTTP Request Handler' diff --git a/supervisor/ui/status.html b/supervisor/ui/status.html index 8347be08c..a1786b571 100644 --- a/supervisor/ui/status.html +++ b/supervisor/ui/status.html @@ -3,98 +3,82 @@ - - - Supervisor 状态监控 + + Supervisor 状态 -
-
StateDescriptionNameAction状态描述名称操作
nominalInfoName正常信息名称
- - - - - - - - +
+
+
+
+

分组名称

+
+ +
+
+
+
状态描述名称操作
+ + + + + + - - - - - - - - -
状态描述名称操作
正常信息名称 - -
+ + + 正常 + + + 信息 + + + 名称 + + + + + + +
+
- - - -
- - diff --git a/supervisor/ui/stylesheets/supervisor.css b/supervisor/ui/stylesheets/supervisor.css index 9c67455b8..e553bf90b 100644 --- a/supervisor/ui/stylesheets/supervisor.css +++ b/supervisor/ui/stylesheets/supervisor.css @@ -1,18 +1,16 @@ /* Supervisor Status Page Modern Style */ /* Reset and Base Styles */ * { - box-sizing: border-box; margin: 0; padding: 0; + box-sizing: border-box; } -html, body { - height: 100%; - font-family: 'Segoe UI', Arial, Helvetica, sans-serif; - font-size: 16px; - line-height: 1.5; +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + line-height: 1.6; color: #333; - background-color: #f5f5f5; + background: #f5f7fa; } a { @@ -27,25 +25,32 @@ a:hover { /* Layout */ #wrapper { - min-height: 100%; - width: 100%; - max-width: 1200px; + max-width: 1400px; margin: 0 auto; padding: 20px; - background-color: #fff; - box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); + min-height: 100vh; } +/* Header */ #header { - margin-bottom: 30px; - padding: 20px 0; - border-bottom: 2px solid #eaeaea; display: flex; align-items: center; + margin-bottom: 30px; + padding: 20px; + background: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); } #header img { - max-height: 50px; + height: 40px; + margin-right: 15px; +} + +#header h1 { + font-size: 24px; + color: #2c3e50; + margin: 0; } .hidden { @@ -53,163 +58,172 @@ a:hover { } /* Status Message */ -#statusmessage { - padding: 12px 15px; +.status_msg { + padding: 15px; margin-bottom: 20px; - background-color: #e8f4fd; - border-left: 4px solid #2c7be5; + background: #e1f5fe; + border-left: 4px solid #03a9f4; border-radius: 4px; - color: #1a56a8; + transition: opacity 0.5s ease-out; } /* Action Buttons */ #buttons { + list-style: none; display: flex; gap: 10px; - margin: 20px 0; - list-style: none; -} - -.action-button { - display: inline-block; + margin-bottom: 20px; } -.action-button a { - display: inline-block; +#buttons li a { + display: inline-flex; + align-items: center; padding: 8px 16px; - background-color: #2c7be5; + background: #1976d2; color: white; + text-decoration: none; border-radius: 4px; font-weight: 500; - text-transform: uppercase; - font-size: 14px; - letter-spacing: 0.5px; - border: none; - transition: all 0.2s; + transition: all 0.2s ease; } -.action-button a:hover { - background-color: #1a56a8; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +#buttons li a:hover { + background: #1565c0; + transform: translateY(-1px); } /* Tables */ table { width: 100%; border-collapse: collapse; - margin: 20px 0; - border-radius: 8px; - overflow: hidden; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + margin: 0; + table-layout: fixed; } th { - background-color: #2c7be5; - color: white; text-align: left; padding: 12px 15px; - font-weight: 500; + background-color: #f3f4f6; + border-bottom: 1px solid #ddd; + font-weight: 600; + color: #4b5563; } -th.state { - width: 80px; - text-align: center; +th.state, td.status { + width: 120px; } -th.desc { - width: 30%; +th.desc, td:nth-child(2) { + width: 250px; } -th.name { - width: 25%; +th.name, td:nth-child(3) { + width: auto; + min-width: 250px; } -th.action { - width: 20%; +th.action, td.action { + width: 300px; } td { - padding: 12px 15px; - border-bottom: 1px solid #eaeaea; -} - -tr:nth-child(even) td { - background-color: #f9f9f9; + padding: 10px 15px; + border-bottom: 1px solid #eee; + vertical-align: middle; } -tr:hover td { - background-color: #f0f7ff; +td:nth-child(3) a { + word-break: break-word; + display: inline-block; + width: 100%; + white-space: normal; } tr:last-child td { border-bottom: none; } +tr.shade { + background-color: #f9f9f9; +} + /* Status Indicators */ .status span { display: inline-block; - padding: 4px 8px; - border-radius: 50px; + padding: 4px 12px; + border-radius: 12px; font-size: 12px; - font-weight: 600; + font-weight: 500; text-align: center; - width: auto; - min-width: 80px; + min-width: 90px; } -.statusnominal { - background-color: #0abb87; - color: white; +.statusrunning { + background: #e3f2fd; + color: #1976d2; } -.statusrunning { - background-color: #ffb822; - color: #333; +.statusnominal { + background: #e8f5e9; + color: #2e7d32; } .statuserror { - background-color: #fd397a; - color: white; + background: #ffebee; + color: #c62828; +} + +.statusstopped { + background: #fff3e0; + color: #e65100; } /* Action Links */ .action ul { + margin: 0; + padding: 0; list-style: none; display: flex; - gap: 8px; + flex-wrap: nowrap; + gap: 10px; } -.action li { +.action ul li { display: inline-block; } -.action a { +.action ul li a { display: inline-block; - padding: 4px 10px; - background-color: #f5f5f5; - border: 1px solid #ddd; - border-radius: 4px; - color: #333; + white-space: nowrap; + padding: 5px 10px; font-size: 13px; - transition: all 0.2s; + color: #1976d2; + text-decoration: none; + border: 1px solid #e0e0e0; + border-radius: 4px; + background-color: #f5f5f5; + transition: all 0.2s ease; } -.action a:hover { - background-color: #2c7be5; +.action ul li a:hover { + background: #1976d2; color: white; - border-color: #2c7be5; + border-color: #1976d2; +} + +/* 移除分隔符,使用边框样式替代 */ +td.action ul li:not(:last-child):after { + content: none !important; + margin-left: 0; + display: none; } /* Footer */ #footer { - display: flex; - justify-content: space-between; - align-items: center; margin-top: 40px; - padding: 15px 0; - border-top: 1px solid #eaeaea; - color: #777; + padding: 20px; + border-top: 1px solid #e9ecef; + color: #6c757d; font-size: 14px; } @@ -223,40 +237,87 @@ tr:last-child td { } .clr:after { - content: ""; + content: ''; display: table; clear: both; } -/* 分组样式 */ +/* 进程组样式 */ .process-group { - margin-bottom: 20px; - border-radius: 8px; + margin-bottom: 30px; + border: 1px solid #e0e0e0; + border-radius: 6px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); - background-color: #fff; } .group-header { - background-color: #f0f7ff; - padding: 12px 15px; - display: flex; - align-items: center; - cursor: pointer; + padding: 10px 15px; + background-color: #f0f0f0; border-bottom: 1px solid #ddd; - transition: background-color 0.2s; } -.group-header:hover { - background-color: #e0f0ff; +.title-with-actions { + display: flex; + align-items: center; } -.group-name { +.group-title { + margin: 0; + padding: 0; + font-size: 16px; font-weight: 600; - font-size: 18px; - color: #2c7be5; - margin: 0 10px; - flex-grow: 1; +} + +.group-actions { + display: flex; + margin-left: 20px; +} + +.group-actions ul { + display: flex; + list-style: none; + margin: 0; + padding: 0; + gap: 10px; +} + +.group-actions ul li a { + display: inline-block; + padding: 4px 12px; + background: #f5f5f5; + color: #333; + text-decoration: none; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 13px; + transition: all 0.2s ease; +} + +.group-actions ul li a:hover { + background: #e0e0e0; + color: #000; +} + +/* 为组操作按钮添加特定颜色 */ +.group-actions ul li:nth-child(1) a { + background: #17a2b8; + color: white; + border-color: #138496; +} + +.group-actions ul li:nth-child(2) a { + background: #dc3545; + color: white; + border-color: #c82333; +} + +.group-actions ul li:nth-child(1) a:hover { + background: #138496; +} + +.group-actions ul li:nth-child(2) a:hover { + background: #c82333; } .group-summary { @@ -291,29 +352,6 @@ tr:last-child td { transition: transform 0.2s; } -.group-actions { - display: flex; - gap: 8px; -} - -.group-actions a { - display: flex; - align-items: center; - justify-content: center; - padding: 4px 10px; - background-color: #f5f5f5; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 13px; - transition: all 0.2s; -} - -.group-actions a:hover { - background-color: #2c7be5; - color: white; - border-color: #2c7be5; -} - .group-content { width: 100%; } @@ -321,19 +359,23 @@ tr:last-child td { /* Responsive Design */ @media (max-width: 768px) { #wrapper { - padding: 15px; + padding: 10px; } #buttons { flex-direction: column; } - .action-button { + .action-button a { width: 100%; + justify-content: center; } - .action-button a { - display: block; + .action ul { + flex-direction: column; + } + + .action a { text-align: center; } @@ -353,3 +395,34 @@ tr:last-child td { justify-content: space-between; } } + +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.status_msg { + animation: fadeIn 0.3s ease-out; +} + +td.action ul { + margin: 0; + padding: 0; + list-style: none; + display: flex; + gap: 8px; +} + +td.action ul li { + display: inline; +} + +td.action a { + color: #0066cc; + text-decoration: none; +} + +td.action a:hover { + text-decoration: underline; +} diff --git a/supervisor/ui/tail.html b/supervisor/ui/tail.html index 117cb5f5d..14f906c0e 100644 --- a/supervisor/ui/tail.html +++ b/supervisor/ui/tail.html @@ -3,25 +3,252 @@ - Supervisor Status + + 进程日志 + + + -
- -

-
-
- -
- - - +
+
+

进程日志

+
+ + + + + +
+
+ + + +
+

+  
+ +
- - -
+ diff --git a/supervisor/version.txt b/supervisor/version.txt index b75a1c1c0..80895903a 100644 --- a/supervisor/version.txt +++ b/supervisor/version.txt @@ -1 +1 @@ -4.3.0.dev0 +4.3.0 diff --git a/supervisor/web.py b/supervisor/web.py index e6d8a97d5..4f3e6b08a 100644 --- a/supervisor/web.py +++ b/supervisor/web.py @@ -176,6 +176,17 @@ def __call__(self): response = self.context.response headers = response['headers'] + + # 处理直接返回的 HTML 字符串 + if isinstance(body, str) or isinstance(body, unicode): + headers['Content-Type'] = self.content_type + headers['Pragma'] = 'no-cache' + headers['Cache-Control'] = 'no-cache' + headers['Expires'] = http_date.build_http_date(0) + response['body'] = as_bytes(body) + return response + + # 原有的处理逻辑 headers['Content-Type'] = self.content_type headers['Pragma'] = 'no-cache' headers['Cache-Control'] = 'no-cache' @@ -216,67 +227,259 @@ def render(self): tail = 'ERROR: unexpected rpc fault [%d] %s' % ( e.code, e.text) - root = self.clone() - - title = root.findmeld('title') - title.content('Supervisor tail of process %s' % processname) - tailbody = root.findmeld('tailbody') - tailbody.content(tail) - - refresh_anchor = root.findmeld('refresh_anchor') + title_text = '进程日志' if processname is None else '进程 %s 的日志' % processname + refresh_url = '' if processname is not None: - refresh_anchor.attributes( - href='tail.html?processname=%s&limit=%s' % ( + refresh_url = 'tail.html?processname=%s&limit=%s' % ( urllib.quote(processname), urllib.quote(str(abs(limit))) ) - ) - else: - refresh_anchor.deparent() + + # 使用自定义 HTML 模板 + html = ''' + + + + %s + + + +
+
+

%s

+
+ + + +
+
+ + + +
+
%s
+
+ + +
+ + + + +''' % (title_text, title_text, refresh_url, tail.replace('<', '<').replace('>', '>')) - return as_string(root.write_xhtmlstring()) + return html class StatusView(MeldView): def actions_for_process(self, process): - state = process.get_state() - processname = urllib.quote(make_namespec(process.group.config.name, - process.config.name)) - start = { - 'name': 'Start', - 'href': 'index.html?processname=%s&action=start' % processname, - 'target': None, - } - restart = { - 'name': 'Restart', + state = process['state'] + processname = urllib.quote(make_namespec(process['group'], process['name'])) + actions = [] + + if state == ProcessStates.RUNNING: + actions.extend([ + { + 'name': '重启', 'href': 'index.html?processname=%s&action=restart' % processname, - 'target': None, - } - stop = { - 'name': 'Stop', + }, + { + 'name': '停止', 'href': 'index.html?processname=%s&action=stop' % processname, - 'target': None, - } - clearlog = { - 'name': 'Clear Log', + }, + { + 'name': '清除日志', 'href': 'index.html?processname=%s&action=clearlog' % processname, - 'target': None, - } - tailf_stdout = { - 'name': 'Tail -f Stdout', + }, + { + 'name': '查看输出', 'href': 'logtail/%s' % processname, 'target': '_blank' } - tailf_stderr = { - 'name': 'Tail -f Stderr', - 'href': 'logtail/%s/stderr' % processname, + ]) + elif state in (ProcessStates.STOPPED, ProcessStates.EXITED, ProcessStates.FATAL): + actions.extend([ + { + 'name': '启动', + 'href': 'index.html?processname=%s&action=start' % processname, + }, + { + 'name': '清除日志', + 'href': 'index.html?processname=%s&action=clearlog' % processname, + }, + { + 'name': '查看输出', + 'href': 'logtail/%s' % processname, 'target': '_blank' } - if state == ProcessStates.RUNNING: - actions = [restart, stop, clearlog, tailf_stdout, tailf_stderr] - elif state in (ProcessStates.STOPPED, ProcessStates.EXITED, - ProcessStates.FATAL): - actions = [start, None, clearlog, tailf_stdout, tailf_stderr] + ]) else: - actions = [None, None, clearlog, tailf_stdout, tailf_stderr] + actions.extend([ + { + 'name': '清除日志', + 'href': 'index.html?processname=%s&action=clearlog' % processname, + }, + { + 'name': '查看输出', + 'href': 'logtail/%s' % processname, + 'target': '_blank' + } + ]) return actions def css_class_for_state(self, state): @@ -284,6 +487,8 @@ def css_class_for_state(self, state): return 'statusrunning' elif state in (ProcessStates.FATAL, ProcessStates.BACKOFF): return 'statuserror' + elif state == ProcessStates.STOPPED: + return 'statusstopped' else: return 'statusnominal' @@ -328,39 +533,15 @@ def restartall(): restartall.delay = 0.05 return restartall - elif action == 'startProcessGroup': - try: - callback = rpcinterface.supervisor.startProcessGroup(namespec) - except RPCError as e: - msg = 'unexpected rpc fault [%d] %s' % (e.code, e.text) - def startgrperr(): - return msg - startgrperr.delay = 0.05 - return startgrperr - - def startgroup(): - if callback() is NOT_DONE_YET: - return NOT_DONE_YET - return '组 %s 的所有进程已启动' % namespec - startgroup.delay = 0.05 - return startgroup - - elif action == 'stopProcessGroup': - try: - callback = rpcinterface.supervisor.stopProcessGroup(namespec) - except RPCError as e: - msg = 'unexpected rpc fault [%d] %s' % (e.code, e.text) - def stopgrperr(): - return msg - stopgrperr.delay = 0.05 - return stopgrperr - - def stopgroup(): - if callback() is NOT_DONE_YET: - return NOT_DONE_YET - return '组 %s 的所有进程已停止' % namespec - stopgroup.delay = 0.05 - return stopgroup + elif action == 'startgroup': + # 启动整个组 + return self.start_group(namespec) + elif action == 'stopgroup': + # 停止整个组 + return self.stop_group(namespec) + elif action == 'restartgroup': + # 重启整个组 + return self.restart_group(namespec) elif namespec: def wrong(): @@ -505,15 +686,13 @@ def render(self): if not self.callback: self.callback = self.make_callback(processname, action) return NOT_DONE_YET - else: - message = self.callback() + message = self.callback() if message is NOT_DONE_YET: return NOT_DONE_YET if message is not None: server_url = form['SERVER_URL'] - location = server_url + "/" + '?message=%s' % urllib.quote( - message) + location = server_url + "/" + '?message=%s' % urllib.quote(message) response['headers']['Location'] = location supervisord = self.context.supervisord @@ -522,210 +701,104 @@ def render(self): SupervisorNamespaceRPCInterface(supervisord))] ) - # 按组收集进程 - groups = {} - for groupname, group in supervisord.process_groups.items(): - groups[groupname] = [] - for process_name in group.processes.keys(): - sent_name = make_namespec(groupname, process_name) - info = rpcinterface.supervisor.getProcessInfo(sent_name) - process = group.processes[process_name] - actions = self.actions_for_process(process) - groups[groupname].append({ - 'status': info['statename'], - 'name': process_name, - 'group': groupname, - 'actions': actions, - 'state': info['state'], - 'description': info['description'], - }) + # 按组和独立进程组织 + groups = {} # 组名 -> 组下的进程名列表 + ungrouped = [] # 未分组的进程列表 + + # 首先构建所有组名的集合 + group_names = set() + for process_group in supervisord.process_groups.values(): + # 检查是否是真实的组(有多个进程) + processes = process_group.processes + if len(processes) > 1: + # 可能是一个组 + group_names.add(process_group.config.name) - # 按照组名称排序 + # 现在判断进程是否属于组并分类 + for process_group in supervisord.process_groups.values(): + group_name = process_group.config.name + + if group_name in group_names and len(process_group.processes) > 1: + # 这是一个组 + if group_name not in groups: + groups[group_name] = [] + + for process in process_group.processes.values(): + groups[group_name].append((group_name, process.config.name)) + else: + # 未分组的独立进程 + for process in process_group.processes.values(): + ungrouped.append((group_name, process.config.name)) + + # 对所有组排序 sorted_groups = sorted(groups.items()) + # 排序未分组进程 + ungrouped.sort() root = self.clone() - if message is not None: statusarea = root.findmeld('statusmessage') statusarea.attrib['class'] = 'status_msg' statusarea.content(message) - # 处理分组显示 - if sorted_groups: - process_groups_div = root.findmeld('process-groups') + if not (sorted_groups or ungrouped): + table = root.findmeld('statustable') + table.replace('暂无进程') + else: + content_div = root.findmeld('content') + process_groups = root.findmeld('process_groups') + template_group = process_groups.findmeld('template_group') - for groupname, processes in sorted_groups: - # 计算组的整体状态 - running_count = sum(1 for p in processes if p['state'] == ProcessStates.RUNNING) - error_count = sum(1 for p in processes if p['state'] in (ProcessStates.FATAL, ProcessStates.BACKOFF)) - total_count = len(processes) - - # 创建组容器 - group_div = templating.Element('div') - group_div.attrib['class'] = 'process-group' - - # 创建组标题栏 - header_div = templating.Element('div') - header_div.attrib['class'] = 'group-header' - - # 添加折叠图标 - icon = templating.Element('i') - icon.attrib['class'] = 'fas fa-angle-down group-icon' - header_div.append(icon) - - # 添加组名称 - group_name = templating.Element('div') - group_name.attrib['class'] = 'group-name' - group_name.content(groupname) - header_div.append(group_name) - - # 添加组状态摘要 - summary_div = templating.Element('div') - summary_div.attrib['class'] = 'group-summary' - - # 状态标签 - status_span = templating.Element('div') - if error_count > 0: - status_class = 'group-status error' - status_text = '%d/%d 错误' % (error_count, total_count) - elif running_count == total_count: - status_class = 'group-status running' - status_text = '全部运行 (%d)' % total_count - elif running_count == 0: - status_class = 'group-status' - status_text = '全部停止 (%d)' % total_count - else: - status_class = 'group-status partial' - status_text = '%d/%d 运行中' % (running_count, total_count) - - status_span.attrib['class'] = status_class - status_span.content(status_text) - summary_div.append(status_span) - - # 组操作按钮 - actions_div = templating.Element('div') - actions_div.attrib['class'] = 'group-actions' - - # 启动全部按钮 - start_a = templating.Element('a') - start_a.attrib['href'] = 'index.html?action=startProcessGroup&processname=%s' % urllib.quote(groupname) - start_a.content('启动全部') - actions_div.append(start_a) - - # 停止全部按钮 - stop_a = templating.Element('a') - stop_a.attrib['href'] = 'index.html?action=stopProcessGroup&processname=%s' % urllib.quote(groupname) - stop_a.content('停止全部') - actions_div.append(stop_a) - - summary_div.append(actions_div) - header_div.append(summary_div) - - group_div.append(header_div) - - # 创建组内容区域 - content_div = templating.Element('div') - content_div.attrib['class'] = 'group-content' + # 移除模板组 + template_group.deparent() + + # 处理未分组进程(如果有) + if ungrouped: + group_div = template_group.clone() + group_title = group_div.findmeld('group_title') + group_title.content('独立进程') - # 创建进程表格 - table = templating.Element('table') + # 隐藏组操作按钮 - 添加错误处理 + group_actions = group_div.findmeld('group_actions') + if group_actions is not None: # 确保元素存在 + group_actions.attrib['style'] = 'display: none;' - # 表头 - thead = templating.Element('thead') - tr = templating.Element('tr') + table = group_div.findmeld('statustable') + template_row = table.findmeld('tr') - th_state = templating.Element('th') - th_state.attrib['class'] = 'state' - th_state.content('状态') - tr.append(th_state) + # 移除模板行 + template_row.deparent() - th_desc = templating.Element('th') - th_desc.attrib['class'] = 'desc' - th_desc.content('描述') - tr.append(th_desc) + for i, (groupname, processname) in enumerate(ungrouped): + self._render_row(table, template_row, i, groupname, processname, rpcinterface) - th_name = templating.Element('th') - th_name.attrib['class'] = 'name' - th_name.content('名称') - tr.append(th_name) + process_groups.append(group_div) + + # 处理每个分组 + for group_name, processes in sorted_groups: + group_div = template_group.clone() + group_title = group_div.findmeld('group_title') + group_title.content('分组: ' + group_name) - th_action = templating.Element('th') - th_action.attrib['class'] = 'action' - th_action.content('操作') - tr.append(th_action) + # 恢复组操作按钮设置,并添加错误处理 + group_stop = group_div.findmeld('group_stop_anchor') + if group_stop is not None: + group_stop.attributes(href='index.html?action=stopgroup&processname=' + group_name) - thead.append(tr) - table.append(thead) + group_restart = group_div.findmeld('group_restart_anchor') + if group_restart is not None: + group_restart.attributes(href='index.html?action=restartgroup&processname=' + group_name) - # 表内容 - tbody = templating.Element('tbody') + table = group_div.findmeld('statustable') + template_row = table.findmeld('tr') - for i, process in enumerate(processes): - tr = templating.Element('tr') - if i % 2: - tr.attrib['class'] = 'shade' - - # 状态列 - td_status = templating.Element('td') - td_status.attrib['class'] = 'status' - - status_span = templating.Element('span') - status_span.attrib['class'] = self.css_class_for_state(process['state']) - status_span.content(process['status'].lower()) - td_status.append(status_span) - tr.append(td_status) - - # 描述列 - td_info = templating.Element('td') - info_span = templating.Element('span') - info_span.content(process['description']) - td_info.append(info_span) - tr.append(td_info) - - # 名称列 - td_name = templating.Element('td') - name_a = templating.Element('a') - processname = make_namespec(process['group'], process['name']) - name_a.attrib['href'] = 'tail.html?processname=%s' % urllib.quote(processname) - name_a.attrib['target'] = '_blank' - name_a.content(processname) - td_name.append(name_a) - tr.append(td_name) - - # 操作列 - td_action = templating.Element('td') - td_action.attrib['class'] = 'action' - ul = templating.Element('ul') - - for action in process['actions']: - li = templating.Element('li') - if action is None: - li.attrib['class'] = 'hidden' - a = templating.Element('a') - a.attrib['href'] = '#' - li.append(a) - else: - a = templating.Element('a') - a.attrib['href'] = action['href'] - a.content(action['name']) - if action['target']: - a.attrib['target'] = action['target'] - li.append(a) - ul.append(li) - - td_action.append(ul) - tr.append(td_action) - - tbody.append(tr) + # 移除模板行 + template_row.deparent() - table.append(tbody) - content_div.append(table) - group_div.append(content_div) + for i, (groupname, processname) in enumerate(processes): + self._render_row(table, template_row, i, groupname, processname, rpcinterface) - process_groups_div.append(group_div) - else: - process_groups_div = root.findmeld('process-groups') - process_groups_div.content('没有程序可以管理') + process_groups.append(group_div) root.findmeld('supervisor_version').content(VERSION) copyright_year = str(datetime.date.today().year) @@ -733,6 +806,141 @@ def render(self): return as_string(root.write_xhtmlstring()) + def _render_row(self, table, template_row, i, groupname, processname, rpcinterface): + row = template_row.clone() + sent_name = make_namespec(groupname, processname) + info = rpcinterface.supervisor.getProcessInfo(sent_name) + actions = self.actions_for_process(info) + + status_text = row.findmeld('status_text') + info_text = row.findmeld('info_text') + name_anchor = row.findmeld('name_anchor') + + if i % 2: + row.attrib['class'] = 'shade' + else: + row.attrib['class'] = '' + + status_text.content(info['statename']) + status_text.attrib['class'] = self.css_class_for_state(info['state']) + info_text.content(info['description']) + name_anchor.attributes(href='tail.html?processname=%s' % urllib.quote(sent_name)) + name_anchor.content(sent_name) + + actionitem_td = row.findmeld('actionitem_td') + template_action = actionitem_td.findmeld('actionitem') + + # 移除模板操作项 + template_action.deparent() + + for action in actions: + action_item = template_action.clone() + action_anchor = action_item.findmeld('actionitem_anchor') + action_anchor.attributes(href=action['href']) + if 'target' in action: + action_anchor.attributes(target=action['target']) + action_anchor.content(action['name']) + actionitem_td.append(action_item) + + table.append(row) + + def start_group(self, group_name): + """启动整个组的所有进程""" + supervisord = self.context.supervisord + rpcinterface = SupervisorNamespaceRPCInterface(supervisord) + + # 先停止所有进程,然后再启动 + try: + stop_callback = rpcinterface.stopProcessGroup(group_name) + except RPCError as e: + msg = '无法启动组 %s: [%d] %s' % (group_name, e.code, e.text) + def startgrperr(): + return msg + startgrperr.delay = 0.05 + return startgrperr + + def start_group_cont(): + if stop_callback() is NOT_DONE_YET: + return NOT_DONE_YET + + # 停止完成,现在启动 + try: + start_callback = rpcinterface.startProcessGroup(group_name) + except RPCError as e: + return '组 %s 已停止,但无法重新启动: [%d] %s' % (group_name, e.code, e.text) + + def start_group_cont(): + if start_callback() is NOT_DONE_YET: + return NOT_DONE_YET + return '组 %s 的所有进程已重启' % group_name + + start_group_cont.delay = 0.05 + return start_group_cont() + + start_group_cont.delay = 0.05 + return start_group_cont + + def stop_group(self, group_name): + """停止整个组的所有进程""" + supervisord = self.context.supervisord + rpcinterface = SupervisorNamespaceRPCInterface(supervisord) + + try: + callback = rpcinterface.stopProcessGroup(group_name) + except RPCError as e: + msg = '无法停止组 %s: [%d] %s' % (group_name, e.code, e.text) + def stopgrperr(): + return msg + stopgrperr.delay = 0.05 + return stopgrperr + + def stopgroup(): + if callback() is NOT_DONE_YET: + return NOT_DONE_YET + return '组 %s 的所有进程已停止' % group_name + stopgroup.delay = 0.05 + return stopgroup + + def restart_group(self, group_name): + """重启整个组的所有进程""" + supervisord = self.context.supervisord + + # 创建正确的RPC接口 + main = ('supervisor', SupervisorNamespaceRPCInterface(supervisord)) + system = ('system', SystemNamespaceRPCInterface([main])) + rpcinterface = RootRPCInterface([main, system]) + + # 使用multicall一次性执行停止和启动操作 + try: + callback = rpcinterface.system.multicall([ + {'methodName': 'supervisor.stopProcessGroup', 'params': [group_name]}, + {'methodName': 'supervisor.startProcessGroup', 'params': [group_name]} + ]) + except RPCError as e: + msg = '无法重启组 %s: [%d] %s' % (group_name, e.code, e.text) + def restartgrperr(): + return msg + restartgrperr.delay = 0.05 + return restartgrperr + + def restart_result(): + result = callback() + if result is NOT_DONE_YET: + return NOT_DONE_YET + + # 检查结果 + stop_result, start_result = result + if isinstance(stop_result, dict) and 'faultString' in stop_result: + return '组 %s 重启失败: %s' % (group_name, stop_result['faultString']) + + if isinstance(start_result, dict) and 'faultString' in start_result: + return '组 %s 已停止,但无法重新启动: %s' % (group_name, start_result['faultString']) + + return '组 %s 的所有进程已重启' % group_name + + restart_result.delay = 0.05 + return restart_result + class OKView: delay = 0 def __init__(self, context): diff --git a/supervisor_cmd.sh b/supervisor_cmd.sh new file mode 100755 index 000000000..ebe944168 --- /dev/null +++ b/supervisor_cmd.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Supervisor 命令辅助脚本 + +# 如果没有提供参数,则显示帮助 +if [ $# -eq 0 ]; then + echo "Supervisor 命令辅助脚本" + echo "用法: $0 <命令>" + echo "" + echo "常用命令:" + echo " status - 查看所有进程状态" + echo " start - 启动进程" + echo " stop - 停止进程" + echo " restart - 重启进程" + echo " shutdown - 关闭 Supervisor" + echo " reread - 重新读取配置" + echo " update - 更新配置" + echo " help - 显示更多命令" + exit 1 +fi + +# 使用配置文件路径运行 supervisorctl +supervisorctl -c $(pwd)/supervisord.conf "$@" \ No newline at end of file From c1491accf092bf5481f1c41274e1e269ec3bfa82 Mon Sep 17 00:00:00 2001 From: feiwentao <505735413@qq.com> Date: Mon, 17 Mar 2025 07:46:21 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E9=94=99=E8=AF=AF:=20=E8=A7=A3=E5=86=B3tail.html=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E4=B8=AD=E9=87=8D=E5=A4=8Dmeld=20ID=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervisor/ui/tail.html | 2 +- supervisor/web.py | 203 +++------------------------------------- 2 files changed, 12 insertions(+), 193 deletions(-) diff --git a/supervisor/ui/tail.html b/supervisor/ui/tail.html index 14f906c0e..b97463f81 100644 --- a/supervisor/ui/tail.html +++ b/supervisor/ui/tail.html @@ -132,7 +132,7 @@
-

进程日志

+

进程日志

diff --git a/supervisor/web.py b/supervisor/web.py index 4f3e6b08a..4ece3b109 100644 --- a/supervisor/web.py +++ b/supervisor/web.py @@ -204,7 +204,8 @@ class TailView(MeldView): def render(self): supervisord = self.context.supervisord form = self.context.form - + root = self.clone() + if not 'processname' in form: tail = 'No process name found' processname = None @@ -233,198 +234,16 @@ def render(self): refresh_url = 'tail.html?processname=%s&limit=%s' % ( urllib.quote(processname), urllib.quote(str(abs(limit))) ) - - # 使用自定义 HTML 模板 - html = ''' - - - - %s - - - -
-
-

%s

-
- - - -
-
- - - -
-
%s
-
- - -
- - - - -''' % (title_text, title_text, refresh_url, tail.replace('<', '<').replace('>', '>')) - - return html + # 设置模板中的值 + root.findmeld('title').content(title_text) + root.findmeld('header_title').content(title_text) + refresh_anchor = root.findmeld('refresh_anchor') + refresh_anchor.attributes(href=refresh_url) + tailbody = root.findmeld('tailbody') + tailbody.content(tail) + + return root.write_xhtmlstring() class StatusView(MeldView): def actions_for_process(self, process): From 50681a22dd89a76537d193152b6e45ab3c89fe26 Mon Sep 17 00:00:00 2001 From: feiwentao <505735413@qq.com> Date: Mon, 17 Mar 2025 07:50:08 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E6=9F=A5=E7=9C=8B=E5=8A=9F=E8=83=BD=EF=BC=9A=E5=9C=A8logtail?= =?UTF-8?q?=5Fhandler=E4=B8=AD=E4=BF=AE=E5=A4=8DURI=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=92=8C=E6=A0=BC=E5=BC=8F=E5=8C=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervisor/http.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/supervisor/http.py b/supervisor/http.py index 2e0b2f715..52e0b3e3d 100644 --- a/supervisor/http.py +++ b/supervisor/http.py @@ -778,11 +778,11 @@ def handle_request(self, request): return # 获取请求的 format 参数,决定返回 HTML 还是纯文本 - is_html = True + is_html = False if query: import cgi parsed_query = cgi.parse_qs(query) - is_html = parsed_query.get('format', ['html'])[0] != 'plain' + is_html = parsed_query.get('format', ['plain'])[0] == 'html' if is_html: # 返回美化的 HTML 页面 @@ -909,7 +909,7 @@ def handle_request(self, request):
-
''' % (full_process_name, full_process_name, request.uri)
+      
''' % (full_process_name, full_process_name, request.args[0])
 
             html_foot = '''
@@ -930,11 +930,13 @@ def handle_request(self, request): else: # 原始的纯文本响应 + # 设置内容类型和必要的头部 + request['Content-Type'] = 'text/plain;charset=utf-8' mtime = os.stat(logfile)[stat.ST_MTIME] request['Last-Modified'] = http_date.build_http_date(mtime) - request['Content-Type'] = 'text/plain;charset=utf-8' request['X-Accel-Buffering'] = 'no' - + + # 直接推送日志内容 request.push(tail_f_producer(request, logfile, 1024, is_html=False)) request.done() From a78c4e032c611b857964acd1d87ebc7197781178 Mon Sep 17 00:00:00 2001 From: feiwentao <505735413@qq.com> Date: Mon, 17 Mar 2025 07:53:23 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E5=B0=86=E7=95=8C=E9=9D=A2=E4=BB=8E?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E6=94=B9=E5=9B=9E=E8=8B=B1=E6=96=87=EF=BC=9A?= =?UTF-8?q?=E4=BF=AE=E6=94=B9status=E9=A1=B5=E9=9D=A2=E5=92=8Ctail?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervisor/http.py | 8 +-- supervisor/ui/status.html | 32 ++++++------ supervisor/ui/tail.html | 22 ++++---- supervisor/web.py | 104 +++++++++++++++++++------------------- 4 files changed, 82 insertions(+), 84 deletions(-) diff --git a/supervisor/http.py b/supervisor/http.py index 52e0b3e3d..fb567fdc5 100644 --- a/supervisor/http.py +++ b/supervisor/http.py @@ -801,7 +801,7 @@ def handle_request(self, request): - 进程 %s 的日志 + Process %s Log