From 316dc36077f97ca92b2372a66a8c250f00fb7490 Mon Sep 17 00:00:00 2001 From: "COT001\\DEV" <871066422@qq.com> Date: Fri, 27 Mar 2026 10:29:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9A=E7=A8=BF=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _verify_report.py | 149 ++ check_template_structure.py | 54 + configs/1000泵/table.py | 38 +- configs/600泵/config.json | 2 +- configs/600泵/table.py | 115 +- diagnose_word_com.py | 243 +++ experiments.db1 | Bin 0 -> 1212416 bytes report_generator.py | 2139 ++++--------------------- report_generator.py.bak2 | 240 +++ report_generator_docx.py | 163 ++ temperature_table_with_load_status.py | 49 + test_docx_fill.py | 30 + test_table_script_debug.py | 65 + testdemo.py | 0 ui_main.py | 163 +- 15 files changed, 1546 insertions(+), 1904 deletions(-) create mode 100644 _verify_report.py create mode 100644 check_template_structure.py create mode 100644 diagnose_word_com.py create mode 100644 experiments.db1 create mode 100644 report_generator.py.bak2 create mode 100644 report_generator_docx.py create mode 100644 test_docx_fill.py create mode 100644 test_table_script_debug.py create mode 100644 testdemo.py diff --git a/_verify_report.py b/_verify_report.py new file mode 100644 index 0000000..70039db --- /dev/null +++ b/_verify_report.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +"""验证 report_generator.py 的核心逻辑""" +import sys, os, io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +sys.path.insert(0, os.path.dirname(__file__)) + +from docx import Document +from report_generator import _replace_token_across_runs, _get_unique_cells, _fill_script_table_docx, _replace_texts_docx + +# 测试1: 跨run替换 +print("=" * 60) +print("Test 1: _replace_token_across_runs") +print("=" * 60) + +doc = Document() + +# {text4} split: '{' + 'text4' + '}' +para1 = doc.add_paragraph() +para1.add_run('before\t') +para1.add_run('{') +para1.add_run('text4') +para1.add_run('}') +para1.add_run('\tafter') +print(f"Before: '{para1.text}'") +_replace_token_across_runs(para1, '{text4}', 'VALUE4') +print(f"After: '{para1.text}'") +assert '{' not in para1.text and 'VALUE4' in para1.text, "FAIL: text4" +print("PASS: text4") + +# {isNormal} split: '{isNormal' + '}' +para2 = doc.add_paragraph() +para2.add_run('OK ') +para2.add_run('{isNormal') +para2.add_run('}') +para2.add_run(' NOT') +print(f"\nBefore: '{para2.text}'") +_replace_token_across_runs(para2, '{isNormal}', 'V') +print(f"After: '{para2.text}'") +assert '{' not in para2.text and 'V' in para2.text, "FAIL: isNormal" +print("PASS: isNormal") + +# {scriptTable1} split: '{' + 'scriptTable1' + '}' + text +para3 = doc.add_paragraph() +para3.add_run('{') +para3.add_run('scriptTable1') +para3.add_run('}') +para3.add_run('label') +print(f"\nBefore: '{para3.text}'") +_replace_token_across_runs(para3, '{scriptTable1}', '') +print(f"After: '{para3.text}'") +assert para3.text == 'label', f"FAIL: scriptTable1, got '{para3.text}'" +print("PASS: scriptTable1") + +# 测试2: 模板表格结构分析 +print("\n" + "=" * 60) +print("Test 2: Template table structure") +print("=" * 60) + +template_path = 'configs/600泵/template.docx' +if not os.path.exists(template_path): + print("SKIP: template not found") + sys.exit(0) + +doc2 = Document(template_path) +table = doc2.tables[0] + +for ri in range(min(5, len(table.rows))): + unique = _get_unique_cells(table.rows[ri]) + raw_count = len(table.rows[ri].cells) + texts = [c.text.replace('\n', '|')[:25] for c in unique] + print(f"Row {ri}: raw={raw_count}, unique={len(unique)}: {texts}") + +# 找 token 位置 +token_ucol = -1 +for ri, row in enumerate(table.rows): + unique = _get_unique_cells(row) + for uci, cell in enumerate(unique): + if 'scriptTable1' in cell.text: + token_ucol = uci + print(f"\nToken at row={ri}, unique_col={uci}, text='{cell.text}'") + break + +# 测试3: 完整填充 +print("\n" + "=" * 60) +print("Test 3: Full fill test") +print("=" * 60) + +doc3 = Document(template_path) + +# 文本替换 +text_map = { + 'text3': 'P67-13-103', + 'text2': 'W2001150.001-01:10', + 'text4': 'ZhuJS', + 'text1': '2025-12-03', + 'isNormal': 'V', +} +_replace_texts_docx(doc3, text_map) + +# 验证段落替换 +for para in doc3.paragraphs: + t = para.text + if 'ZhuJS' in t or 'V' in t or 'P67' in t: + print(f" Para: '{t[:80]}'") + +# 表格填充 +spec = { + 'token': 'scriptTable1', + 'cells': [ + {'row': 0, 'col': 1, 'value': '11.2C'}, + {'row': 1, 'col': 1, 'value': '2026-03-16 14:17:33'}, + {'row': 1, 'col': 3, 'value': '2026-03-16 17:47:33'}, + {'row': 4, 'col': 0, 'value': '21.6'}, + {'row': 4, 'col': 1, 'value': '26.7'}, + {'row': 4, 'col': 6, 'value': '36.0'}, + {'row': 16, 'col': 0, 'value': '11.6'}, + ] +} +_fill_script_table_docx(doc3, 'scriptTable1', spec) + +# 验证结果 +table3 = doc3.tables[0] + +u0 = _get_unique_cells(table3.rows[0]) +u1 = _get_unique_cells(table3.rows[1]) +u4 = _get_unique_cells(table3.rows[4]) +u16 = _get_unique_cells(table3.rows[16]) + +results = [ + (f"Row0[1]", u0[1].text, 'label should remain'), + (f"Row0[2]", u0[2].text, 'should be 11.2C'), + (f"Row1[2]", u1[2].text, 'should be start time'), + (f"Row1[4]", u1[4].text, 'should be end time'), + (f"Row4[0]", u4[0].text, 'label should remain'), + (f"Row4[1]", u4[1].text, 'should be 21.6'), + (f"Row4[2]", u4[2].text, 'should be 26.7'), + (f"Row4[7]", u4[7].text if len(u4) > 7 else 'N/A', 'should be 36.0'), + (f"Row16[1]", u16[1].text if len(u16) > 1 else 'N/A', 'should be 11.6'), +] + +print("\nResults:") +all_ok = True +for name, val, desc in results: + status = 'OK' if val.strip() else 'EMPTY' + print(f" {name} = '{val}' ({desc}) [{status}]") + +doc3.save('test_output_verify.docx') +print(f"\nSaved: test_output_verify.docx") +print("\nDone!") diff --git a/check_template_structure.py b/check_template_structure.py new file mode 100644 index 0000000..4215a3a --- /dev/null +++ b/check_template_structure.py @@ -0,0 +1,54 @@ +import win32com.client as win32 +import pythoncom + +pythoncom.CoInitialize() +try: + word = win32.Dispatch("Word.Application") + word.Visible = False + + doc = word.Documents.Open(r"c:\PPRO\PCM_Report\configs\600泵\template.docx") + + print("=== 模板文档分析 ===") + print(f"表格数量: {doc.Tables.Count}") + + if doc.Tables.Count > 0: + table = doc.Tables(1) + print(f"\n第一个表格:") + print(f" 行数: {table.Rows.Count}") + print(f" 列数: {table.Columns.Count}") + + print("\n查找 {scriptTable1} token:") + found = False + for row_idx in range(1, min(5, table.Rows.Count + 1)): + row = table.Rows(row_idx) + for col_idx in range(1, min(10, row.Cells.Count + 1)): + try: + cell = row.Cells(col_idx) + text = cell.Range.Text + clean = text.replace('\r\x07', '').replace('\x07', '').strip() + if '{scriptTable1}' in clean or 'scriptTable' in clean: + print(f" 找到! 位置: 行{row_idx}, 列{col_idx}") + print(f" 原始文本: {repr(text[:50])}") + print(f" 清理后: {clean[:50]}") + found = True + except: + pass + + if not found: + print(" 未找到 {scriptTable1}") + print("\n前3行前5列内容:") + for row_idx in range(1, min(4, table.Rows.Count + 1)): + print(f"\n 行{row_idx}:") + row = table.Rows(row_idx) + for col_idx in range(1, min(6, row.Cells.Count + 1)): + try: + cell = row.Cells(col_idx) + text = cell.Range.Text.replace('\r\x07', '').replace('\x07', '').strip() + print(f" 列{col_idx}: {text[:30]}") + except: + pass + + doc.Close(False) + word.Quit() +finally: + pythoncom.CoUninitialize() diff --git a/configs/1000泵/table.py b/configs/1000泵/table.py index 778e9b6..34dd4fc 100644 --- a/configs/1000泵/table.py +++ b/configs/1000泵/table.py @@ -170,8 +170,8 @@ def _parse_time_slot(slot_str: str) -> float: def _time_slots() -> List[str]: raw = os.environ.get("TABLE_TIME_SLOTS", "").strip() if not raw: - # 根据图片,时间刻度是:0.5h, 1h, 1.5h, 2h, 2.5h, 3h, 3.5h(7列) - return ["0.5h", "1h", "1.5h", "2h", "2.5h", "3h", "3.5h"] + # 时间刻度:0.5h, 1h, 1.5h, 2h, 2.5h, 3h, 3.4h(7列) + return ["0.5h", "1h", "1.5h", "2h", "2.5h", "3h", "3.4h"] slots = [slot.strip() for slot in raw.split(",")] return [slot for slot in slots if slot] @@ -382,7 +382,16 @@ def _calculate_effective_time_points( if target_effective_hours > total_effective_hours: LOGGER.warning("Target effective time %.3fh exceeds total effective time %.3fh for slot %s", target_effective_hours, total_effective_hours, slot_str) - effective_time_points[slot_str] = None + # 取最后一个有效运行结束时间点,往前60秒 + if effective_periods: + last_period = effective_periods[-1] + target_time_point = last_period['end'] - timedelta(seconds=60) + effective_time_points[slot_str] = target_time_point + LOGGER.info("Slot %s: effective %.3fh > total %.3fh, using last period end-60s: %s", + slot_str, target_effective_hours, total_effective_hours, + target_time_point.strftime('%H:%M:%S')) + else: + effective_time_points[slot_str] = None continue # 在有效时间段中查找累计运行target_effective_hours小时的时间点 @@ -672,8 +681,12 @@ def _load_temperature_data_with_load_status( target_time_point = effective_time_points.get(slot_str) if target_time_point is None: - LOGGER.warning("No effective time point calculated for slot %s, skipping", slot_str) - continue + LOGGER.warning("No effective time point for slot %s, using simple offset", slot_str) + slot_hours = _parse_time_slot(slot_str) + target_time_point = start_time + timedelta(hours=slot_hours) + if target_time_point > end_time: + LOGGER.warning("Time point %s exceeds end time, skipping", slot_str) + continue LOGGER.debug("Processing slot %s at effective time point %s", slot_str, target_time_point.strftime('%Y-%m-%d %H:%M:%S')) @@ -842,10 +855,15 @@ def build_temperature_table_with_load_status(_: Dict[str, Any]) -> Dict[str, Any start_str = os.environ.get("EXPERIMENT_START", "").strip() if start_str and start_time: try: - # 使用与原始脚本相同的时间处理逻辑 - utc_aware_dt = datetime.strptime(start_str, "%Y-%m-%dT%H:%M:%S%z") - local_dt1 = utc_aware_dt.astimezone(tz=None) - local_dt2 = utc_aware_dt.astimezone(tz=None) + timedelta(hours=3.5) + # 尝试带时区和不带时区两种格式 + try: + utc_aware_dt = datetime.strptime(start_str, "%Y-%m-%dT%H:%M:%S%z") + local_dt1 = utc_aware_dt.astimezone(tz=None) + except ValueError: + # 不带时区,直接解析为本地时间 + local_dt1 = datetime.strptime(start_str, "%Y-%m-%dT%H:%M:%S") + + local_dt2 = local_dt1 + timedelta(hours=3.5) start_time_value = local_dt1.strftime("%Y-%m-%d %H:%M:%S") end_time_value = local_dt2.strftime("%Y-%m-%d %H:%M:%S") cells.append({"row": start_time_row, "col": start_time_value_col, "value": start_time_value}) @@ -875,7 +893,7 @@ def build_temperature_table_with_load_status(_: Dict[str, Any]) -> Dict[str, Any ) # 确保value不是None,避免Word COM操作异常(与原始脚本一致) if value is not None: - cells.append({"row": 0, "col": 1, "value": f"{value:.1f}"}) + cells.append({"row": 0, "col": 1, "value": f"{value:.1f}℃"}) else: cells.append({"row": 0, "col": 1, "value": ""}) diff --git a/configs/600泵/config.json b/configs/600泵/config.json index 1982fd7..4cbcb4e 100644 --- a/configs/600泵/config.json +++ b/configs/600泵/config.json @@ -167,7 +167,7 @@ "10" ] ], - "scriptFile": "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uDQojIC0qLSBjb2Rpbmc6IHV0Zi04IC0qLQ0KIiIiDQrmtYvor5Xpg6jkvY3muKnluqborrDlvZXooajnlJ/miJDohJrmnKzvvIjluKbotJ/ovb3nirbmgIHnrZvpgInvvIkNCg0KLSDlv73nlaXkvKDlhaXnmoQgZXhwZXJpbWVudFByb2Nlc3PvvIzoh6rooYzmnoTpgKDlm7rlrprnu5PmnoTnmoTmlbDmja4NCi0g5LuOIEluZmx1eERCIOafpeivouavj+S4qua1i+ivlemDqOS9jeWcqOWQhOaXtumXtOeCueeahOeerOaXtua4qeW6puWAvA0KLSDmt7vliqAgbG9hZF9zdGF0dXMgPSAxIOeahOetm+mAieadoeS7tu+8jOehruS/neWPquWcqOecn+ato+mHh+mbhuaVsOaNruaXtuiOt+WPlua4qeW6pg0KLSDovpPlh7rmoLzlvI/kuI7lupTnlKjkuK3nmoQgc2NyaXB0VGFibGUg5Y2g5L2N56ym5YW85a65DQotIOm7mOiupOaKiiB7c2NyaXB0VGFibGUxfSDmlL7lnKgi5rWL6K+V6YOo5L2NIuaJgOWcqOeahOWNleWFg+agvA0KDQrnjq/looPlj5jph4/vvJoNCiAgICBUQUJMRV9UT0tFTiAgICAgICAgIOebruagh+WNoOS9jeespu+8jOm7mOiupCBzY3JpcHRUYWJsZTENCiAgICBUQUJMRV9TVEFSVF9ST1cgICAgIOWGmeWFpei1t+Wni+ihjOWBj+enu++8jOm7mOiupCAwDQogICAgVEFCTEVfU1RBUlRfQ09MICAgICDlhpnlhaXotbflp4vliJflgY/np7vvvIzpu5jorqQgMA0KICAgIFRBQkxFX1RJTUVfU0xPVFMgICAg6YCX5Y+35YiG6ZqU55qE5pe26Ze05Yi75bqm77yM6buY6K6kICIwLjVoLDFoLDEuNWgsMmgsMi41aCwzaCwzLjVoIg0KICAgIFRBQkxFX01PVE9SX1NQRUVEICAg55S15py66L2s6YCf5qCH562+77yM6buY6K6kICI5ODBSUE0iDQogICAgRVhQRVJJTUVOVF9TVEFSVCAgICAg5a6e6aqM5byA5aeL5pe26Ze077yISVNPIDg2MDEg5qC85byP77yM5aaCIDIwMjQtMDEtMDFUMTA6MDA6MDBa77yJDQogICAgRVhQRVJJTUVOVF9FTkQgICAgICAg5a6e6aqM57uT5p2f5pe26Ze077yISVNPIDg2MDEg5qC85byP77yJDQogICAgSU5GTFVYX1VSTCAgICAgICAgICAgSW5mbHV4REIgVVJMDQogICAgSU5GTFVYX09SRyAgICAgICAgICAgSW5mbHV4REIg57uE57uHDQogICAgSU5GTFVYX1RPS0VOICAgICAgICAgSW5mbHV4REIg5Luk54mMDQogICAgSU5GTFVYX0JVQ0tFVCAgICAgICAgSW5mbHV4REIg5qG25ZCN77yM6buY6K6kIFBDTQ0KICAgIElORkxVWF9NRUFTVVJFTUVOVCAgIEluZmx1eERCIOa1i+mHj+WQje+8jOm7mOiupCBQQ01fTWVhc3VyZW1lbnQNCiIiIg0KDQpmcm9tIF9fZnV0dXJlX18gaW1wb3J0IGFubm90YXRpb25zDQoNCmltcG9ydCBqc29uDQppbXBvcnQgbG9nZ2luZw0KaW1wb3J0IG9zDQppbXBvcnQgc3lzDQpmcm9tIGRhdGV0aW1lIGltcG9ydCBkYXRldGltZSwgdGltZWRlbHRhDQpmcm9tIHR5cGluZyBpbXBvcnQgQW55LCBEaWN0LCBMaXN0LCBPcHRpb25hbA0KDQoNCkxPR0dFUiA9IGxvZ2dpbmcuZ2V0TG9nZ2VyKF9fbmFtZV9fKQ0KDQoNCmRlZiBfbWFza19zZWNyZXQodmFsdWU6IE9wdGlvbmFsW3N0cl0pIC0+IHN0cjoNCiAgICAiIiLmjqnnoIHmlY/mhJ/kv6Hmga8iIiINCiAgICBpZiBub3QgdmFsdWU6DQogICAgICAgIHJldHVybiAiPGVtcHR5PiINCiAgICBpZiBsZW4odmFsdWUpIDw9IDg6DQogICAgICAgIHJldHVybiAiKiIgKiBsZW4odmFsdWUpDQogICAgcmV0dXJuIHZhbHVlWzo0XSArICIqIiAqIChsZW4odmFsdWUpIC0gOCkgKyB2YWx1ZVstNDpdDQoNCg0KZGVmIF9zZXR1cF9sb2dnaW5nKCkgLT4gTm9uZToNCiAgICAiIiLorr7nva7ml6Xlv5ciIiINCiAgICBsb2dfbGV2ZWxfc3RyID0gb3MuZW52aXJvbi5nZXQoIlRBQkxFX0xPR19MRVZFTCIsICJERUJVRyIpLnVwcGVyKCkNCiAgICBsb2dfbGV2ZWwgPSBnZXRhdHRyKGxvZ2dpbmcsIGxvZ19sZXZlbF9zdHIsIGxvZ2dpbmcuREVCVUcpDQogICAgDQogICAgIyDphY3nva7moLnml6Xlv5forrDlvZXlmagNCiAgICBsb2dnaW5nLmJhc2ljQ29uZmlnKA0KICAgICAgICBsZXZlbD1sb2dfbGV2ZWwsDQogICAgICAgIGZvcm1hdD0nJShhc2N0aW1lKXMgWyUobGV2ZWxuYW1lKXNdICUobmFtZSlzOiAlKG1lc3NhZ2UpcycsDQogICAgICAgIGhhbmRsZXJzPVsNCiAgICAgICAgICAgIGxvZ2dpbmcuU3RyZWFtSGFuZGxlcihzeXMuc3RkZXJyKQ0KICAgICAgICBdDQogICAgKQ0KICAgIA0KICAgICMg5aaC5p6c5oyH5a6a5LqG5pel5b+X5paH5Lu277yM5re75Yqg5paH5Lu25aSE55CG5ZmoDQogICAgbG9nX2ZpbGUgPSBvcy5lbnZpcm9uLmdldCgiVEFCTEVfTE9HX0ZJTEUiLCAiIikuc3RyaXAoKQ0KICAgIGlmIGxvZ19maWxlOg0KICAgICAgICB0cnk6DQogICAgICAgICAgICBmaWxlX2hhbmRsZXIgPSBsb2dnaW5nLkZpbGVIYW5kbGVyKGxvZ19maWxlLCBlbmNvZGluZz0ndXRmLTgnKQ0KICAgICAgICAgICAgZmlsZV9oYW5kbGVyLnNldExldmVsKGxvZ19sZXZlbCkNCiAgICAgICAgICAgIGZpbGVfaGFuZGxlci5zZXRGb3JtYXR0ZXIobG9nZ2luZy5Gb3JtYXR0ZXIoDQogICAgICAgICAgICAgICAgJyUoYXNjdGltZSlzIFslKGxldmVsbmFtZSlzXSAlKG5hbWUpczogJShtZXNzYWdlKXMnDQogICAgICAgICAgICApKQ0KICAgICAgICAgICAgbG9nZ2luZy5nZXRMb2dnZXIoKS5hZGRIYW5kbGVyKGZpbGVfaGFuZGxlcikNCiAgICAgICAgICAgIExPR0dFUi5pbmZvKCLml6Xlv5fmlofku7blt7LphY3nva46ICVzIiwgbG9nX2ZpbGUpDQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToNCiAgICAgICAgICAgIExPR0dFUi53YXJuaW5nKCLphY3nva7ml6Xlv5fmlofku7blpLHotKU6ICVzIiwgZSkNCg0KDQpkZWYgX2dldF9pbmZsdXhfY29uZmlnKCkgLT4gRGljdFtzdHIsIHN0cl06DQogICAgIiIi6I635Y+WSW5mbHV4RELphY3nva4iIiINCiAgICBjb25maWcgPSB7DQogICAgICAgICd1cmwnOiBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX1VSTCIsICIiKS5zdHJpcCgpLA0KICAgICAgICAnb3JnJzogb3MuZW52aXJvbi5nZXQoIklORkxVWF9PUkciLCAiIikuc3RyaXAoKSwNCiAgICAgICAgJ3Rva2VuJzogb3MuZW52aXJvbi5nZXQoIklORkxVWF9UT0tFTiIsICIiKS5zdHJpcCgpLA0KICAgICAgICAnYnVja2V0Jzogb3MuZW52aXJvbi5nZXQoIklORkxVWF9CVUNLRVQiLCAiUENNIikuc3RyaXAoKSwNCiAgICAgICAgJ21lYXN1cmVtZW50Jzogb3MuZW52aXJvbi5nZXQoIklORkxVWF9NRUFTVVJFTUVOVCIsICJQQ01fTWVhc3VyZW1lbnQiKS5zdHJpcCgpLA0KICAgIH0NCiAgICANCiAgICBMT0dHRVIuZGVidWcoDQogICAgICAgICJJbmZsdXhEQumFjee9rjogdXJsPSVzIG9yZz0lcyB0b2tlbj0lcyBidWNrZXQ9JXMgbWVhc3VyZW1lbnQ9JXMiLA0KICAgICAgICBjb25maWdbJ3VybCddIG9yICI8ZW1wdHk+IiwNCiAgICAgICAgY29uZmlnWydvcmcnXSBvciAiPGVtcHR5PiIsDQogICAgICAgIF9tYXNrX3NlY3JldChjb25maWdbJ3Rva2VuJ10pLA0KICAgICAgICBjb25maWdbJ2J1Y2tldCddLA0KICAgICAgICBjb25maWdbJ21lYXN1cmVtZW50J10sDQogICAgKQ0KICAgIA0KICAgIHJldHVybiBjb25maWcNCg0KDQpkZWYgX3BhcnNlX2V4cGVyaW1lbnRfdGltZXMoKSAtPiB0dXBsZVtPcHRpb25hbFtkYXRldGltZV0sIE9wdGlvbmFsW2RhdGV0aW1lXV06DQogICAgIiIi6Kej5p6Q5a6e6aqM5pe26Ze077yM5YmN56uv5Lyg5YWl5pys5Zyw5pe26Ze077yM6L2s5o2i5Li6VVRD55So5LqOSW5mbHV4RELmn6Xor6IiIiINCiAgICBmcm9tIGRhdGV0aW1lIGltcG9ydCB0aW1lem9uZSwgdGltZWRlbHRhDQogICAgDQogICAgc3RhcnRfc3RyID0gb3MuZW52aXJvbi5nZXQoIkVYUEVSSU1FTlRfU1RBUlQiLCAiIikuc3RyaXAoKQ0KICAgIGVuZF9zdHIgPSBvcy5lbnZpcm9uLmdldCgiRVhQRVJJTUVOVF9FTkQiLCAiIikuc3RyaXAoKQ0KICAgIA0KICAgIExPR0dFUi5kZWJ1Zygi5Y6f5aeL5pe26Ze05a2X56ym5LiyOiBTVEFSVD0lcywgRU5EPSVzIiwgc3RhcnRfc3RyLCBlbmRfc3RyKQ0KICAgIA0KICAgIHN0YXJ0X3RpbWU6IE9wdGlvbmFsW2RhdGV0aW1lXSA9IE5vbmUNCiAgICBlbmRfdGltZTogT3B0aW9uYWxbZGF0ZXRpbWVdID0gTm9uZQ0KICAgIA0KICAgIGlmIHN0YXJ0X3N0cjoNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgZm9yIGZtdCBpbiBbIiVZLSVtLSVkVCVIOiVNOiVTIiwgIiVZLSVtLSVkVCVIOiVNOiVTLiVmIl06DQogICAgICAgICAgICAgICAgdHJ5Og0KICAgICAgICAgICAgICAgICAgICBzdGFydF90aW1lID0gZGF0ZXRpbWUuc3RycHRpbWUoc3RhcnRfc3RyLCBmbXQpDQogICAgICAgICAgICAgICAgICAgICMg5pys5Zyw5pe26Ze0LTjlsI/ml7Y9VVRDDQogICAgICAgICAgICAgICAgICAgIHN0YXJ0X3RpbWUgPSBzdGFydF90aW1lIC0gdGltZWRlbHRhKGhvdXJzPTgpDQogICAgICAgICAgICAgICAgICAgIHN0YXJ0X3RpbWUgPSBzdGFydF90aW1lLnJlcGxhY2UodHppbmZvPXRpbWV6b25lLnV0YykNCiAgICAgICAgICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCLop6PmnpBTVEFSVDog5pys5ZywPSVzIOKGkiBVVEM9JXMiLCBzdGFydF9zdHIsIHN0YXJ0X3RpbWUpDQogICAgICAgICAgICAgICAgICAgIGJyZWFrDQogICAgICAgICAgICAgICAgZXhjZXB0IFZhbHVlRXJyb3I6DQogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlDQogICAgICAgICAgICBpZiBzdGFydF90aW1lIGlzIE5vbmU6DQogICAgICAgICAgICAgICAgTE9HR0VSLndhcm5pbmcoIuaXoOazleino+aekEVYUEVSSU1FTlRfU1RBUlQ6ICVzIiwgc3RhcnRfc3RyKQ0KICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgICAgICBMT0dHRVIuZXJyb3IoIuino+aekEVYUEVSSU1FTlRfU1RBUlTlpLHotKUgJyVzJzogJXMiLCBzdGFydF9zdHIsIGUpDQogICAgDQogICAgaWYgZW5kX3N0cjoNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgZm9yIGZtdCBpbiBbIiVZLSVtLSVkVCVIOiVNOiVTIiwgIiVZLSVtLSVkVCVIOiVNOiVTLiVmIl06DQogICAgICAgICAgICAgICAgdHJ5Og0KICAgICAgICAgICAgICAgICAgICBlbmRfdGltZSA9IGRhdGV0aW1lLnN0cnB0aW1lKGVuZF9zdHIsIGZtdCkNCiAgICAgICAgICAgICAgICAgICAgIyDmnKzlnLDml7bpl7QtOOWwj+aXtj1VVEMNCiAgICAgICAgICAgICAgICAgICAgZW5kX3RpbWUgPSBlbmRfdGltZSAtIHRpbWVkZWx0YShob3Vycz04KQ0KICAgICAgICAgICAgICAgICAgICBlbmRfdGltZSA9IGVuZF90aW1lLnJlcGxhY2UodHppbmZvPXRpbWV6b25lLnV0YykNCiAgICAgICAgICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCLop6PmnpBFTkQ6IOacrOWcsD0lcyDihpIgVVRDPSVzIiwgZW5kX3N0ciwgZW5kX3RpbWUpDQogICAgICAgICAgICAgICAgICAgIGJyZWFrDQogICAgICAgICAgICAgICAgZXhjZXB0IFZhbHVlRXJyb3I6DQogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlDQogICAgICAgICAgICBpZiBlbmRfdGltZSBpcyBOb25lOg0KICAgICAgICAgICAgICAgIExPR0dFUi53YXJuaW5nKCLml6Dms5Xop6PmnpBFWFBFUklNRU5UX0VORDogJXMiLCBlbmRfc3RyKQ0KICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgICAgICBMT0dHRVIuZXJyb3IoIuino+aekEVYUEVSSU1FTlRfRU5E5aSx6LSlICclcyc6ICVzIiwgZW5kX3N0ciwgZSkNCiAgICANCiAgICByZXR1cm4gc3RhcnRfdGltZSwgZW5kX3RpbWUNCg0KDQpkZWYgX3BhcnNlX3RpbWVfc2xvdChzbG90X3N0cjogc3RyKSAtPiBmbG9hdDoNCiAgICAiIiLop6PmnpDml7bpl7Tmp73lrZfnrKbkuLLkuLrlsI/ml7bmlbAiIiINCiAgICBpZiBub3Qgc2xvdF9zdHI6DQogICAgICAgIHJldHVybiAwLjANCiAgICANCiAgICBzbG90X3N0ciA9IHNsb3Rfc3RyLnN0cmlwKCkubG93ZXIoKQ0KICAgIA0KICAgIGlmIHNsb3Rfc3RyLmVuZHN3aXRoKCdoJyk6DQogICAgICAgIHRyeToNCiAgICAgICAgICAgIHJldHVybiBmbG9hdChzbG90X3N0cls6LTFdKQ0KICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoNCiAgICAgICAgICAgIHBhc3MNCiAgICANCiAgICB0cnk6DQogICAgICAgIHJldHVybiBmbG9hdChzbG90X3N0cikNCiAgICBleGNlcHQgVmFsdWVFcnJvcjoNCiAgICAgICAgcGFzcw0KICAgIA0KICAgIHJldHVybiAwLjANCg0KDQpkZWYgX3RpbWVfc2xvdHMoKSAtPiBMaXN0W3N0cl06DQogICAgcmF3ID0gb3MuZW52aXJvbi5nZXQoIlRBQkxFX1RJTUVfU0xPVFMiLCAiIikuc3RyaXAoKQ0KICAgIGlmIG5vdCByYXc6DQogICAgICAgICMg5qC55o2u5Zu+54mH77yM5pe26Ze05Yi75bqm5piv77yaMC41aCwgMWgsIDEuNWgsIDJoLCAyLjVoLCAzaCwgMy41aO+8iDfliJfvvIkNCiAgICAgICAgcmV0dXJuIFsiMC41aCIsICIxaCIsICIxLjVoIiwgIjJoIiwgIjIuNWgiLCAiM2giLCAiMy41aCJdDQogICAgc2xvdHMgPSBbc2xvdC5zdHJpcCgpIGZvciBzbG90IGluIHJhdy5zcGxpdCgiLCIpXQ0KICAgIHJldHVybiBbc2xvdCBmb3Igc2xvdCBpbiBzbG90cyBpZiBzbG90XQ0KDQoNCmRlZiBfZGVmYXVsdF9zZWN0aW9ucygpIC0+IExpc3RbRGljdFtzdHIsIEFueV1dOg0KICAgICMgbmFtZSAtPiByb3dzIHVuZGVybmVhdGjvvIhlbnRyaWVz77yJDQogICAgIyDmr4/kuKogZW50cnkg5a+55bqU5LiA5Liq5rWL6K+V6YOo5L2N77yM6ZyA6KaB5pig5bCE5YiwIEluZmx1eERCIOeahCBmaWVsZCDmiJYgdGFnDQogICAgcmV0dXJuIFsNCiAgICAgICAgeyJuYW1lIjogIuS4u+i9tOaJvyIsICJlbnRyaWVzIjogWw0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjMSIsICJmaWVsZCI6ICLkuLvovbTmib8jMSIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5Li76L205om/IzEifSwNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzIiLCAiZmllbGQiOiAi5Li76L205om/IzIiLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIkxTREFRIn0sICJyZXN1bHRfa2V5IjogIuS4u+i9tOaJvyMyIn0sDQogICAgICAgICAgICB7ImxhYmVsIjogIiMzIiwgImZpZWxkIjogIuS4u+i9tOaJvyMzIiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLkuLvovbTmib8jMyJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjNCIsICJmaWVsZCI6ICLkuLvovbTmib8jNCIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5Li76L205om/IzQifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLljYHlrZflpLQiLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzEiLCAiZmllbGQiOiAi5Y2B5a2X5aS0IzEiLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIkxTREFRIn0sICJyZXN1bHRfa2V5IjogIuWNgeWtl+WktCMxIn0sDQogICAgICAgICAgICB7ImxhYmVsIjogIiMyIiwgImZpZWxkIjogIuWNgeWtl+WktCMyIiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLljYHlrZflpLQjMiJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjMyIsICJmaWVsZCI6ICLljYHlrZflpLQjMyIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5Y2B5a2X5aS0IzMifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLlh4/pgJ/nrrHlsI/ovbTmib8iLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzHvvIjovpPlhaXms5XlhbDnq6/vvIkiLCAiZmllbGQiOiAi5YeP6YCf566x5bCP6L205om/MSIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5YeP6YCf566x5bCP6L205om/IzEifSwNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzIiLCAiZmllbGQiOiAi5YeP6YCf566x5bCP6L205om/IzIiLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIkxTREFRIn0sICJyZXN1bHRfa2V5IjogIuWHj+mAn+euseWwj+i9tOaJvyMyIn0sDQogICAgICAgIF19LA0KICAgICAgICB7Im5hbWUiOiAi5YeP6YCf566x5aSn6L205om/IiwgImVudHJpZXMiOiBbDQogICAgICAgICAgICB7ImxhYmVsIjogIiMz77yI5aSn56uv55uW56uv77yJIiwgImZpZWxkIjogIuWHj+mAn+euseWkp+i9tOaJvyMzIiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLlh4/pgJ/nrrHlpKfovbTmib8jMyJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjNCIsICJmaWVsZCI6ICLlh4/pgJ/nrrHlpKfovbTmib8jNCIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5YeP6YCf566x5aSn6L205om/IzQifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLmtqbmu5HmsrnmuKkiLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIiwgImZpZWxkIjogIm1lYW4iLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIua2pua7keayuea4qSJ9LCAicmVzdWx0X2tleSI6ICLmtqbmu5HmsrnmuKkifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLmtqbmu5HmsrnljosiLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiKFBzaSkiLCAiZmllbGQiOiAibWVhbiIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAi5ram5ruR5rK55Y6LIn0sICJyZXN1bHRfa2V5IjogIua2pua7keayueWOiyJ9LA0KICAgICAgICBdfSwNCiAgICBdDQoNCmRlZiBfcXVlcnlfbG9hZF9zdGF0dXNfdGltZWxpbmUoDQogICAgc3RhcnRfdGltZTogZGF0ZXRpbWUsDQogICAgZW5kX3RpbWU6IGRhdGV0aW1lLA0KICAgIGluZmx1eF91cmw6IHN0ciwNCiAgICBpbmZsdXhfb3JnOiBzdHIsDQogICAgaW5mbHV4X3Rva2VuOiBzdHIsDQogICAgaW5mbHV4X2J1Y2tldDogc3RyLA0KICAgIGluZmx1eF9tZWFzdXJlbWVudDogc3RyLA0KKSAtPiBMaXN0W0RpY3Rbc3RyLCBBbnldXToNCiAgICAiIiLmn6Xor6LmlbTkuKrlrp7pqozmnJ/pl7TnmoRsb2FkX3N0YXR1c+aXtumXtOe6v+aVsOaNriIiIg0KICAgIHRyeToNCiAgICAgICAgZnJvbSBpbmZsdXhkYl9jbGllbnQgaW1wb3J0IEluZmx1eERCQ2xpZW50DQogICAgICAgIGltcG9ydCBwYW5kYXMgYXMgcGQNCiAgICAgICAgaW1wb3J0IHdhcm5pbmdzDQogICAgICAgIGZyb20gaW5mbHV4ZGJfY2xpZW50LmNsaWVudC53YXJuaW5ncyBpbXBvcnQgTWlzc2luZ1Bpdm90RnVuY3Rpb24NCiAgICBleGNlcHQgSW1wb3J0RXJyb3IgYXMgZToNCiAgICAgICAgTE9HR0VSLmVycm9yKCJJbmZsdXhEQuWuouaIt+err+WvvOWFpeWksei0pTogJXPvvIzor7flronoo4U6IHBpcCBpbnN0YWxsIGluZmx1eGRiLWNsaWVudCBwYW5kYXMiLCBlKQ0KICAgICAgICByZXR1cm4gW10NCg0KICAgIHRyeToNCiAgICAgICAgY2xpZW50ID0gSW5mbHV4REJDbGllbnQodXJsPWluZmx1eF91cmwsIG9yZz1pbmZsdXhfb3JnLCB0b2tlbj1pbmZsdXhfdG9rZW4pDQogICAgICAgIHF1ZXJ5X2FwaSA9IGNsaWVudC5xdWVyeV9hcGkoKQ0KDQogICAgICAgICMg56Gu5L+d5L2/55SoVVRD5pe26Ze05qC85byP5p+l6K+iDQogICAgICAgIHN0YXJ0X3JmYyA9IHN0YXJ0X3RpbWUuc3RyZnRpbWUoJyVZLSVtLSVkVCVIOiVNOiVTWicpDQogICAgICAgIGVuZF9yZmMgPSBlbmRfdGltZS5zdHJmdGltZSgnJVktJW0tJWRUJUg6JU06JVNaJykNCiAgICAgICAgDQogICAgICAgIExPR0dFUi5kZWJ1Zygi5p+l6K+ibG9hZF9zdGF0dXPml7bpl7TojIPlm7Q6ICVzIOWIsCAlcyIsIHN0YXJ0X3JmYywgZW5kX3JmYykNCg0KICAgICAgICBmbHV4ID0gZicnJw0KZnJvbShidWNrZXQ6ICJ7aW5mbHV4X2J1Y2tldH0iKQ0KICB8PiByYW5nZShzdGFydDoge3N0YXJ0X3JmY30sIHN0b3A6IHtlbmRfcmZjfSkNCiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsiX21lYXN1cmVtZW50Il0gPT0gIntpbmZsdXhfbWVhc3VyZW1lbnR9IikNCiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsiZGF0YV90eXBlIl0gPT0gIkJyZWFrZXIiKQ0KICB8PiBmaWx0ZXIoZm46IChyKSA9PiByWyJfZmllbGQiXSA9PSAibG9hZF9zdGF0dXMiKQ0KICB8PiBzb3J0KGNvbHVtbnM6IFsiX3RpbWUiXSkNCiAgfD4geWllbGQobmFtZTogImxvYWRfc3RhdHVzX3RpbWVsaW5lIikNCicnJy5zdHJpcCgpDQoNCiAgICAgICAgTE9HR0VSLmRlYnVnKCJMb2FkIHN0YXR1cyB0aW1lbGluZSBxdWVyeTpcbiVzIiwgZmx1eCkNCg0KICAgICAgICB3aXRoIHdhcm5pbmdzLmNhdGNoX3dhcm5pbmdzKCk6DQogICAgICAgICAgICB3YXJuaW5ncy5zaW1wbGVmaWx0ZXIoImlnbm9yZSIsIE1pc3NpbmdQaXZvdEZ1bmN0aW9uKQ0KICAgICAgICAgICAgZnJhbWVzID0gcXVlcnlfYXBpLnF1ZXJ5X2RhdGFfZnJhbWUoZmx1eCkNCiAgICAgICAgDQogICAgICAgIGlmIGlzaW5zdGFuY2UoZnJhbWVzLCBsaXN0KToNCiAgICAgICAgICAgIGRmID0gcGQuY29uY2F0KGZyYW1lcywgaWdub3JlX2luZGV4PVRydWUpIGlmIGZyYW1lcyBlbHNlIHBkLkRhdGFGcmFtZSgpDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBkZiA9IGZyYW1lcw0KDQogICAgICAgIGlmIGRmLmVtcHR5IG9yICdfdmFsdWUnIG5vdCBpbiBkZi5jb2x1bW5zIG9yICdfdGltZScgbm90IGluIGRmLmNvbHVtbnM6DQogICAgICAgICAgICBMT0dHRVIud2FybmluZygiTm8gbG9hZF9zdGF0dXMgdGltZWxpbmUgZGF0YSBmb3VuZCIpDQogICAgICAgICAgICByZXR1cm4gW10NCg0KICAgICAgICAjIOi9rOaNouS4uuaXtumXtOe6v+aVsOaNru+8jOS/neaMgVVUQ+aXtuWMug0KICAgICAgICBmcm9tIGRhdGV0aW1lIGltcG9ydCB0aW1lem9uZQ0KICAgICAgICB0aW1lbGluZSA9IFtdDQogICAgICAgIGZvciBfLCByb3cgaW4gZGYuaXRlcnJvd3MoKToNCiAgICAgICAgICAgIHRpbWVfb2JqID0gcGQudG9fZGF0ZXRpbWUocm93WydfdGltZSddKQ0KICAgICAgICAgICAgIyDnoa7kv53ovazmjaLkuLpVVEPml7bljLrnmoRkYXRldGltZeWvueixoQ0KICAgICAgICAgICAgaWYgaGFzYXR0cih0aW1lX29iaiwgJ3R6X2xvY2FsaXplJyk6DQogICAgICAgICAgICAgICAgaWYgdGltZV9vYmoudHogaXMgTm9uZToNCiAgICAgICAgICAgICAgICAgICAgdGltZV9vYmogPSB0aW1lX29iai50el9sb2NhbGl6ZSh0aW1lem9uZS51dGMpDQogICAgICAgICAgICAgICAgZWxzZToNCiAgICAgICAgICAgICAgICAgICAgdGltZV9vYmogPSB0aW1lX29iai50el9jb252ZXJ0KHRpbWV6b25lLnV0YykNCiAgICAgICAgICAgIA0KICAgICAgICAgICAgaWYgaGFzYXR0cih0aW1lX29iaiwgJ3RvX3B5ZGF0ZXRpbWUnKToNCiAgICAgICAgICAgICAgICB0aW1lX29iaiA9IHRpbWVfb2JqLnRvX3B5ZGF0ZXRpbWUoKQ0KICAgICAgICAgICAgICAgIA0KICAgICAgICAgICAgdGltZWxpbmUuYXBwZW5kKHsNCiAgICAgICAgICAgICAgICAndGltZSc6IHRpbWVfb2JqLA0KICAgICAgICAgICAgICAgICdsb2FkX3N0YXR1cyc6IGZsb2F0KHJvd1snX3ZhbHVlJ10pDQogICAgICAgICAgICB9KQ0KDQogICAgICAgIExPR0dFUi5pbmZvKCJMb2FkIHN0YXR1cyB0aW1lbGluZTogJWQgZGF0YSBwb2ludHMgZnJvbSAlcyB0byAlcyIsIA0KICAgICAgICAgICAgICAgICAgIGxlbih0aW1lbGluZSksIHN0YXJ0X3RpbWUsIGVuZF90aW1lKQ0KICAgICAgICANCiAgICAgICAgIyDosIPor5XvvJrmo4Dmn6Xml7bpl7Tlr7nosaHnsbvlnosNCiAgICAgICAgaWYgdGltZWxpbmU6DQogICAgICAgICAgICBmaXJzdF90aW1lID0gdGltZWxpbmVbMF1bJ3RpbWUnXQ0KICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCJUaW1lbGluZSBmaXJzdCB0aW1lOiAlcyAodHlwZTogJXMsIHR6aW5mbzogJXMpIiwgDQogICAgICAgICAgICAgICAgICAgICAgICBmaXJzdF90aW1lLCB0eXBlKGZpcnN0X3RpbWUpLCBnZXRhdHRyKGZpcnN0X3RpbWUsICd0emluZm8nLCBOb25lKSkNCiAgICAgICAgTE9HR0VSLmRlYnVnKCJzdGFydF90aW1lOiAlcyAodHlwZTogJXMsIHR6aW5mbzogJXMpIiwgDQogICAgICAgICAgICAgICAgICAgIHN0YXJ0X3RpbWUsIHR5cGUoc3RhcnRfdGltZSksIGdldGF0dHIoc3RhcnRfdGltZSwgJ3R6aW5mbycsIE5vbmUpKQ0KICAgICAgICBMT0dHRVIuZGVidWcoImVuZF90aW1lOiAlcyAodHlwZTogJXMsIHR6aW5mbzogJXMpIiwgDQogICAgICAgICAgICAgICAgICAgIGVuZF90aW1lLCB0eXBlKGVuZF90aW1lKSwgZ2V0YXR0cihlbmRfdGltZSwgJ3R6aW5mbycsIE5vbmUpKQ0KICAgICAgICANCiAgICAgICAgcmV0dXJuIHRpbWVsaW5lDQoNCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgIExPR0dFUi5lcnJvcigiRXJyb3IgcXVlcnlpbmcgbG9hZF9zdGF0dXMgdGltZWxpbmU6ICVzIiwgZSkNCiAgICAgICAgcmV0dXJuIFtdDQogICAgZmluYWxseToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgY2xpZW50LmNsb3NlKCkNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoNCiAgICAgICAgICAgIHBhc3MNCg0KDQpkZWYgX2NhbGN1bGF0ZV9lZmZlY3RpdmVfdGltZV9wb2ludHMoDQogICAgc3RhcnRfdGltZTogZGF0ZXRpbWUsDQogICAgZW5kX3RpbWU6IGRhdGV0aW1lLA0KICAgIHRpbWVfc2xvdHM6IExpc3Rbc3RyXSwNCiAgICBpbmZsdXhfY29uZmlnOiBEaWN0W3N0ciwgc3RyXQ0KKSAtPiBEaWN0W3N0ciwgT3B0aW9uYWxbZGF0ZXRpbWVdXToNCiAgICAiIiLorqHnrpfln7rkuo7mnInmlYjov5DooYzml7bpl7TntK/orqHnmoTnnJ/lrp7ml7bpl7TngrkiIiINCiAgICANCiAgICAjIDEuIOiOt+WPlmxvYWRfc3RhdHVz5pe26Ze057q/DQogICAgdGltZWxpbmUgPSBfcXVlcnlfbG9hZF9zdGF0dXNfdGltZWxpbmUoDQogICAgICAgIHN0YXJ0X3RpbWUsIGVuZF90aW1lLA0KICAgICAgICBpbmZsdXhfY29uZmlnWyd1cmwnXSwgaW5mbHV4X2NvbmZpZ1snb3JnJ10sIGluZmx1eF9jb25maWdbJ3Rva2VuJ10sDQogICAgICAgIGluZmx1eF9jb25maWdbJ2J1Y2tldCddLCBpbmZsdXhfY29uZmlnWydtZWFzdXJlbWVudCddDQogICAgKQ0KICAgIA0KICAgIGlmIG5vdCB0aW1lbGluZToNCiAgICAgICAgTE9HR0VSLndhcm5pbmcoIk5vIGxvYWRfc3RhdHVzIHRpbWVsaW5lIGRhdGEsIGZhbGxiYWNrIHRvIG9yaWdpbmFsIHRpbWUgY2FsY3VsYXRpb24iKQ0KICAgICAgICAjIOWbnumAgOWIsOWOn+Wni+aXtumXtOiuoeeulw0KICAgICAgICByZXN1bHQgPSB7fQ0KICAgICAgICBmb3Igc2xvdF9zdHIgaW4gdGltZV9zbG90czoNCiAgICAgICAgICAgIHNsb3RfaG91cnMgPSBfcGFyc2VfdGltZV9zbG90KHNsb3Rfc3RyKQ0KICAgICAgICAgICAgcmVzdWx0W3Nsb3Rfc3RyXSA9IHN0YXJ0X3RpbWUgKyB0aW1lZGVsdGEoaG91cnM9c2xvdF9ob3VycykNCiAgICAgICAgcmV0dXJuIHJlc3VsdA0KICAgIA0KICAgICMgMi4g6K6h566X5pyJ5pWI6L+Q6KGM5pe26Ze05q61DQogICAgZWZmZWN0aXZlX3BlcmlvZHMgPSBbXQ0KICAgIGN1cnJlbnRfcGVyaW9kX3N0YXJ0ID0gTm9uZQ0KICAgIA0KICAgIGZvciBpLCBwb2ludCBpbiBlbnVtZXJhdGUodGltZWxpbmUpOg0KICAgICAgICBpZiBwb2ludFsnbG9hZF9zdGF0dXMnXSA9PSAxLjA6DQogICAgICAgICAgICBpZiBjdXJyZW50X3BlcmlvZF9zdGFydCBpcyBOb25lOg0KICAgICAgICAgICAgICAgIGN1cnJlbnRfcGVyaW9kX3N0YXJ0ID0gcG9pbnRbJ3RpbWUnXQ0KICAgICAgICBlbHNlOiAgIyBsb2FkX3N0YXR1cyAhPSAxLjANCiAgICAgICAgICAgIGlmIGN1cnJlbnRfcGVyaW9kX3N0YXJ0IGlzIG5vdCBOb25lOg0KICAgICAgICAgICAgICAgIGVmZmVjdGl2ZV9wZXJpb2RzLmFwcGVuZCh7DQogICAgICAgICAgICAgICAgICAgICdzdGFydCc6IGN1cnJlbnRfcGVyaW9kX3N0YXJ0LA0KICAgICAgICAgICAgICAgICAgICAnZW5kJzogcG9pbnRbJ3RpbWUnXSwNCiAgICAgICAgICAgICAgICAgICAgJ2R1cmF0aW9uX2hvdXJzJzogKHBvaW50Wyd0aW1lJ10gLSBjdXJyZW50X3BlcmlvZF9zdGFydCkudG90YWxfc2Vjb25kcygpIC8gMzYwMC4wDQogICAgICAgICAgICAgICAgfSkNCiAgICAgICAgICAgICAgICBjdXJyZW50X3BlcmlvZF9zdGFydCA9IE5vbmUNCiAgICANCiAgICAjIOWkhOeQhuacgOWQjuS4gOS4quWRqOacn++8iOWmguaenOWunumqjOe7k+adn+aXtuS7jeWcqOi/kOihjO+8iQ0KICAgIGlmIGN1cnJlbnRfcGVyaW9kX3N0YXJ0IGlzIG5vdCBOb25lOg0KICAgICAgICBlZmZlY3RpdmVfcGVyaW9kcy5hcHBlbmQoew0KICAgICAgICAgICAgJ3N0YXJ0JzogY3VycmVudF9wZXJpb2Rfc3RhcnQsDQogICAgICAgICAgICAnZW5kJzogZW5kX3RpbWUsDQogICAgICAgICAgICAnZHVyYXRpb25faG91cnMnOiAoZW5kX3RpbWUgLSBjdXJyZW50X3BlcmlvZF9zdGFydCkudG90YWxfc2Vjb25kcygpIC8gMzYwMC4wDQogICAgICAgIH0pDQogICAgDQogICAgdG90YWxfZWZmZWN0aXZlX2hvdXJzID0gc3VtKHBlcmlvZFsnZHVyYXRpb25faG91cnMnXSBmb3IgcGVyaW9kIGluIGVmZmVjdGl2ZV9wZXJpb2RzKQ0KICAgIExPR0dFUi5pbmZvKCJFZmZlY3RpdmUgcnVubmluZyBwZXJpb2RzOiAlZCBwZXJpb2RzLCB0b3RhbCAlLjNmIGhvdXJzIiwgDQogICAgICAgICAgICAgICBsZW4oZWZmZWN0aXZlX3BlcmlvZHMpLCB0b3RhbF9lZmZlY3RpdmVfaG91cnMpDQogICAgDQogICAgZm9yIHBlcmlvZCBpbiBlZmZlY3RpdmVfcGVyaW9kczoNCiAgICAgICAgTE9HR0VSLmRlYnVnKCJFZmZlY3RpdmUgcGVyaW9kOiAlcyDihpIgJXMgKCUuM2YgaG91cnMpIiwNCiAgICAgICAgICAgICAgICAgICAgcGVyaW9kWydzdGFydCddLnN0cmZ0aW1lKCclSDolTTolUycpLA0KICAgICAgICAgICAgICAgICAgICBwZXJpb2RbJ2VuZCddLnN0cmZ0aW1lKCclSDolTTolUycpLA0KICAgICAgICAgICAgICAgICAgICBwZXJpb2RbJ2R1cmF0aW9uX2hvdXJzJ10pDQogICAgDQogICAgIyAzLiDorqHnrpfmr4/kuKrml7bpl7Tmp73lr7nlupTnmoTnnJ/lrp7ml7bpl7TngrkNCiAgICBlZmZlY3RpdmVfdGltZV9wb2ludHMgPSB7fQ0KICAgIA0KICAgIGZvciBzbG90X3N0ciBpbiB0aW1lX3Nsb3RzOg0KICAgICAgICB0YXJnZXRfZWZmZWN0aXZlX2hvdXJzID0gX3BhcnNlX3RpbWVfc2xvdChzbG90X3N0cikNCiAgICAgICAgDQogICAgICAgIGlmIHRhcmdldF9lZmZlY3RpdmVfaG91cnMgPD0gMDoNCiAgICAgICAgICAgIGVmZmVjdGl2ZV90aW1lX3BvaW50c1tzbG90X3N0cl0gPSBOb25lDQogICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICANCiAgICAgICAgaWYgdGFyZ2V0X2VmZmVjdGl2ZV9ob3VycyA+IHRvdGFsX2VmZmVjdGl2ZV9ob3VyczoNCiAgICAgICAgICAgIExPR0dFUi53YXJuaW5nKCJUYXJnZXQgZWZmZWN0aXZlIHRpbWUgJS4zZmggZXhjZWVkcyB0b3RhbCBlZmZlY3RpdmUgdGltZSAlLjNmaCBmb3Igc2xvdCAlcyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIHRhcmdldF9lZmZlY3RpdmVfaG91cnMsIHRvdGFsX2VmZmVjdGl2ZV9ob3Vycywgc2xvdF9zdHIpDQogICAgICAgICAgICBlZmZlY3RpdmVfdGltZV9wb2ludHNbc2xvdF9zdHJdID0gTm9uZQ0KICAgICAgICAgICAgY29udGludWUNCiAgICAgICAgDQogICAgICAgICMg5Zyo5pyJ5pWI5pe26Ze05q615Lit5p+l5om+57Sv6K6h6L+Q6KGMdGFyZ2V0X2VmZmVjdGl2ZV9ob3Vyc+Wwj+aXtueahOaXtumXtOeCuQ0KICAgICAgICBjdW11bGF0aXZlX2hvdXJzID0gMC4wDQogICAgICAgIHRhcmdldF90aW1lX3BvaW50ID0gTm9uZQ0KICAgICAgICANCiAgICAgICAgZm9yIHBlcmlvZCBpbiBlZmZlY3RpdmVfcGVyaW9kczoNCiAgICAgICAgICAgIHBlcmlvZF9kdXJhdGlvbiA9IHBlcmlvZFsnZHVyYXRpb25faG91cnMnXQ0KICAgICAgICAgICAgDQogICAgICAgICAgICBpZiBjdW11bGF0aXZlX2hvdXJzICsgcGVyaW9kX2R1cmF0aW9uID49IHRhcmdldF9lZmZlY3RpdmVfaG91cnM6DQogICAgICAgICAgICAgICAgIyDnm67moIfml7bpl7TngrnlnKjov5nkuKrlkajmnJ/lhoUNCiAgICAgICAgICAgICAgICByZW1haW5pbmdfaG91cnMgPSB0YXJnZXRfZWZmZWN0aXZlX2hvdXJzIC0gY3VtdWxhdGl2ZV9ob3Vycw0KICAgICAgICAgICAgICAgIHRhcmdldF90aW1lX3BvaW50ID0gcGVyaW9kWydzdGFydCddICsgdGltZWRlbHRhKGhvdXJzPXJlbWFpbmluZ19ob3VycykNCiAgICAgICAgICAgICAgICBicmVhaw0KICAgICAgICAgICAgZWxzZToNCiAgICAgICAgICAgICAgICBjdW11bGF0aXZlX2hvdXJzICs9IHBlcmlvZF9kdXJhdGlvbg0KICAgICAgICANCiAgICAgICAgZWZmZWN0aXZlX3RpbWVfcG9pbnRzW3Nsb3Rfc3RyXSA9IHRhcmdldF90aW1lX3BvaW50DQogICAgICAgIA0KICAgICAgICBpZiB0YXJnZXRfdGltZV9wb2ludDoNCiAgICAgICAgICAgIExPR0dFUi5pbmZvKCJTbG90ICVzOiBlZmZlY3RpdmUgJS4zZmgg4oaSIGFjdHVhbCB0aW1lICVzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgc2xvdF9zdHIsIHRhcmdldF9lZmZlY3RpdmVfaG91cnMsIHRhcmdldF90aW1lX3BvaW50LnN0cmZ0aW1lKCclSDolTTolUycpKQ0KICAgICAgICBlbHNlOg0KICAgICAgICAgICAgTE9HR0VSLndhcm5pbmcoIkNvdWxkIG5vdCBjYWxjdWxhdGUgZWZmZWN0aXZlIHRpbWUgcG9pbnQgZm9yIHNsb3QgJXMiLCBzbG90X3N0cikNCiAgICANCiAgICByZXR1cm4gZWZmZWN0aXZlX3RpbWVfcG9pbnRzDQoNCg0KZGVmIF9xdWVyeV9pbmZsdXhkYl9yYW5nZV93aXRoX2xvYWRfc3RhdHVzKA0KICAgIGZpZWxkX25hbWU6IHN0ciwNCiAgICBzdGFydF90aW1lOiBkYXRldGltZSwNCiAgICBlbmRfdGltZTogZGF0ZXRpbWUsDQogICAgaW5mbHV4X3VybDogc3RyLA0KICAgIGluZmx1eF9vcmc6IHN0ciwNCiAgICBpbmZsdXhfdG9rZW46IHN0ciwNCiAgICBpbmZsdXhfYnVja2V0OiBzdHIsDQogICAgaW5mbHV4X21lYXN1cmVtZW50OiBzdHIsDQogICAgZmlsdGVyczogT3B0aW9uYWxbRGljdFtzdHIsIHN0cl1dID0gTm9uZSwNCikgLT4gT3B0aW9uYWxbZmxvYXRdOg0KICAgICIiIuafpeivoiBJbmZsdXhEQiDojrflj5bmjIflrprlrZfmrrXlnKjml7bpl7TojIPlm7TlhoXnmoTlubPlnYflgLzvvIjku4XlvZMgbG9hZF9zdGF0dXMgPSAxIOaXtu+8iSIiIg0KICAgIHRyeToNCiAgICAgICAgZnJvbSBpbmZsdXhkYl9jbGllbnQgaW1wb3J0IEluZmx1eERCQ2xpZW50DQogICAgICAgIGltcG9ydCBwYW5kYXMgYXMgcGQNCiAgICAgICAgaW1wb3J0IHdhcm5pbmdzDQogICAgICAgIGZyb20gaW5mbHV4ZGJfY2xpZW50LmNsaWVudC53YXJuaW5ncyBpbXBvcnQgTWlzc2luZ1Bpdm90RnVuY3Rpb24NCiAgICBleGNlcHQgSW1wb3J0RXJyb3I6DQogICAgICAgIExPR0dFUi53YXJuaW5nKCJJbmZsdXhEQiBjbGllbnQgbm90IGF2YWlsYWJsZSwgc2tpcCBxdWVyeSBmb3IgZmllbGQ9JXMiLCBmaWVsZF9uYW1lKQ0KICAgICAgICByZXR1cm4gTm9uZQ0KDQogICAgdHJ5Og0KICAgICAgICBjbGllbnQgPSBJbmZsdXhEQkNsaWVudCh1cmw9aW5mbHV4X3VybCwgb3JnPWluZmx1eF9vcmcsIHRva2VuPWluZmx1eF90b2tlbikNCiAgICAgICAgcXVlcnlfYXBpID0gY2xpZW50LnF1ZXJ5X2FwaSgpDQoNCiAgICAgICAgIyDnoa7kv53kvb/nlKhVVEPml7bpl7TmoLzlvI8NCiAgICAgICAgc3RhcnRfcmZjID0gc3RhcnRfdGltZS5zdHJmdGltZSgnJVktJW0tJWRUJUg6JU06JVNaJykNCiAgICAgICAgZW5kX3JmYyA9IGVuZF90aW1lLnN0cmZ0aW1lKCclWS0lbS0lZFQlSDolTTolU1onKQ0KICAgICAgICANCiAgICAgICAgTE9HR0VSLmRlYnVnKCLmn6Xor6LlrZfmrrUgJXMg5pe26Ze06IyD5Zu0OiAlcyDliLAgJXMiLCBmaWVsZF9uYW1lLCBzdGFydF9yZmMsIGVuZF9yZmMpDQoNCiAgICAgICAgIyDmnoTlu7rov4fmu6TmnaHku7YNCiAgICAgICAgdGFnX2ZpbHRlcnMgPSAiIg0KICAgICAgICBpZiBmaWx0ZXJzOg0KICAgICAgICAgICAgZm9yIGtleSwgdmFsdWUgaW4gZmlsdGVycy5pdGVtcygpOg0KICAgICAgICAgICAgICAgIHRhZ19maWx0ZXJzICs9IGYnXG4gIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIntrZXl9Il0gPT0gInt2YWx1ZX0iKScNCg0KICAgICAgICAjIOWvueS6jueOr+Wig+a4qeW6pu+8jOWPluWFqOmDqOmdnjDmlbDmja7nmoTlnYflgLzvvJvlhbbku5blrZfmrrXku43pnIBsb2FkX3N0YXR1cz0x562b6YCJDQogICAgICAgIGlmIGZpZWxkX25hbWUgPT0gIueOr+Wig+a4qeW6piI6DQogICAgICAgICAgICBmbHV4ID0gZicnJw0KZnJvbShidWNrZXQ6ICJ7aW5mbHV4X2J1Y2tldH0iKQ0KICB8PiByYW5nZShzdGFydDoge3N0YXJ0X3JmY30sIHN0b3A6IHtlbmRfcmZjfSkNCiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsiX21lYXN1cmVtZW50Il0gPT0gIntpbmZsdXhfbWVhc3VyZW1lbnR9IikNCiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsiX2ZpZWxkIl0gPT0gIntmaWVsZF9uYW1lfSIpDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl92YWx1ZSJdICE9IDAuMCl7dGFnX2ZpbHRlcnN9DQogIHw+IG1lYW4oKQ0KICB8PiB5aWVsZChuYW1lOiAibWVhbl9ub25femVybyIpDQonJycuc3RyaXAoKQ0KICAgICAgICBlbHNlOg0KICAgICAgICAgICAgZmx1eCA9IGYnJycNCmZyb20oYnVja2V0OiAie2luZmx1eF9idWNrZXR9IikNCiAgfD4gcmFuZ2Uoc3RhcnQ6IHtzdGFydF9yZmN9LCBzdG9wOiB7ZW5kX3JmY30pDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9tZWFzdXJlbWVudCJdID09ICJ7aW5mbHV4X21lYXN1cmVtZW50fSIpDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9maWVsZCJdID09ICJ7ZmllbGRfbmFtZX0iKXt0YWdfZmlsdGVyc30NCiAgfD4gbWVhbigpDQogIHw+IHlpZWxkKG5hbWU6ICJtZWFuX3RlbXBlcmF0dXJlX2RhdGEiKQ0KJycnLnN0cmlwKCkNCg0KICAgICAgICBMT0dHRVIuZGVidWcoIkZsdXjmn6Xor6Lor63lj6UgKHJhbmdlKTpcbiVzIiwgZmx1eCkNCg0KICAgICAgICB3aXRoIHdhcm5pbmdzLmNhdGNoX3dhcm5pbmdzKCk6DQogICAgICAgICAgICB3YXJuaW5ncy5zaW1wbGVmaWx0ZXIoImlnbm9yZSIsIE1pc3NpbmdQaXZvdEZ1bmN0aW9uKQ0KICAgICAgICAgICAgZnJhbWVzID0gcXVlcnlfYXBpLnF1ZXJ5X2RhdGFfZnJhbWUoZmx1eCkNCiAgICAgICAgDQogICAgICAgIGlmIGlzaW5zdGFuY2UoZnJhbWVzLCBsaXN0KToNCiAgICAgICAgICAgIGRmID0gcGQuY29uY2F0KGZyYW1lcywgaWdub3JlX2luZGV4PVRydWUpIGlmIGZyYW1lcyBlbHNlIHBkLkRhdGFGcmFtZSgpDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBkZiA9IGZyYW1lcw0KDQogICAgICAgIGlmIGRmLmVtcHR5IG9yICdfdmFsdWUnIG5vdCBpbiBkZi5jb2x1bW5zOg0KICAgICAgICAgICAgaWYgZmllbGRfbmFtZSA9PSAi546v5aKD5rip5bqmIjoNCiAgICAgICAgICAgICAgICBMT0dHRVIuZGVidWcoIk5vIHZhbGlkIHJhbmdlIGRhdGEgZm91bmQgZm9yIGZpZWxkPSVzIChub24temVybyBkYXRhKSIsIGZpZWxkX25hbWUpDQogICAgICAgICAgICBlbHNlOg0KICAgICAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygiTm8gdmFsaWQgcmFuZ2UgZGF0YSBmb3VuZCBmb3IgZmllbGQ9JXMiLCBmaWVsZF9uYW1lKQ0KICAgICAgICAgICAgcmV0dXJuIE5vbmUNCiAgICAgICAgICAgIA0KICAgICAgICBtZWFuX3ZhbHVlID0gZGZbJ192YWx1ZSddLmlsb2NbMF0NCiAgICAgICAgaWYgcGQuaXNuYShtZWFuX3ZhbHVlKToNCiAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygiTWVhbiB2YWx1ZSBpcyBOYU4gZm9yIGZpZWxkPSVzIiwgZmllbGRfbmFtZSkNCiAgICAgICAgICAgIHJldHVybiBOb25lDQoNCiAgICAgICAgdmFsdWUgPSBmbG9hdChtZWFuX3ZhbHVlKQ0KICAgICAgICBpZiBmaWVsZF9uYW1lID09ICLnjq/looPmuKnluqYiOg0KICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCJGaWVsZD0lcyByYW5nZV9tZWFuX3ZhbHVlPSUuM2YgKG5vbi16ZXJvIGRhdGEpIiwgZmllbGRfbmFtZSwgdmFsdWUpDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBMT0dHRVIuZGVidWcoIkZpZWxkPSVzIHJhbmdlX21lYW5fdmFsdWU9JS4zZiIsIGZpZWxkX25hbWUsIHZhbHVlKQ0KICAgICAgICByZXR1cm4gdmFsdWUNCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgIExPR0dFUi5lcnJvcigiRXJyb3IgcXVlcnlpbmcgSW5mbHV4REIgcmFuZ2UgZm9yIGZpZWxkPSVzOiAlcyIsIGZpZWxkX25hbWUsIGUpDQogICAgICAgIHJldHVybiBOb25lDQogICAgZmluYWxseToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgY2xpZW50LmNsb3NlKCkNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoNCiAgICAgICAgICAgIHBhc3MNCg0KDQpkZWYgX3F1ZXJ5X2luZmx1eGRiX3dpdGhfbG9hZF9zdGF0dXMoDQogICAgZmllbGRfbmFtZTogc3RyLA0KICAgIHRhcmdldF90aW1lOiBkYXRldGltZSwNCiAgICBpbmZsdXhfdXJsOiBzdHIsDQogICAgaW5mbHV4X29yZzogc3RyLA0KICAgIGluZmx1eF90b2tlbjogc3RyLA0KICAgIGluZmx1eF9idWNrZXQ6IHN0ciwNCiAgICBpbmZsdXhfbWVhc3VyZW1lbnQ6IHN0ciwNCiAgICBmaWx0ZXJzOiBPcHRpb25hbFtEaWN0W3N0ciwgc3RyXV0gPSBOb25lLA0KKSAtPiBPcHRpb25hbFtmbG9hdF06DQogICAgIiIi5p+l6K+iIEluZmx1eERCIOiOt+WPluaMh+WumuWtl+auteWcqOaMh+WumuaXtumXtOeCueeahOeerOaXtuWAvO+8iOS7heW9kyBsb2FkX3N0YXR1cyA9IDEg5pe277yJIiIiDQogICAgdHJ5Og0KICAgICAgICBmcm9tIGluZmx1eGRiX2NsaWVudCBpbXBvcnQgSW5mbHV4REJDbGllbnQNCiAgICAgICAgaW1wb3J0IHBhbmRhcyBhcyBwZA0KICAgICAgICBpbXBvcnQgd2FybmluZ3MNCiAgICAgICAgZnJvbSBpbmZsdXhkYl9jbGllbnQuY2xpZW50Lndhcm5pbmdzIGltcG9ydCBNaXNzaW5nUGl2b3RGdW5jdGlvbg0KICAgIGV4Y2VwdCBJbXBvcnRFcnJvcjoNCiAgICAgICAgTE9HR0VSLndhcm5pbmcoIkluZmx1eERCIGNsaWVudCBub3QgYXZhaWxhYmxlLCBza2lwIHF1ZXJ5IGZvciBmaWVsZD0lcyIsIGZpZWxkX25hbWUpDQogICAgICAgIHJldHVybiBOb25lDQoNCiAgICB0cnk6DQogICAgICAgIGNsaWVudCA9IEluZmx1eERCQ2xpZW50KHVybD1pbmZsdXhfdXJsLCBvcmc9aW5mbHV4X29yZywgdG9rZW49aW5mbHV4X3Rva2VuKQ0KICAgICAgICBxdWVyeV9hcGkgPSBjbGllbnQucXVlcnlfYXBpKCkNCg0KICAgICAgICAjIOehruS/neS9v+eUqFVUQ+aXtumXtA0KICAgICAgICB0YXJnZXRfdGltZV9yZmMgPSB0YXJnZXRfdGltZS5zdHJmdGltZSgnJVktJW0tJWRUJUg6JU06JVNaJykNCiAgICAgICAgDQogICAgICAgIExPR0dFUi5kZWJ1ZygNCiAgICAgICAgICAgICLmn6Xor6LlrZfmrrU9JXMg55uu5qCH5pe26Ze0PSVzIChVVEMpIOi/h+a7pOWZqD0lcyIsDQogICAgICAgICAgICBmaWVsZF9uYW1lLA0KICAgICAgICAgICAgdGFyZ2V0X3RpbWVfcmZjLA0KICAgICAgICAgICAgZmlsdGVycyBvciB7fSwNCiAgICAgICAgKQ0KDQogICAgICAgICMg5L2/55So5pe26Ze056qX5Y+j5p+l5om+5pyA5o6l6L+R55qE5pWw5o2u54K5DQogICAgICAgIHdpbmRvd19taW51dGVzID0gMTANCiAgICAgICAgDQogICAgICAgIHF1ZXJ5X3N0YXJ0ID0gdGFyZ2V0X3RpbWUgLSB0aW1lZGVsdGEobWludXRlcz13aW5kb3dfbWludXRlcykNCiAgICAgICAgcXVlcnlfZW5kID0gdGFyZ2V0X3RpbWUgKyB0aW1lZGVsdGEobWludXRlcz13aW5kb3dfbWludXRlcykNCiAgICAgICAgDQogICAgICAgIHF1ZXJ5X3N0YXJ0X3JmYyA9IHF1ZXJ5X3N0YXJ0LnN0cmZ0aW1lKCclWS0lbS0lZFQlSDolTTolU1onKQ0KICAgICAgICBxdWVyeV9lbmRfcmZjID0gcXVlcnlfZW5kLnN0cmZ0aW1lKCclWS0lbS0lZFQlSDolTTolU1onKQ0KICAgICAgICANCiAgICAgICAgTE9HR0VSLmRlYnVnKCLmn6Xor6Lnqpflj6M6ICVzIOWIsCAlcyIsIHF1ZXJ5X3N0YXJ0X3JmYywgcXVlcnlfZW5kX3JmYykNCg0KICAgICAgICAjIOaehOW7uui/h+a7pOadoeS7tg0KICAgICAgICB0YWdfZmlsdGVycyA9ICIiDQogICAgICAgIGlmIGZpbHRlcnM6DQogICAgICAgICAgICBmb3Iga2V5LCB2YWx1ZSBpbiBmaWx0ZXJzLml0ZW1zKCk6DQogICAgICAgICAgICAgICAgdGFnX2ZpbHRlcnMgKz0gZidcbiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsie2tleX0iXSA9PSAie3ZhbHVlfSIpJw0KDQogICAgICAgICMg5p+l6K+i5rip5bqm5pWw5o2u77yI5LiN6ZyA6KaBbG9hZF9zdGF0dXPnrZvpgInvvIzlm6DkuLrlt7Lnu4/ln7rkuo7mnInmlYjml7bpl7Tngrnmn6Xor6LvvIkNCiAgICAgICAgZmx1eCA9IGYnJycNCmZyb20oYnVja2V0OiAie2luZmx1eF9idWNrZXR9IikNCiAgfD4gcmFuZ2Uoc3RhcnQ6IHtxdWVyeV9zdGFydF9yZmN9LCBzdG9wOiB7cXVlcnlfZW5kX3JmY30pDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9tZWFzdXJlbWVudCJdID09ICJ7aW5mbHV4X21lYXN1cmVtZW50fSIpDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9maWVsZCJdID09ICJ7ZmllbGRfbmFtZX0iKXt0YWdfZmlsdGVyc30NCiAgfD4gc29ydChjb2x1bW5zOiBbIl90aW1lIl0pDQogIHw+IGxhc3QoKQ0KICB8PiB5aWVsZChuYW1lOiAiaW5zdGFudGFuZW91c19hdF9lZmZlY3RpdmVfdGltZSIpDQonJycuc3RyaXAoKQ0KDQogICAgICAgIExPR0dFUi5kZWJ1ZygiRmx1eOafpeivouivreWPpTpcbiVzIiwgZmx1eCkNCg0KICAgICAgICB3aXRoIHdhcm5pbmdzLmNhdGNoX3dhcm5pbmdzKCk6DQogICAgICAgICAgICB3YXJuaW5ncy5zaW1wbGVmaWx0ZXIoImlnbm9yZSIsIE1pc3NpbmdQaXZvdEZ1bmN0aW9uKQ0KICAgICAgICAgICAgZnJhbWVzID0gcXVlcnlfYXBpLnF1ZXJ5X2RhdGFfZnJhbWUoZmx1eCkNCiAgICAgICAgDQogICAgICAgIGlmIGlzaW5zdGFuY2UoZnJhbWVzLCBsaXN0KToNCiAgICAgICAgICAgIGRmID0gcGQuY29uY2F0KGZyYW1lcywgaWdub3JlX2luZGV4PVRydWUpIGlmIGZyYW1lcyBlbHNlIHBkLkRhdGFGcmFtZSgpDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBkZiA9IGZyYW1lcw0KDQogICAgICAgICMg6I635Y+W556s5pe25YC877yI5pyA6L+R55qE5LiA5Liq5pyJ5pWI5pWw5o2u54K577yJDQogICAgICAgIGlmIGRmLmVtcHR5IG9yICdfdmFsdWUnIG5vdCBpbiBkZi5jb2x1bW5zOg0KICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCJObyB2YWxpZCBkYXRhIGZvdW5kIGZvciBmaWVsZD0lcyBhdCBlZmZlY3RpdmUgdGltZSBwb2ludCIsIGZpZWxkX25hbWUpDQogICAgICAgICAgICByZXR1cm4gTm9uZQ0KICAgICAgICAgICAgDQogICAgICAgICMg5Y+W56ys5LiA6KGM55qE5YC877yI5Zug5Li65p+l6K+i5bey57uP5o6S5bqP5bm25Y+W5LqGbGFzdCgp77yJDQogICAgICAgIGluc3RhbnRfdmFsdWUgPSBkZlsnX3ZhbHVlJ10uaWxvY1swXQ0KICAgICAgICBpZiBwZC5pc25hKGluc3RhbnRfdmFsdWUpOg0KICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCJJbnN0YW50YW5lb3VzIHZhbHVlIGlzIE5hTiBmb3IgZmllbGQ9JXMiLCBmaWVsZF9uYW1lKQ0KICAgICAgICAgICAgcmV0dXJuIE5vbmUNCg0KICAgICAgICB2YWx1ZSA9IGZsb2F0KGluc3RhbnRfdmFsdWUpDQogICAgICAgIA0KICAgICAgICAjIOWmguaenOacieaXtumXtOS/oeaBr++8jOiusOW9leWunumZheeahOaVsOaNruaXtumXtOeCuQ0KICAgICAgICBpZiAnX3RpbWUnIGluIGRmLmNvbHVtbnM6DQogICAgICAgICAgICBhY3R1YWxfdGltZSA9IGRmWydfdGltZSddLmlsb2NbMF0NCiAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygiRmllbGQ9JXMgaW5zdGFudGFuZW91c192YWx1ZT0lLjNmIGFjdHVhbF90aW1lPSVzIChhdCBlZmZlY3RpdmUgdGltZSkiLCANCiAgICAgICAgICAgICAgICAgICAgICAgIGZpZWxkX25hbWUsIHZhbHVlLCBhY3R1YWxfdGltZSkNCiAgICAgICAgZWxzZToNCiAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygiRmllbGQ9JXMgaW5zdGFudGFuZW91c192YWx1ZT0lLjNmIChhdCBlZmZlY3RpdmUgdGltZSkiLCBmaWVsZF9uYW1lLCB2YWx1ZSkNCiAgICAgICAgICAgIA0KICAgICAgICByZXR1cm4gdmFsdWUNCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgIExPR0dFUi5lcnJvcigiRXJyb3IgcXVlcnlpbmcgSW5mbHV4REIgZm9yIGZpZWxkPSVzOiAlcyIsIGZpZWxkX25hbWUsIGUpDQogICAgICAgIHJldHVybiBOb25lDQogICAgZmluYWxseToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgY2xpZW50LmNsb3NlKCkNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoNCiAgICAgICAgICAgIHBhc3MNCg0KDQpkZWYgX2xvYWRfdGVtcGVyYXR1cmVfZGF0YV93aXRoX2xvYWRfc3RhdHVzKA0KICAgIHRpbWVfc2xvdHM6IExpc3Rbc3RyXSwNCiAgICBzZWN0aW9uczogTGlzdFtEaWN0W3N0ciwgQW55XV0sDQogICAgc3RhcnRfdGltZTogT3B0aW9uYWxbZGF0ZXRpbWVdLA0KICAgIGVuZF90aW1lOiBPcHRpb25hbFtkYXRldGltZV0sDQopIC0+IERpY3Rbc3RyLCBEaWN0W3N0ciwgZmxvYXRdXToNCiAgICAiIiLku44gSW5mbHV4REIg5p+l6K+i5omA5pyJ5rWL6K+V6YOo5L2N5Zyo5ZCE5pe26Ze054K555qE556s5pe25rip5bqm5YC877yI5LuF5b2TIGxvYWRfc3RhdHVzID0gMSDml7bvvIkiIiINCiAgICBpZiBub3Qgc3RhcnRfdGltZSBvciBub3QgZW5kX3RpbWU6DQogICAgICAgIExPR0dFUi5pbmZvKCJTa2lwIGRhdGEgcXVlcnk6IG1pc3Npbmcgc3RhcnQvZW5kICglcywgJXMpIiwgc3RhcnRfdGltZSwgZW5kX3RpbWUpDQogICAgICAgIHJldHVybiB7fQ0KICAgIA0KICAgIGluZmx1eF9jb25maWcgPSBfZ2V0X2luZmx1eF9jb25maWcoKQ0KICAgIA0KICAgIGlmIG5vdCBhbGwoW2luZmx1eF9jb25maWdbJ3VybCddLCBpbmZsdXhfY29uZmlnWydvcmcnXSwgaW5mbHV4X2NvbmZpZ1sndG9rZW4nXSwgDQogICAgICAgICAgICAgICAgaW5mbHV4X2NvbmZpZ1snYnVja2V0J10sIGluZmx1eF9jb25maWdbJ21lYXN1cmVtZW50J11dKToNCiAgICAgICAgTE9HR0VSLndhcm5pbmcoDQogICAgICAgICAgICAiU2tpcCBkYXRhIHF1ZXJ5OiBtaXNzaW5nIEluZmx1eCBjb25maWcgdXJsPSVzIGJ1Y2tldD0lcyBtZWFzdXJlbWVudD0lcyIsDQogICAgICAgICAgICBpbmZsdXhfY29uZmlnWyd1cmwnXSBvciAiPGVtcHR5PiIsDQogICAgICAgICAgICBpbmZsdXhfY29uZmlnWydidWNrZXQnXSBvciAiPGVtcHR5PiIsDQogICAgICAgICAgICBpbmZsdXhfY29uZmlnWydtZWFzdXJlbWVudCddIG9yICI8ZW1wdHk+IiwNCiAgICAgICAgKQ0KICAgICAgICByZXR1cm4ge30NCiAgICANCiAgICAjIOiuoeeul+aAu+aXtumVv++8iOWwj+aXtu+8iQ0KICAgIHRvdGFsX2R1cmF0aW9uID0gKGVuZF90aW1lIC0gc3RhcnRfdGltZSkudG90YWxfc2Vjb25kcygpIC8gMzYwMC4wDQogICAgTE9HR0VSLmluZm8oDQogICAgICAgICJGZXRjaCBpbnN0YW50YW5lb3VzIHRlbXBlcmF0dXJlIGRhdGEgKGxvYWRfc3RhdHVzPTEpIHdpbmRvdz0lc+KGkiVzIHRvdGFsX2hvdXJzPSUuM2YgdGltZV9wb2ludHM9JXMiLA0KICAgICAgICBzdGFydF90aW1lLmlzb2Zvcm1hdCgpLA0KICAgICAgICBlbmRfdGltZS5pc29mb3JtYXQoKSwNCiAgICAgICAgdG90YWxfZHVyYXRpb24sDQogICAgICAgICIsIi5qb2luKHRpbWVfc2xvdHMpLA0KICAgICkNCiAgICANCiAgICAjIOaUtumbhuaJgOaciemcgOimgeafpeivoueahOWtl+autQ0KICAgIHF1ZXJ5X3RhcmdldHM6IExpc3RbdHVwbGVbc3RyLCBEaWN0W3N0ciwgQW55XV1dID0gW10NCiAgICBmb3Igc2VjdGlvbiBpbiBzZWN0aW9uczoNCiAgICAgICAgZW50cmllcyA9IHNlY3Rpb24uZ2V0KCJlbnRyaWVzIikgb3IgW10NCiAgICAgICAgZm9yIGVudHJ5IGluIGVudHJpZXM6DQogICAgICAgICAgICBpZiBpc2luc3RhbmNlKGVudHJ5LCBkaWN0KToNCiAgICAgICAgICAgICAgICBmaWVsZF9uYW1lID0gZW50cnkuZ2V0KCJmaWVsZCIsICIiKQ0KICAgICAgICAgICAgICAgIGlmIGZpZWxkX25hbWU6DQogICAgICAgICAgICAgICAgICAgIHF1ZXJ5X3RhcmdldHMuYXBwZW5kKChmaWVsZF9uYW1lLCBlbnRyeSkpDQoNCiAgICBpZiBub3QgcXVlcnlfdGFyZ2V0czoNCiAgICAgICAgcmV0dXJuIHt9DQogICAgDQogICAgIyDorqHnrpfln7rkuo7mnInmlYjov5DooYzml7bpl7TntK/orqHnmoTnnJ/lrp7ml7bpl7TngrkNCiAgICBMT0dHRVIuaW5mbygiPT09IOW8gOWni+iuoeeul+acieaViOaXtumXtOeCuSA9PT0iKQ0KICAgIGVmZmVjdGl2ZV90aW1lX3BvaW50cyA9IF9jYWxjdWxhdGVfZWZmZWN0aXZlX3RpbWVfcG9pbnRzKA0KICAgICAgICBzdGFydF90aW1lLCBlbmRfdGltZSwgdGltZV9zbG90cywgaW5mbHV4X2NvbmZpZw0KICAgICkNCiAgICANCiAgICAjIOS4uuavj+S4quacieaViOaXtumXtOeCueafpeivoua4qeW6puaVsOaNrg0KICAgIHRlbXBlcmF0dXJlX2RhdGE6IERpY3Rbc3RyLCBEaWN0W3N0ciwgZmxvYXRdXSA9IHt9DQogICAgDQogICAgZm9yIGlkeCwgc2xvdF9zdHIgaW4gZW51bWVyYXRlKHRpbWVfc2xvdHMpOg0KICAgICAgICB0YXJnZXRfdGltZV9wb2ludCA9IGVmZmVjdGl2ZV90aW1lX3BvaW50cy5nZXQoc2xvdF9zdHIpDQogICAgICAgIA0KICAgICAgICBpZiB0YXJnZXRfdGltZV9wb2ludCBpcyBOb25lOg0KICAgICAgICAgICAgTE9HR0VSLndhcm5pbmcoIk5vIGVmZmVjdGl2ZSB0aW1lIHBvaW50IGNhbGN1bGF0ZWQgZm9yIHNsb3QgJXMsIHNraXBwaW5nIiwgc2xvdF9zdHIpDQogICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICANCiAgICAgICAgTE9HR0VSLmRlYnVnKCJQcm9jZXNzaW5nIHNsb3QgJXMgYXQgZWZmZWN0aXZlIHRpbWUgcG9pbnQgJXMiLCANCiAgICAgICAgICAgICAgICAgICAgc2xvdF9zdHIsIHRhcmdldF90aW1lX3BvaW50LnN0cmZ0aW1lKCclWS0lbS0lZCAlSDolTTolUycpKQ0KICAgICAgICANCiAgICAgICAgZm9yIGZpZWxkX25hbWUsIGVudHJ5IGluIHF1ZXJ5X3RhcmdldHM6DQogICAgICAgICAgICByZXN1bHRfa2V5ID0gZW50cnkuZ2V0KCJyZXN1bHRfa2V5Iikgb3IgZmllbGRfbmFtZQ0KICAgICAgICAgICAgaWYgbm90IHJlc3VsdF9rZXk6DQogICAgICAgICAgICAgICAgcmVzdWx0X2tleSA9IGZpZWxkX25hbWUNCiAgICAgICAgICAgIGVudHJ5X2ZpbHRlcnMgPSBlbnRyeS5nZXQoImZpbHRlcnMiKSBpZiBpc2luc3RhbmNlKGVudHJ5LCBkaWN0KSBlbHNlIE5vbmUNCiAgICAgICAgICAgIGlmIHJlc3VsdF9rZXkgbm90IGluIHRlbXBlcmF0dXJlX2RhdGE6DQogICAgICAgICAgICAgICAgdGVtcGVyYXR1cmVfZGF0YVtyZXN1bHRfa2V5XSA9IHt9DQoNCiAgICAgICAgICAgICMg5L2/55So57Si5byV5L2c5Li6a2V577yM5Zug5Li65Y+v6IO95pyJ6YeN5aSN55qE5pe26Ze05Yi75bqmDQogICAgICAgICAgICBzbG90X2tleSA9IGYie2lkeH1fe3Nsb3Rfc3RyfSIgICMg5L2/55So57Si5byVK+aXtumXtOWIu+W6puS9nOS4uuWUr+S4gGtleQ0KDQogICAgICAgICAgICAjIOafpeivoueerOaXtuWAvO+8iOWcqOacieaViOaXtumXtOeCue+8iQ0KICAgICAgICAgICAgdmFsdWUgPSBfcXVlcnlfaW5mbHV4ZGJfd2l0aF9sb2FkX3N0YXR1cygNCiAgICAgICAgICAgICAgICBmaWVsZF9uYW1lLA0KICAgICAgICAgICAgICAgIHRhcmdldF90aW1lX3BvaW50LA0KICAgICAgICAgICAgICAgIGluZmx1eF9jb25maWdbJ3VybCddLA0KICAgICAgICAgICAgICAgIGluZmx1eF9jb25maWdbJ29yZyddLA0KICAgICAgICAgICAgICAgIGluZmx1eF9jb25maWdbJ3Rva2VuJ10sDQogICAgICAgICAgICAgICAgaW5mbHV4X2NvbmZpZ1snYnVja2V0J10sDQogICAgICAgICAgICAgICAgaW5mbHV4X2NvbmZpZ1snbWVhc3VyZW1lbnQnXSwNCiAgICAgICAgICAgICAgICBmaWx0ZXJzPWVudHJ5X2ZpbHRlcnMgaWYgZW50cnlfZmlsdGVycyBlbHNlIE5vbmUsDQogICAgICAgICAgICApDQoNCiAgICAgICAgICAgIGlmIHZhbHVlIGlzIG5vdCBOb25lOg0KICAgICAgICAgICAgICAgIHRlbXBlcmF0dXJlX2RhdGFbcmVzdWx0X2tleV1bc2xvdF9rZXldID0gdmFsdWUNCiAgICAgICAgICAgICAgICBMT0dHRVIuZGVidWcoDQogICAgICAgICAgICAgICAgICAgICJTbG90PSVzIGZpZWxkPSVzIHZhbHVlPSUuM2YgYXQgZWZmZWN0aXZlX3RpbWU9JXMiLA0KICAgICAgICAgICAgICAgICAgICBzbG90X2tleSwNCiAgICAgICAgICAgICAgICAgICAgcmVzdWx0X2tleSwNCiAgICAgICAgICAgICAgICAgICAgdmFsdWUsDQogICAgICAgICAgICAgICAgICAgIHRhcmdldF90aW1lX3BvaW50LnN0cmZ0aW1lKCclSDolTTolUycpDQogICAgICAgICAgICAgICAgKQ0KICAgICAgICAgICAgZWxzZToNCiAgICAgICAgICAgICAgICBMT0dHRVIuZGVidWcoDQogICAgICAgICAgICAgICAgICAgICJTbG90PSVzIGZpZWxkPSVzIG5vX2RhdGEgYXQgZWZmZWN0aXZlX3RpbWU9JXMiLA0KICAgICAgICAgICAgICAgICAgICBzbG90X2tleSwNCiAgICAgICAgICAgICAgICAgICAgcmVzdWx0X2tleSwNCiAgICAgICAgICAgICAgICAgICAgdGFyZ2V0X3RpbWVfcG9pbnQuc3RyZnRpbWUoJyVIOiVNOiVTJykNCiAgICAgICAgICAgICAgICApDQoNCiAgICByZXR1cm4gdGVtcGVyYXR1cmVfZGF0YQ0KDQoNCmRlZiBfYnVpbGRfY2VsbHNfd2l0aF9sb2FkX3N0YXR1cygNCiAgICB0aW1lX3Nsb3RzOiBMaXN0W3N0cl0sDQogICAgc2VjdGlvbnM6IExpc3RbRGljdFtzdHIsIEFueV1dLA0KICAgIG1vdG9yX3NwZWVkOiBzdHIsDQogICAgc3RhcnRfdGltZTogT3B0aW9uYWxbZGF0ZXRpbWVdLA0KICAgIGVuZF90aW1lOiBPcHRpb25hbFtkYXRldGltZV0sDQogICAgdGVtcGVyYXR1cmVfZGF0YTogRGljdFtzdHIsIERpY3Rbc3RyLCBmbG9hdF1dLA0KICAgIHVzZV9kZWZhdWx0czogYm9vbCA9IEZhbHNlLA0KKSAtPiBMaXN0W0RpY3Rbc3RyLCBBbnldXToNCiAgICAiIiLmnoTlu7rljZXlhYPmoLzmlbDmja7vvIjln7rkuo4gbG9hZF9zdGF0dXMgPSAxIOeahOacieaViOaVsOaNru+8iS0g5LiO5Y6f5aeL6ISa5pys57uT5p6E5a6M5YWo5LiA6Ie0IiIiDQogICAgY2VsbHM6IExpc3RbRGljdFtzdHIsIEFueV1dID0gW10NCg0KICAgIGRlZiBhZGRfY2VsbChyb3c6IGludCwgY29sOiBpbnQsIHZhbHVlOiBzdHIgPSAiIiwgcm93c3BhbjogaW50ID0gMSwgY29sc3BhbjogaW50ID0gMSkgLT4gTm9uZToNCiAgICAgICAgcGF5bG9hZDogRGljdFtzdHIsIEFueV0gPSB7InJvdyI6IHJvdywgImNvbCI6IGNvbCwgInZhbHVlIjogdmFsdWV9DQogICAgICAgIGlmIHJvd3NwYW4gPiAxOg0KICAgICAgICAgICAgcGF5bG9hZFsicm93c3BhbiJdID0gcm93c3Bhbg0KICAgICAgICBpZiBjb2xzcGFuID4gMToNCiAgICAgICAgICAgIHBheWxvYWRbImNvbHNwYW4iXSA9IGNvbHNwYW4NCiAgICAgICAgY2VsbHMuYXBwZW5kKHBheWxvYWQpDQoNCiAgICAjIOaooeadv+W3puS+p+agh+mimOWIl+W3sue7j+WOu+mZpO+8jOi/memHjOS7heeUn+aIkOe6r+aVsOaNruWMuu+8jOS7jiAoMCwwKSDlvIDlp4vloavlhaXmlbDlgLzjgIINCiAgICAjIGN1cnJlbnRfcm93IOWvueW6lOaooeadv+S4reeahOWunumZheaVsOaNruihjOe0ouW8leOAgg0KICAgIGN1cnJlbnRfcm93ID0gMA0KICAgIGZvciBzZWN0aW9uIGluIHNlY3Rpb25zOg0KICAgICAgICBlbnRyaWVzID0gc2VjdGlvbi5nZXQoImVudHJpZXMiKSBvciBbXQ0KICAgICAgICBpZiBub3QgZW50cmllczoNCiAgICAgICAgICAgIGNvbnRpbnVlDQogICAgICAgICMg5q+P5Liq5rWL6K+V6YOo5L2N5a2Q6aG55a+55bqU5qih5p2/5Lit55qE5LiA6KGMDQogICAgICAgIGZvciBlbnRyeSBpbiBlbnRyaWVzOg0KICAgICAgICAgICAgIyDmlK/mjIHmlrDmoLzlvI/vvIjluKYgZmllbGQg5pig5bCE77yJ5ZKM5pen5qC85byP77yI57qv5a2X56ym5Liy77yJDQogICAgICAgICAgICBpZiBpc2luc3RhbmNlKGVudHJ5LCBkaWN0KToNCiAgICAgICAgICAgICAgICBmaWVsZF9uYW1lID0gZW50cnkuZ2V0KCJmaWVsZCIsICIiKQ0KICAgICAgICAgICAgICAgIGVudHJ5X2ZpbHRlcnMgPSBlbnRyeS5nZXQoImZpbHRlcnMiKQ0KICAgICAgICAgICAgICAgIGVudHJ5X2tleSA9IGVudHJ5LmdldCgicmVzdWx0X2tleSIpIG9yIGZpZWxkX25hbWUNCiAgICAgICAgICAgIGVsc2U6DQogICAgICAgICAgICAgICAgZmllbGRfbmFtZSA9ICIiDQogICAgICAgICAgICAgICAgZW50cnlfZmlsdGVycyA9IE5vbmUNCiAgICAgICAgICAgICAgICBlbnRyeV9rZXkgPSAiIg0KDQogICAgICAgICAgICAjIOS7hei+k+WHuuaVsOWAvOWIl++8muWIl+e0ouW8leebtOaOpeWvueW6lOaXtumXtOautQ0KICAgICAgICAgICAgIyDlvLrliLbloavlhYXmiYDmnInliJfvvIzkvJjlhYjkvb/nlKjmn6Xor6LmlbDmja7vvIzlkKbliJnkvb/nlKjpu5jorqTlgLwNCiAgICAgICAgICAgIGlmIGZpZWxkX25hbWU6DQogICAgICAgICAgICAgICAgdGFyZ2V0X2tleSA9IGVudHJ5X2tleSBvciBmaWVsZF9uYW1lDQoNCiAgICAgICAgICAgICAgICAjIOmBjeWOhuaJgOacieaXtumXtOauteWIl++8jOehruS/neavj+S4gOWIl+mDveacieaVsOaNrg0KICAgICAgICAgICAgICAgIGZvciBjb2xfaWR4LCBzbG90IGluIGVudW1lcmF0ZSh0aW1lX3Nsb3RzKToNCiAgICAgICAgICAgICAgICAgICAgdmFsdWUgPSBOb25lDQoNCiAgICAgICAgICAgICAgICAgICAgIyDkvJjlhYjkvb/nlKjmn6Xor6LliLDnmoTmlbDmja4NCiAgICAgICAgICAgICAgICAgICAgaWYgdGVtcGVyYXR1cmVfZGF0YToNCiAgICAgICAgICAgICAgICAgICAgICAgIHNsb3RfZGF0YSA9IHRlbXBlcmF0dXJlX2RhdGEuZ2V0KHRhcmdldF9rZXksIHt9KQ0KICAgICAgICAgICAgICAgICAgICAgICAgaWYgc2xvdF9kYXRhOg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNsb3Rfa2V5ID0gZiJ7Y29sX2lkeH1fe3Nsb3R9Ig0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlID0gc2xvdF9kYXRhLmdldChzbG90X2tleSkNCg0KICAgICAgICAgICAgICAgICAgICBpZiB2YWx1ZSBpcyBOb25lIGFuZCB1c2VfZGVmYXVsdHM6DQogICAgICAgICAgICAgICAgICAgICAgICAjIOS9v+eUqOWfuuehgOm7mOiupOWAvCArIOaXtumXtOauteWBj+enu++8iOavj+S4quaXtumXtOauteWinuWKoDAuMeW6pu+8iQ0KICAgICAgICAgICAgICAgICAgICAgICAgZGVmYXVsdF9iYXNlX3ZhbHVlID0gMjUuMCAgIyDnroDljJbnmoTpu5jorqTlgLwNCiAgICAgICAgICAgICAgICAgICAgICAgIHRpbWVfb2Zmc2V0ID0gY29sX2lkeCAqIDAuMQ0KICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWUgPSBkZWZhdWx0X2Jhc2VfdmFsdWUgKyB0aW1lX29mZnNldA0KDQogICAgICAgICAgICAgICAgICAgIGlmIHZhbHVlIGlzIE5vbmU6DQogICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZV9zdHIgPSAiIg0KICAgICAgICAgICAgICAgICAgICBlbHNlOg0KICAgICAgICAgICAgICAgICAgICAgICAgIyDmoLzlvI/ljJbkuLrlrZfnrKbkuLLvvIjkv53nlZkx5L2N5bCP5pWw77yJDQogICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZV9zdHIgPSBmInt2YWx1ZTouMWZ9Ig0KDQogICAgICAgICAgICAgICAgICAgIGFkZF9jZWxsKGN1cnJlbnRfcm93LCBjb2xfaWR4LCB2YWx1ZV9zdHIpDQogICAgICAgICAgICBlbHNlOg0KICAgICAgICAgICAgICAgICMg5aaC5p6c5rKh5pyJ5a2X5q615ZCN77yM5aGr5YWF56m65a2X56ym5LiyDQogICAgICAgICAgICAgICAgZm9yIGNvbF9pZHggaW4gcmFuZ2UobGVuKHRpbWVfc2xvdHMpKToNCiAgICAgICAgICAgICAgICAgICAgYWRkX2NlbGwoY3VycmVudF9yb3csIGNvbF9pZHgsICIiKQ0KICAgICAgICAgICAgY3VycmVudF9yb3cgKz0gMQ0KDQogICAgcmV0dXJuIGNlbGxzDQoNCg0KZGVmIGJ1aWxkX3RlbXBlcmF0dXJlX3RhYmxlX3dpdGhfbG9hZF9zdGF0dXMoXzogRGljdFtzdHIsIEFueV0pIC0+IERpY3Rbc3RyLCBBbnldOg0KICAgICIiIuaehOW7uua4qeW6puihqOagvOaVsOaNru+8iOS7heS9v+eUqCBsb2FkX3N0YXR1cyA9IDEg55qE5pyJ5pWI5pWw5o2u77yJIiIiDQogICAgX3NldHVwX2xvZ2dpbmcoKQ0KICAgIA0KICAgIHRva2VuID0gb3MuZW52aXJvbi5nZXQoIlRBQkxFX1RPS0VOIiwgInNjcmlwdFRhYmxlMSIpDQogICAgcm93X29mZnNldCA9IGludChvcy5lbnZpcm9uLmdldCgiVEFCTEVfU1RBUlRfUk9XIiwgIjAiKSBvciAwKQ0KICAgIGNvbF9vZmZzZXQgPSBpbnQob3MuZW52aXJvbi5nZXQoIlRBQkxFX1NUQVJUX0NPTCIsICIwIikgb3IgMCkNCiAgICBtb3Rvcl9zcGVlZCA9IG9zLmVudmlyb24uZ2V0KCJUQUJMRV9NT1RPUl9TUEVFRCIsICI5ODBSUE0iKQ0KICAgIA0KICAgICMg6Kej5p6Q5a6e6aqM5pe26Ze06IyD5Zu0DQogICAgc3RhcnRfdGltZSwgZW5kX3RpbWUgPSBfcGFyc2VfZXhwZXJpbWVudF90aW1lcygpDQogICAgDQogICAgdGltZV9zbG90cyA9IF90aW1lX3Nsb3RzKCkNCiAgICBzZWN0aW9ucyA9IF9kZWZhdWx0X3NlY3Rpb25zKCkNCiAgICANCiAgICAjIOafpeivoua4qeW6puaVsOaNru+8iOS7heW9kyBsb2FkX3N0YXR1cyA9IDEg5pe277yJDQogICAgdGVtcGVyYXR1cmVfZGF0YSA9IF9sb2FkX3RlbXBlcmF0dXJlX2RhdGFfd2l0aF9sb2FkX3N0YXR1cyh0aW1lX3Nsb3RzLCBzZWN0aW9ucywgc3RhcnRfdGltZSwgZW5kX3RpbWUpDQogICAgDQogICAgIyDlp4vnu4jnpoHmraLpu5jorqTmlbDmja7vvIzkv53or4Hmn6Xor6LkuI3liLDlgLzml7bkv53mjIHnqbrnmb0NCiAgICB1c2VfZGVmYXVsdHMgPSBGYWxzZQ0KICAgIA0KICAgIGNlbGxzID0gX2J1aWxkX2NlbGxzX3dpdGhfbG9hZF9zdGF0dXMoDQogICAgICAgIHRpbWVfc2xvdHMsIA0KICAgICAgICBzZWN0aW9ucywgDQogICAgICAgIG1vdG9yX3NwZWVkLCANCiAgICAgICAgc3RhcnRfdGltZSwgDQogICAgICAgIGVuZF90aW1lLCANCiAgICAgICAgdGVtcGVyYXR1cmVfZGF0YSwNCiAgICAgICAgdXNlX2RlZmF1bHRzPXVzZV9kZWZhdWx0cw0KICAgICkNCiAgICANCiAgICAjIOW6lOeUqOihjOWBj+enuw0KICAgIGZvciBjZWxsIGluIGNlbGxzOg0KICAgICAgICBjZWxsWyJyb3ciXSArPSA0DQogICAgDQogICAgIyDmt7vliqDlrp7pqozml7bpl7Tkv6Hmga/vvIjkuI7ljp/lp4vohJrmnKzlrozlhajkuIDoh7TnmoTpgLvovpHvvIkNCiAgICBzdGFydF90aW1lX3JvdyA9IDENCiAgICBzdGFydF90aW1lX3ZhbHVlX2NvbCA9IDENCiAgICBlbmRfdGltZV92YWx1ZV9jb2wgPSAzDQogICAgDQogICAgIyDojrflj5bljp/lp4vml7bpl7TlrZfnrKbkuLLov5vooYzlpITnkIbvvIjkuI7ljp/lp4vohJrmnKzkv53mjIHkuIDoh7TvvIkNCiAgICBzdGFydF9zdHIgPSBvcy5lbnZpcm9uLmdldCgiRVhQRVJJTUVOVF9TVEFSVCIsICIiKS5zdHJpcCgpDQogICAgaWYgc3RhcnRfc3RyIGFuZCBzdGFydF90aW1lOg0KICAgICAgICB0cnk6DQogICAgICAgICAgICAjIOS9v+eUqOS4juWOn+Wni+iEmuacrOebuOWQjOeahOaXtumXtOWkhOeQhumAu+i+kQ0KICAgICAgICAgICAgdXRjX2F3YXJlX2R0ID0gZGF0ZXRpbWUuc3RycHRpbWUoc3RhcnRfc3RyLCAiJVktJW0tJWRUJUg6JU06JVMleiIpDQogICAgICAgICAgICBsb2NhbF9kdDEgPSB1dGNfYXdhcmVfZHQuYXN0aW1lem9uZSh0ej1Ob25lKQ0KICAgICAgICAgICAgbG9jYWxfZHQyID0gdXRjX2F3YXJlX2R0LmFzdGltZXpvbmUodHo9Tm9uZSkgKyB0aW1lZGVsdGEoaG91cnM9My41KQ0KICAgICAgICAgICAgc3RhcnRfdGltZV92YWx1ZSA9IGxvY2FsX2R0MS5zdHJmdGltZSgiJVktJW0tJWQgJUg6JU06JVMiKQ0KICAgICAgICAgICAgZW5kX3RpbWVfdmFsdWUgPSBsb2NhbF9kdDIuc3RyZnRpbWUoIiVZLSVtLSVkICVIOiVNOiVTIikNCiAgICAgICAgICAgIGNlbGxzLmFwcGVuZCh7InJvdyI6IHN0YXJ0X3RpbWVfcm93LCAiY29sIjogc3RhcnRfdGltZV92YWx1ZV9jb2wsICJ2YWx1ZSI6IHN0YXJ0X3RpbWVfdmFsdWV9KQ0KICAgICAgICAgICAgY2VsbHMuYXBwZW5kKHsicm93Ijogc3RhcnRfdGltZV9yb3csICJjb2wiOiBlbmRfdGltZV92YWx1ZV9jb2wsICJ2YWx1ZSI6IGVuZF90aW1lX3ZhbHVlfSkNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOg0KICAgICAgICAgICAgTE9HR0VSLndhcm5pbmcoIkZhaWxlZCB0byBwcm9jZXNzIGV4cGVyaW1lbnQgdGltZSBzdHJpbmdzOiAlcyIsIGUpDQogICAgDQogICAgIyDmn6Xor6Lnjq/looPmuKnluqbvvIjkuI7ljp/lp4vohJrmnKzlrozlhajkuIDoh7TnmoTpgLvovpHvvIkNCiAgICBpbmZsdXhfdXJsID0gb3MuZW52aXJvbi5nZXQoIklORkxVWF9VUkwiLCAiIikuc3RyaXAoKQ0KICAgIGluZmx1eF9vcmcgPSBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX09SRyIsICIiKS5zdHJpcCgpDQogICAgaW5mbHV4X3Rva2VuID0gb3MuZW52aXJvbi5nZXQoIklORkxVWF9UT0tFTiIsICIiKS5zdHJpcCgpDQogICAgaW5mbHV4X2J1Y2tldCA9IG9zLmVudmlyb24uZ2V0KCJJTkZMVVhfQlVDS0VUIiwgIlBDTSIpLnN0cmlwKCkNCiAgICBpbmZsdXhfbWVhc3VyZW1lbnQgPSBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX01FQVNVUkVNRU5UIiwgIlBDTV9NZWFzdXJlbWVudCIpLnN0cmlwKCkNCiAgICANCiAgICBpZiBzdGFydF90aW1lIGFuZCBlbmRfdGltZToNCiAgICAgICAgIyDlr7nkuo7njq/looPmuKnluqbvvIzkvb/nlKjml7bpl7TojIPlm7Tmn6Xor6LvvIjkuI7ljp/lp4vohJrmnKzpgLvovpHkuIDoh7TvvIkNCiAgICAgICAgdmFsdWUgPSBfcXVlcnlfaW5mbHV4ZGJfcmFuZ2Vfd2l0aF9sb2FkX3N0YXR1cygNCiAgICAgICAgICAgICLnjq/looPmuKnluqYiLA0KICAgICAgICAgICAgc3RhcnRfdGltZSwNCiAgICAgICAgICAgIGVuZF90aW1lLA0KICAgICAgICAgICAgaW5mbHV4X3VybCwNCiAgICAgICAgICAgIGluZmx1eF9vcmcsDQogICAgICAgICAgICBpbmZsdXhfdG9rZW4sDQogICAgICAgICAgICBpbmZsdXhfYnVja2V0LA0KICAgICAgICAgICAgaW5mbHV4X21lYXN1cmVtZW50LA0KICAgICAgICAgICAgZmlsdGVycz17ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LA0KICAgICAgICApDQogICAgICAgICMg56Gu5L+ddmFsdWXkuI3mmK9Ob25l77yM6YG/5YWNV29yZCBDT03mk43kvZzlvILluLjvvIjkuI7ljp/lp4vohJrmnKzkuIDoh7TvvIkNCiAgICAgICAgaWYgdmFsdWUgaXMgbm90IE5vbmU6DQogICAgICAgICAgICBjZWxscy5hcHBlbmQoeyJyb3ciOiAwLCAiY29sIjogMSwgInZhbHVlIjogZiJ7dmFsdWU6LjFmfSJ9KQ0KICAgICAgICBlbHNlOg0KICAgICAgICAgICAgY2VsbHMuYXBwZW5kKHsicm93IjogMCwgImNvbCI6IDEsICJ2YWx1ZSI6ICIifSkNCiAgICANCiAgICBMT0dHRVIuaW5mbygNCiAgICAgICAgIlRlbXBlcmF0dXJlIHRhYmxlIGJ1aWx0IHdpdGggbG9hZF9zdGF0dXM9MSBmaWx0ZXI6IHRva2VuPSVzIGNlbGxzPSVkIHRpbWVfc2xvdHM9JXMiLA0KICAgICAgICB0b2tlbiwNCiAgICAgICAgbGVuKGNlbGxzKSwNCiAgICAgICAgIiwiLmpvaW4odGltZV9zbG90cyksDQogICAgKQ0KICAgIA0KICAgIHJldHVybiB7DQogICAgICAgICJ0b2tlbiI6IHRva2VuLA0KICAgICAgICAic3RhcnRSb3ciOiByb3dfb2Zmc2V0LA0KICAgICAgICAic3RhcnRDb2wiOiBjb2xfb2Zmc2V0LA0KICAgICAgICAiY2VsbHMiOiBjZWxscywNCiAgICB9DQoNCg0KZGVmIF9sb2FkX3BheWxvYWQoKSAtPiBEaWN0W3N0ciwgQW55XToNCiAgICAiIiLku47moIflh4bovpPlhaXmiJbnjq/looPlj5jph4/liqDovb1wYXlsb2Fk5pWw5o2uIiIiDQogICAgdHJ5Og0KICAgICAgICAjIOWwneivleS7juagh+WHhui+k+WFpeivu+WPlkpTT04NCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgaW1wb3J0IHNlbGVjdA0KICAgICAgICAgICAgaWYgc2VsZWN0LnNlbGVjdChbc3lzLnN0ZGluXSwgW10sIFtdLCAwLjApWzBdOg0KICAgICAgICAgICAgICAgIHBheWxvYWRfc3RyID0gc3lzLnN0ZGluLnJlYWQoKS5zdHJpcCgpDQogICAgICAgICAgICAgICAgaWYgcGF5bG9hZF9zdHI6DQogICAgICAgICAgICAgICAgICAgIHJldHVybiBqc29uLmxvYWRzKHBheWxvYWRfc3RyKQ0KICAgICAgICBleGNlcHQgSW1wb3J0RXJyb3I6DQogICAgICAgICAgICAjIFdpbmRvd3PkuIpzZWxlY3Tlj6/og73kuI3lj6/nlKjvvIzlsJ3or5Xnm7TmjqXor7vlj5YNCiAgICAgICAgICAgIGltcG9ydCBtc3ZjcnQNCiAgICAgICAgICAgIGlmIG1zdmNydC5rYmhpdCgpOg0KICAgICAgICAgICAgICAgIHBheWxvYWRfc3RyID0gc3lzLnN0ZGluLnJlYWQoKS5zdHJpcCgpDQogICAgICAgICAgICAgICAgaWYgcGF5bG9hZF9zdHI6DQogICAgICAgICAgICAgICAgICAgIHJldHVybiBqc29uLmxvYWRzKHBheWxvYWRfc3RyKQ0KICAgIGV4Y2VwdCBFeGNlcHRpb246DQogICAgICAgIHBhc3MNCiAgICANCiAgICAjIOWmguaenOayoeacieagh+WHhui+k+WFpe+8jOi/lOWbnuepuuWtl+WFuA0KICAgIHJldHVybiB7fQ0KDQoNCmRlZiBfbG9nX2Vudmlyb25tZW50X3ZhcmlhYmxlcygpIC0+IE5vbmU6DQogICAgIiIi6K6w5b2V55u45YWz546v5aKD5Y+Y6YePIiIiDQogICAgZW52X3ZhcnMgPSBbDQogICAgICAgICJUQUJMRV9UT0tFTiIsICJUQUJMRV9TVEFSVF9ST1ciLCAiVEFCTEVfU1RBUlRfQ09MIiwgIlRBQkxFX1RJTUVfU0xPVFMiLCAiVEFCTEVfTU9UT1JfU1BFRUQiLA0KICAgICAgICAiRVhQRVJJTUVOVF9TVEFSVCIsICJFWFBFUklNRU5UX0VORCIsDQogICAgICAgICJJTkZMVVhfVVJMIiwgIklORkxVWF9PUkciLCAiSU5GTFVYX1RPS0VOIiwgIklORkxVWF9CVUNLRVQiLCAiSU5GTFVYX01FQVNVUkVNRU5UIg0KICAgIF0NCiAgICANCiAgICBmb3IgdmFyIGluIGVudl92YXJzOg0KICAgICAgICB2YWx1ZSA9IG9zLmVudmlyb24uZ2V0KHZhciwgIiIpDQogICAgICAgIGlmICJUT0tFTiIgaW4gdmFyIGFuZCB2YWx1ZToNCiAgICAgICAgICAgIHZhbHVlID0gX21hc2tfc2VjcmV0KHZhbHVlKQ0KICAgICAgICBMT0dHRVIuZGVidWcoIkVOViAlcz0lcyIsIHZhciwgdmFsdWUgb3IgIjxlbXB0eT4iKQ0KDQoNCmRlZiBtYWluKCkgLT4gaW50Og0KICAgIHRyeToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgaWYgbm90IGxvZ2dpbmcuZ2V0TG9nZ2VyKCkuaGFuZGxlcnM6DQogICAgICAgICAgICAgICAgbG9nX2xldmVsX25hbWUgPSBvcy5lbnZpcm9uLmdldCgiVEFCTEVfTE9HX0xFVkVMIiwgIkRFQlVHIikuc3RyaXAoKSBvciAiREVCVUciDQogICAgICAgICAgICAgICAgbG9nX2xldmVsID0gZ2V0YXR0cihsb2dnaW5nLCBsb2dfbGV2ZWxfbmFtZS51cHBlcigpLCBsb2dnaW5nLkRFQlVHKQ0KICAgICAgICAgICAgICAgIGxvZ19maWxlX3JhdyA9IG9zLmVudmlyb24uZ2V0KCJUQUJMRV9MT0dfRklMRSIsICJ0ZXN0LmxvZyIpLnN0cmlwKCkgb3IgInRlc3QubG9nIg0KICAgICAgICAgICAgICAgIGxvZ19maWxlID0gb3MucGF0aC5hYnNwYXRoKGxvZ19maWxlX3JhdykNCg0KICAgICAgICAgICAgICAgIGxvZ2dpbmcuYmFzaWNDb25maWcoDQogICAgICAgICAgICAgICAgICAgIGxldmVsPWxvZ19sZXZlbCwNCiAgICAgICAgICAgICAgICAgICAgZm9ybWF0PSIlKGFzY3RpbWUpcyBbJShsZXZlbG5hbWUpc10gJShuYW1lKXM6ICUobWVzc2FnZSlzIiwNCiAgICAgICAgICAgICAgICAgICAgaGFuZGxlcnM9Ww0KICAgICAgICAgICAgICAgICAgICAgICAgbG9nZ2luZy5GaWxlSGFuZGxlcihsb2dfZmlsZSwgZW5jb2Rpbmc9InV0Zi04IiksDQogICAgICAgICAgICAgICAgICAgICAgICBsb2dnaW5nLlN0cmVhbUhhbmRsZXIoc3lzLnN0ZGVyciksDQogICAgICAgICAgICAgICAgICAgIF0sDQogICAgICAgICAgICAgICAgKQ0KICAgICAgICAgICAgICAgIExPR0dFUi5pbmZvKCJMb2dnaW5nIGluaXRpYWxpemVkIC0+IGZpbGU9JXMgbGV2ZWw9JXMiLCBsb2dfZmlsZSwgbG9nZ2luZy5nZXRMZXZlbE5hbWUobG9nX2xldmVsKSkNCiAgICAgICAgICAgICAgICBfbG9nX2Vudmlyb25tZW50X3ZhcmlhYmxlcygpDQogICAgICAgICAgICBzeXMuc3Rkb3V0LnJlY29uZmlndXJlKGVuY29kaW5nPSJ1dGYtOCIpICAjIHR5cGU6IGlnbm9yZVthdHRyLWRlZmluZWRdDQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246DQogICAgICAgICAgICBwYXNzDQogICAgICAgIA0KICAgICAgICBwYXlsb2FkID0gX2xvYWRfcGF5bG9hZCgpDQogICAgICAgIHRhYmxlX3NwZWMgPSBidWlsZF90ZW1wZXJhdHVyZV90YWJsZV93aXRoX2xvYWRfc3RhdHVzKHBheWxvYWQpDQogICAgICAgIHJlc3VsdCA9IHsidGFibGVzIjogW3RhYmxlX3NwZWNdfQ0KICAgICAgICBwcmludChqc29uLmR1bXBzKHJlc3VsdCwgZW5zdXJlX2FzY2lpPUZhbHNlKSkNCiAgICAgICAgcmV0dXJuIDANCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGV4YzoNCiAgICAgICAgcHJpbnQoZiJlcnJvcjoge2V4Y30iLCBmaWxlPXN5cy5zdGRlcnIpDQogICAgICAgIHJldHVybiAxDQoNCg0KaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoNCiAgICBzeXMuZXhpdChtYWluKCkpDQo=", + "scriptFile": "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uDQojIC0qLSBjb2Rpbmc6IHV0Zi04IC0qLQ0KIiIiDQrmtYvor5Xpg6jkvY3muKnluqborrDlvZXooajnlJ/miJDohJrmnKzvvIjluKbotJ/ovb3nirbmgIHnrZvpgInvvIkNCg0KLSDlv73nlaXkvKDlhaXnmoQgZXhwZXJpbWVudFByb2Nlc3PvvIzoh6rooYzmnoTpgKDlm7rlrprnu5PmnoTnmoTmlbDmja4NCi0g5LuOIEluZmx1eERCIOafpeivouavj+S4qua1i+ivlemDqOS9jeWcqOWQhOaXtumXtOeCueeahOeerOaXtua4qeW6puWAvA0KLSDmt7vliqAgbG9hZF9zdGF0dXMgPSAxIOeahOetm+mAieadoeS7tu+8jOehruS/neWPquWcqOecn+ato+mHh+mbhuaVsOaNruaXtuiOt+WPlua4qeW6pg0KLSDovpPlh7rmoLzlvI/kuI7lupTnlKjkuK3nmoQgc2NyaXB0VGFibGUg5Y2g5L2N56ym5YW85a65DQotIOm7mOiupOaKiiB7c2NyaXB0VGFibGUxfSDmlL7lnKgi5rWL6K+V6YOo5L2NIuaJgOWcqOeahOWNleWFg+agvA0KDQrnjq/looPlj5jph4/vvJoNCiAgICBUQUJMRV9UT0tFTiAgICAgICAgIOebruagh+WNoOS9jeespu+8jOm7mOiupCBzY3JpcHRUYWJsZTENCiAgICBUQUJMRV9TVEFSVF9ST1cgICAgIOWGmeWFpei1t+Wni+ihjOWBj+enu++8jOm7mOiupCAwDQogICAgVEFCTEVfU1RBUlRfQ09MICAgICDlhpnlhaXotbflp4vliJflgY/np7vvvIzpu5jorqQgMA0KICAgIFRBQkxFX1RJTUVfU0xPVFMgICAg6YCX5Y+35YiG6ZqU55qE5pe26Ze05Yi75bqm77yM6buY6K6kICIwLjVoLDFoLDEuNWgsMmgsMi41aCwzaCwzLjVoIg0KICAgIFRBQkxFX01PVE9SX1NQRUVEICAg55S15py66L2s6YCf5qCH562+77yM6buY6K6kICI5ODBSUE0iDQogICAgRVhQRVJJTUVOVF9TVEFSVCAgICAg5a6e6aqM5byA5aeL5pe26Ze077yISVNPIDg2MDEg5qC85byP77yM5aaCIDIwMjQtMDEtMDFUMTA6MDA6MDBa77yJDQogICAgRVhQRVJJTUVOVF9FTkQgICAgICAg5a6e6aqM57uT5p2f5pe26Ze077yISVNPIDg2MDEg5qC85byP77yJDQogICAgSU5GTFVYX1VSTCAgICAgICAgICAgSW5mbHV4REIgVVJMDQogICAgSU5GTFVYX09SRyAgICAgICAgICAgSW5mbHV4REIg57uE57uHDQogICAgSU5GTFVYX1RPS0VOICAgICAgICAgSW5mbHV4REIg5Luk54mMDQogICAgSU5GTFVYX0JVQ0tFVCAgICAgICAgSW5mbHV4REIg5qG25ZCN77yM6buY6K6kIFBDTQ0KICAgIElORkxVWF9NRUFTVVJFTUVOVCAgIEluZmx1eERCIOa1i+mHj+WQje+8jOm7mOiupCBQQ01fTWVhc3VyZW1lbnQNCiIiIg0KDQpmcm9tIF9fZnV0dXJlX18gaW1wb3J0IGFubm90YXRpb25zDQoNCmltcG9ydCBqc29uDQppbXBvcnQgbG9nZ2luZw0KaW1wb3J0IG9zDQppbXBvcnQgc3lzDQpmcm9tIGRhdGV0aW1lIGltcG9ydCBkYXRldGltZSwgdGltZWRlbHRhDQpmcm9tIHR5cGluZyBpbXBvcnQgQW55LCBEaWN0LCBMaXN0LCBPcHRpb25hbA0KDQoNCkxPR0dFUiA9IGxvZ2dpbmcuZ2V0TG9nZ2VyKF9fbmFtZV9fKQ0KDQoNCmRlZiBfbWFza19zZWNyZXQodmFsdWU6IE9wdGlvbmFsW3N0cl0pIC0+IHN0cjoNCiAgICAiIiLmjqnnoIHmlY/mhJ/kv6Hmga8iIiINCiAgICBpZiBub3QgdmFsdWU6DQogICAgICAgIHJldHVybiAiPGVtcHR5PiINCiAgICBpZiBsZW4odmFsdWUpIDw9IDg6DQogICAgICAgIHJldHVybiAiKiIgKiBsZW4odmFsdWUpDQogICAgcmV0dXJuIHZhbHVlWzo0XSArICIqIiAqIChsZW4odmFsdWUpIC0gOCkgKyB2YWx1ZVstNDpdDQoNCg0KZGVmIF9zZXR1cF9sb2dnaW5nKCkgLT4gTm9uZToNCiAgICAiIiLorr7nva7ml6Xlv5ciIiINCiAgICBsb2dfbGV2ZWxfc3RyID0gb3MuZW52aXJvbi5nZXQoIlRBQkxFX0xPR19MRVZFTCIsICJERUJVRyIpLnVwcGVyKCkNCiAgICBsb2dfbGV2ZWwgPSBnZXRhdHRyKGxvZ2dpbmcsIGxvZ19sZXZlbF9zdHIsIGxvZ2dpbmcuREVCVUcpDQogICAgDQogICAgIyDphY3nva7moLnml6Xlv5forrDlvZXlmagNCiAgICBsb2dnaW5nLmJhc2ljQ29uZmlnKA0KICAgICAgICBsZXZlbD1sb2dfbGV2ZWwsDQogICAgICAgIGZvcm1hdD0nJShhc2N0aW1lKXMgWyUobGV2ZWxuYW1lKXNdICUobmFtZSlzOiAlKG1lc3NhZ2UpcycsDQogICAgICAgIGhhbmRsZXJzPVsNCiAgICAgICAgICAgIGxvZ2dpbmcuU3RyZWFtSGFuZGxlcihzeXMuc3RkZXJyKQ0KICAgICAgICBdDQogICAgKQ0KICAgIA0KICAgICMg5aaC5p6c5oyH5a6a5LqG5pel5b+X5paH5Lu277yM5re75Yqg5paH5Lu25aSE55CG5ZmoDQogICAgbG9nX2ZpbGUgPSBvcy5lbnZpcm9uLmdldCgiVEFCTEVfTE9HX0ZJTEUiLCAiIikuc3RyaXAoKQ0KICAgIGlmIGxvZ19maWxlOg0KICAgICAgICB0cnk6DQogICAgICAgICAgICBmaWxlX2hhbmRsZXIgPSBsb2dnaW5nLkZpbGVIYW5kbGVyKGxvZ19maWxlLCBlbmNvZGluZz0ndXRmLTgnKQ0KICAgICAgICAgICAgZmlsZV9oYW5kbGVyLnNldExldmVsKGxvZ19sZXZlbCkNCiAgICAgICAgICAgIGZpbGVfaGFuZGxlci5zZXRGb3JtYXR0ZXIobG9nZ2luZy5Gb3JtYXR0ZXIoDQogICAgICAgICAgICAgICAgJyUoYXNjdGltZSlzIFslKGxldmVsbmFtZSlzXSAlKG5hbWUpczogJShtZXNzYWdlKXMnDQogICAgICAgICAgICApKQ0KICAgICAgICAgICAgbG9nZ2luZy5nZXRMb2dnZXIoKS5hZGRIYW5kbGVyKGZpbGVfaGFuZGxlcikNCiAgICAgICAgICAgIExPR0dFUi5pbmZvKCLml6Xlv5fmlofku7blt7LphY3nva46ICVzIiwgbG9nX2ZpbGUpDQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToNCiAgICAgICAgICAgIExPR0dFUi53YXJuaW5nKCLphY3nva7ml6Xlv5fmlofku7blpLHotKU6ICVzIiwgZSkNCg0KDQpkZWYgX2dldF9pbmZsdXhfY29uZmlnKCkgLT4gRGljdFtzdHIsIHN0cl06DQogICAgIiIi6I635Y+WSW5mbHV4RELphY3nva4iIiINCiAgICBjb25maWcgPSB7DQogICAgICAgICd1cmwnOiBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX1VSTCIsICIiKS5zdHJpcCgpLA0KICAgICAgICAnb3JnJzogb3MuZW52aXJvbi5nZXQoIklORkxVWF9PUkciLCAiIikuc3RyaXAoKSwNCiAgICAgICAgJ3Rva2VuJzogb3MuZW52aXJvbi5nZXQoIklORkxVWF9UT0tFTiIsICIiKS5zdHJpcCgpLA0KICAgICAgICAnYnVja2V0Jzogb3MuZW52aXJvbi5nZXQoIklORkxVWF9CVUNLRVQiLCAiUENNIikuc3RyaXAoKSwNCiAgICAgICAgJ21lYXN1cmVtZW50Jzogb3MuZW52aXJvbi5nZXQoIklORkxVWF9NRUFTVVJFTUVOVCIsICJQQ01fTWVhc3VyZW1lbnQiKS5zdHJpcCgpLA0KICAgIH0NCiAgICANCiAgICBMT0dHRVIuZGVidWcoDQogICAgICAgICJJbmZsdXhEQumFjee9rjogdXJsPSVzIG9yZz0lcyB0b2tlbj0lcyBidWNrZXQ9JXMgbWVhc3VyZW1lbnQ9JXMiLA0KICAgICAgICBjb25maWdbJ3VybCddIG9yICI8ZW1wdHk+IiwNCiAgICAgICAgY29uZmlnWydvcmcnXSBvciAiPGVtcHR5PiIsDQogICAgICAgIF9tYXNrX3NlY3JldChjb25maWdbJ3Rva2VuJ10pLA0KICAgICAgICBjb25maWdbJ2J1Y2tldCddLA0KICAgICAgICBjb25maWdbJ21lYXN1cmVtZW50J10sDQogICAgKQ0KICAgIA0KICAgIHJldHVybiBjb25maWcNCg0KDQpkZWYgX3BhcnNlX2V4cGVyaW1lbnRfdGltZXMoKSAtPiB0dXBsZVtPcHRpb25hbFtkYXRldGltZV0sIE9wdGlvbmFsW2RhdGV0aW1lXV06DQogICAgIiIi6Kej5p6Q5a6e6aqM5pe26Ze077yM5YmN56uv5Lyg5YWl5pys5Zyw5pe26Ze077yM6L2s5o2i5Li6VVRD55So5LqOSW5mbHV4RELmn6Xor6IiIiINCiAgICBmcm9tIGRhdGV0aW1lIGltcG9ydCB0aW1lem9uZSwgdGltZWRlbHRhDQogICAgDQogICAgc3RhcnRfc3RyID0gb3MuZW52aXJvbi5nZXQoIkVYUEVSSU1FTlRfU1RBUlQiLCAiIikuc3RyaXAoKQ0KICAgIGVuZF9zdHIgPSBvcy5lbnZpcm9uLmdldCgiRVhQRVJJTUVOVF9FTkQiLCAiIikuc3RyaXAoKQ0KICAgIA0KICAgIExPR0dFUi5kZWJ1Zygi5Y6f5aeL5pe26Ze05a2X56ym5LiyOiBTVEFSVD0lcywgRU5EPSVzIiwgc3RhcnRfc3RyLCBlbmRfc3RyKQ0KICAgIA0KICAgIHN0YXJ0X3RpbWU6IE9wdGlvbmFsW2RhdGV0aW1lXSA9IE5vbmUNCiAgICBlbmRfdGltZTogT3B0aW9uYWxbZGF0ZXRpbWVdID0gTm9uZQ0KICAgIA0KICAgIGlmIHN0YXJ0X3N0cjoNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgZm9yIGZtdCBpbiBbIiVZLSVtLSVkVCVIOiVNOiVTIiwgIiVZLSVtLSVkVCVIOiVNOiVTLiVmIl06DQogICAgICAgICAgICAgICAgdHJ5Og0KICAgICAgICAgICAgICAgICAgICBzdGFydF90aW1lID0gZGF0ZXRpbWUuc3RycHRpbWUoc3RhcnRfc3RyLCBmbXQpDQogICAgICAgICAgICAgICAgICAgICMg5pys5Zyw5pe26Ze0LTjlsI/ml7Y9VVRDDQogICAgICAgICAgICAgICAgICAgIHN0YXJ0X3RpbWUgPSBzdGFydF90aW1lIC0gdGltZWRlbHRhKGhvdXJzPTgpDQogICAgICAgICAgICAgICAgICAgIHN0YXJ0X3RpbWUgPSBzdGFydF90aW1lLnJlcGxhY2UodHppbmZvPXRpbWV6b25lLnV0YykNCiAgICAgICAgICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCLop6PmnpBTVEFSVDog5pys5ZywPSVzIOKGkiBVVEM9JXMiLCBzdGFydF9zdHIsIHN0YXJ0X3RpbWUpDQogICAgICAgICAgICAgICAgICAgIGJyZWFrDQogICAgICAgICAgICAgICAgZXhjZXB0IFZhbHVlRXJyb3I6DQogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlDQogICAgICAgICAgICBpZiBzdGFydF90aW1lIGlzIE5vbmU6DQogICAgICAgICAgICAgICAgTE9HR0VSLndhcm5pbmcoIuaXoOazleino+aekEVYUEVSSU1FTlRfU1RBUlQ6ICVzIiwgc3RhcnRfc3RyKQ0KICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgICAgICBMT0dHRVIuZXJyb3IoIuino+aekEVYUEVSSU1FTlRfU1RBUlTlpLHotKUgJyVzJzogJXMiLCBzdGFydF9zdHIsIGUpDQogICAgDQogICAgaWYgZW5kX3N0cjoNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgZm9yIGZtdCBpbiBbIiVZLSVtLSVkVCVIOiVNOiVTIiwgIiVZLSVtLSVkVCVIOiVNOiVTLiVmIl06DQogICAgICAgICAgICAgICAgdHJ5Og0KICAgICAgICAgICAgICAgICAgICBlbmRfdGltZSA9IGRhdGV0aW1lLnN0cnB0aW1lKGVuZF9zdHIsIGZtdCkNCiAgICAgICAgICAgICAgICAgICAgIyDmnKzlnLDml7bpl7QtOOWwj+aXtj1VVEMNCiAgICAgICAgICAgICAgICAgICAgZW5kX3RpbWUgPSBlbmRfdGltZSAtIHRpbWVkZWx0YShob3Vycz04KQ0KICAgICAgICAgICAgICAgICAgICBlbmRfdGltZSA9IGVuZF90aW1lLnJlcGxhY2UodHppbmZvPXRpbWV6b25lLnV0YykNCiAgICAgICAgICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCLop6PmnpBFTkQ6IOacrOWcsD0lcyDihpIgVVRDPSVzIiwgZW5kX3N0ciwgZW5kX3RpbWUpDQogICAgICAgICAgICAgICAgICAgIGJyZWFrDQogICAgICAgICAgICAgICAgZXhjZXB0IFZhbHVlRXJyb3I6DQogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlDQogICAgICAgICAgICBpZiBlbmRfdGltZSBpcyBOb25lOg0KICAgICAgICAgICAgICAgIExPR0dFUi53YXJuaW5nKCLml6Dms5Xop6PmnpBFWFBFUklNRU5UX0VORDogJXMiLCBlbmRfc3RyKQ0KICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgICAgICBMT0dHRVIuZXJyb3IoIuino+aekEVYUEVSSU1FTlRfRU5E5aSx6LSlICclcyc6ICVzIiwgZW5kX3N0ciwgZSkNCiAgICANCiAgICByZXR1cm4gc3RhcnRfdGltZSwgZW5kX3RpbWUNCg0KDQpkZWYgX3BhcnNlX3RpbWVfc2xvdChzbG90X3N0cjogc3RyKSAtPiBmbG9hdDoNCiAgICAiIiLop6PmnpDml7bpl7Tmp73lrZfnrKbkuLLkuLrlsI/ml7bmlbAiIiINCiAgICBpZiBub3Qgc2xvdF9zdHI6DQogICAgICAgIHJldHVybiAwLjANCiAgICANCiAgICBzbG90X3N0ciA9IHNsb3Rfc3RyLnN0cmlwKCkubG93ZXIoKQ0KICAgIA0KICAgIGlmIHNsb3Rfc3RyLmVuZHN3aXRoKCdoJyk6DQogICAgICAgIHRyeToNCiAgICAgICAgICAgIHJldHVybiBmbG9hdChzbG90X3N0cls6LTFdKQ0KICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoNCiAgICAgICAgICAgIHBhc3MNCiAgICANCiAgICB0cnk6DQogICAgICAgIHJldHVybiBmbG9hdChzbG90X3N0cikNCiAgICBleGNlcHQgVmFsdWVFcnJvcjoNCiAgICAgICAgcGFzcw0KICAgIA0KICAgIHJldHVybiAwLjANCg0KDQpkZWYgX3RpbWVfc2xvdHMoKSAtPiBMaXN0W3N0cl06DQogICAgcmF3ID0gb3MuZW52aXJvbi5nZXQoIlRBQkxFX1RJTUVfU0xPVFMiLCAiIikuc3RyaXAoKQ0KICAgIGlmIG5vdCByYXc6DQogICAgICAgICMg5qC55o2u5Zu+54mH77yM5pe26Ze05Yi75bqm5piv77yaMC41aCwgMWgsIDEuNWgsIDJoLCAyLjVoLCAzaCwgMy41aO+8iDfliJfvvIkNCiAgICAgICAgcmV0dXJuIFsiMC41aCIsICIxaCIsICIxLjVoIiwgIjJoIiwgIjIuNWgiLCAiM2giLCAiMy41aCJdDQogICAgc2xvdHMgPSBbc2xvdC5zdHJpcCgpIGZvciBzbG90IGluIHJhdy5zcGxpdCgiLCIpXQ0KICAgIHJldHVybiBbc2xvdCBmb3Igc2xvdCBpbiBzbG90cyBpZiBzbG90XQ0KDQoNCmRlZiBfZGVmYXVsdF9zZWN0aW9ucygpIC0+IExpc3RbRGljdFtzdHIsIEFueV1dOg0KICAgICMgbmFtZSAtPiByb3dzIHVuZGVybmVhdGjvvIhlbnRyaWVz77yJDQogICAgIyDmr4/kuKogZW50cnkg5a+55bqU5LiA5Liq5rWL6K+V6YOo5L2N77yM6ZyA6KaB5pig5bCE5YiwIEluZmx1eERCIOeahCBmaWVsZCDmiJYgdGFnDQogICAgcmV0dXJuIFsNCiAgICAgICAgeyJuYW1lIjogIuS4u+i9tOaJvyIsICJlbnRyaWVzIjogWw0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjMSIsICJmaWVsZCI6ICLkuLvovbTmib8jMSIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5Li76L205om/IzEifSwNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzIiLCAiZmllbGQiOiAi5Li76L205om/IzIiLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIkxTREFRIn0sICJyZXN1bHRfa2V5IjogIuS4u+i9tOaJvyMyIn0sDQogICAgICAgICAgICB7ImxhYmVsIjogIiMzIiwgImZpZWxkIjogIuS4u+i9tOaJvyMzIiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLkuLvovbTmib8jMyJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjNCIsICJmaWVsZCI6ICLkuLvovbTmib8jNCIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5Li76L205om/IzQifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLljYHlrZflpLQiLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzEiLCAiZmllbGQiOiAi5Y2B5a2X5aS0IzEiLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIkxTREFRIn0sICJyZXN1bHRfa2V5IjogIuWNgeWtl+WktCMxIn0sDQogICAgICAgICAgICB7ImxhYmVsIjogIiMyIiwgImZpZWxkIjogIuWNgeWtl+WktCMyIiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLljYHlrZflpLQjMiJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjMyIsICJmaWVsZCI6ICLljYHlrZflpLQjMyIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5Y2B5a2X5aS0IzMifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLlh4/pgJ/nrrHlsI/ovbTmib8iLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzHvvIjovpPlhaXms5XlhbDnq6/vvIkiLCAiZmllbGQiOiAi5YeP6YCf566x5bCP6L205om/MSIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5YeP6YCf566x5bCP6L205om/IzEifSwNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzIiLCAiZmllbGQiOiAi5YeP6YCf566x5bCP6L205om/IzIiLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIkxTREFRIn0sICJyZXN1bHRfa2V5IjogIuWHj+mAn+euseWwj+i9tOaJvyMyIn0sDQogICAgICAgIF19LA0KICAgICAgICB7Im5hbWUiOiAi5YeP6YCf566x5aSn6L205om/IiwgImVudHJpZXMiOiBbDQogICAgICAgICAgICB7ImxhYmVsIjogIiMz77yI5aSn56uv55uW56uv77yJIiwgImZpZWxkIjogIuWHj+mAn+euseWkp+i9tOaJvyMzIiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLlh4/pgJ/nrrHlpKfovbTmib8jMyJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjNCIsICJmaWVsZCI6ICLlh4/pgJ/nrrHlpKfovbTmib8jNCIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5YeP6YCf566x5aSn6L205om/IzQifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLmtqbmu5HmsrnmuKkiLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIiwgImZpZWxkIjogIm1lYW4iLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIua2pua7keayuea4qSJ9LCAicmVzdWx0X2tleSI6ICLmtqbmu5HmsrnmuKkifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLmtqbmu5HmsrnljosiLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiKFBzaSkiLCAiZmllbGQiOiAibWVhbiIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAi5ram5ruR5rK55Y6LIn0sICJyZXN1bHRfa2V5IjogIua2pua7keayueWOiyJ9LA0KICAgICAgICBdfSwNCiAgICBdDQoNCmRlZiBfcXVlcnlfbG9hZF9zdGF0dXNfdGltZWxpbmUoDQogICAgc3RhcnRfdGltZTogZGF0ZXRpbWUsDQogICAgZW5kX3RpbWU6IGRhdGV0aW1lLA0KICAgIGluZmx1eF91cmw6IHN0ciwNCiAgICBpbmZsdXhfb3JnOiBzdHIsDQogICAgaW5mbHV4X3Rva2VuOiBzdHIsDQogICAgaW5mbHV4X2J1Y2tldDogc3RyLA0KICAgIGluZmx1eF9tZWFzdXJlbWVudDogc3RyLA0KKSAtPiBMaXN0W0RpY3Rbc3RyLCBBbnldXToNCiAgICAiIiLmn6Xor6LmlbTkuKrlrp7pqozmnJ/pl7TnmoRsb2FkX3N0YXR1c+aXtumXtOe6v+aVsOaNriIiIg0KICAgIHRyeToNCiAgICAgICAgZnJvbSBpbmZsdXhkYl9jbGllbnQgaW1wb3J0IEluZmx1eERCQ2xpZW50DQogICAgICAgIGltcG9ydCBwYW5kYXMgYXMgcGQNCiAgICAgICAgaW1wb3J0IHdhcm5pbmdzDQogICAgICAgIGZyb20gaW5mbHV4ZGJfY2xpZW50LmNsaWVudC53YXJuaW5ncyBpbXBvcnQgTWlzc2luZ1Bpdm90RnVuY3Rpb24NCiAgICBleGNlcHQgSW1wb3J0RXJyb3IgYXMgZToNCiAgICAgICAgTE9HR0VSLmVycm9yKCJJbmZsdXhEQuWuouaIt+err+WvvOWFpeWksei0pTogJXPvvIzor7flronoo4U6IHBpcCBpbnN0YWxsIGluZmx1eGRiLWNsaWVudCBwYW5kYXMiLCBlKQ0KICAgICAgICByZXR1cm4gW10NCg0KICAgIHRyeToNCiAgICAgICAgY2xpZW50ID0gSW5mbHV4REJDbGllbnQodXJsPWluZmx1eF91cmwsIG9yZz1pbmZsdXhfb3JnLCB0b2tlbj1pbmZsdXhfdG9rZW4pDQogICAgICAgIHF1ZXJ5X2FwaSA9IGNsaWVudC5xdWVyeV9hcGkoKQ0KDQogICAgICAgICMg56Gu5L+d5L2/55SoVVRD5pe26Ze05qC85byP5p+l6K+iDQogICAgICAgIHN0YXJ0X3JmYyA9IHN0YXJ0X3RpbWUuc3RyZnRpbWUoJyVZLSVtLSVkVCVIOiVNOiVTWicpDQogICAgICAgIGVuZF9yZmMgPSBlbmRfdGltZS5zdHJmdGltZSgnJVktJW0tJWRUJUg6JU06JVNaJykNCiAgICAgICAgDQogICAgICAgIExPR0dFUi5kZWJ1Zygi5p+l6K+ibG9hZF9zdGF0dXPml7bpl7TojIPlm7Q6ICVzIOWIsCAlcyIsIHN0YXJ0X3JmYywgZW5kX3JmYykNCg0KICAgICAgICBmbHV4ID0gZicnJw0KZnJvbShidWNrZXQ6ICJ7aW5mbHV4X2J1Y2tldH0iKQ0KICB8PiByYW5nZShzdGFydDoge3N0YXJ0X3JmY30sIHN0b3A6IHtlbmRfcmZjfSkNCiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsiX21lYXN1cmVtZW50Il0gPT0gIntpbmZsdXhfbWVhc3VyZW1lbnR9IikNCiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsiZGF0YV90eXBlIl0gPT0gIkJyZWFrZXIiKQ0KICB8PiBmaWx0ZXIoZm46IChyKSA9PiByWyJfZmllbGQiXSA9PSAibG9hZF9zdGF0dXMiKQ0KICB8PiBzb3J0KGNvbHVtbnM6IFsiX3RpbWUiXSkNCiAgfD4geWllbGQobmFtZTogImxvYWRfc3RhdHVzX3RpbWVsaW5lIikNCicnJy5zdHJpcCgpDQoNCiAgICAgICAgTE9HR0VSLmRlYnVnKCJMb2FkIHN0YXR1cyB0aW1lbGluZSBxdWVyeTpcbiVzIiwgZmx1eCkNCg0KICAgICAgICB3aXRoIHdhcm5pbmdzLmNhdGNoX3dhcm5pbmdzKCk6DQogICAgICAgICAgICB3YXJuaW5ncy5zaW1wbGVmaWx0ZXIoImlnbm9yZSIsIE1pc3NpbmdQaXZvdEZ1bmN0aW9uKQ0KICAgICAgICAgICAgZnJhbWVzID0gcXVlcnlfYXBpLnF1ZXJ5X2RhdGFfZnJhbWUoZmx1eCkNCiAgICAgICAgDQogICAgICAgIGlmIGlzaW5zdGFuY2UoZnJhbWVzLCBsaXN0KToNCiAgICAgICAgICAgIGRmID0gcGQuY29uY2F0KGZyYW1lcywgaWdub3JlX2luZGV4PVRydWUpIGlmIGZyYW1lcyBlbHNlIHBkLkRhdGFGcmFtZSgpDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBkZiA9IGZyYW1lcw0KDQogICAgICAgIGlmIGRmLmVtcHR5IG9yICdfdmFsdWUnIG5vdCBpbiBkZi5jb2x1bW5zIG9yICdfdGltZScgbm90IGluIGRmLmNvbHVtbnM6DQogICAgICAgICAgICBMT0dHRVIud2FybmluZygiTm8gbG9hZF9zdGF0dXMgdGltZWxpbmUgZGF0YSBmb3VuZCIpDQogICAgICAgICAgICByZXR1cm4gW10NCg0KICAgICAgICAjIOi9rOaNouS4uuaXtumXtOe6v+aVsOaNru+8jOS/neaMgVVUQ+aXtuWMug0KICAgICAgICBmcm9tIGRhdGV0aW1lIGltcG9ydCB0aW1lem9uZQ0KICAgICAgICB0aW1lbGluZSA9IFtdDQogICAgICAgIGZvciBfLCByb3cgaW4gZGYuaXRlcnJvd3MoKToNCiAgICAgICAgICAgIHRpbWVfb2JqID0gcGQudG9fZGF0ZXRpbWUocm93WydfdGltZSddKQ0KICAgICAgICAgICAgIyDnoa7kv53ovazmjaLkuLpVVEPml7bljLrnmoRkYXRldGltZeWvueixoQ0KICAgICAgICAgICAgaWYgaGFzYXR0cih0aW1lX29iaiwgJ3R6X2xvY2FsaXplJyk6DQogICAgICAgICAgICAgICAgaWYgdGltZV9vYmoudHogaXMgTm9uZToNCiAgICAgICAgICAgICAgICAgICAgdGltZV9vYmogPSB0aW1lX29iai50el9sb2NhbGl6ZSh0aW1lem9uZS51dGMpDQogICAgICAgICAgICAgICAgZWxzZToNCiAgICAgICAgICAgICAgICAgICAgdGltZV9vYmogPSB0aW1lX29iai50el9jb252ZXJ0KHRpbWV6b25lLnV0YykNCiAgICAgICAgICAgIA0KICAgICAgICAgICAgaWYgaGFzYXR0cih0aW1lX29iaiwgJ3RvX3B5ZGF0ZXRpbWUnKToNCiAgICAgICAgICAgICAgICB0aW1lX29iaiA9IHRpbWVfb2JqLnRvX3B5ZGF0ZXRpbWUoKQ0KICAgICAgICAgICAgICAgIA0KICAgICAgICAgICAgdGltZWxpbmUuYXBwZW5kKHsNCiAgICAgICAgICAgICAgICAndGltZSc6IHRpbWVfb2JqLA0KICAgICAgICAgICAgICAgICdsb2FkX3N0YXR1cyc6IGZsb2F0KHJvd1snX3ZhbHVlJ10pDQogICAgICAgICAgICB9KQ0KDQogICAgICAgIExPR0dFUi5pbmZvKCJMb2FkIHN0YXR1cyB0aW1lbGluZTogJWQgZGF0YSBwb2ludHMgZnJvbSAlcyB0byAlcyIsIA0KICAgICAgICAgICAgICAgICAgIGxlbih0aW1lbGluZSksIHN0YXJ0X3RpbWUsIGVuZF90aW1lKQ0KICAgICAgICANCiAgICAgICAgIyDosIPor5XvvJrmo4Dmn6Xml7bpl7Tlr7nosaHnsbvlnosNCiAgICAgICAgaWYgdGltZWxpbmU6DQogICAgICAgICAgICBmaXJzdF90aW1lID0gdGltZWxpbmVbMF1bJ3RpbWUnXQ0KICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCJUaW1lbGluZSBmaXJzdCB0aW1lOiAlcyAodHlwZTogJXMsIHR6aW5mbzogJXMpIiwgDQogICAgICAgICAgICAgICAgICAgICAgICBmaXJzdF90aW1lLCB0eXBlKGZpcnN0X3RpbWUpLCBnZXRhdHRyKGZpcnN0X3RpbWUsICd0emluZm8nLCBOb25lKSkNCiAgICAgICAgTE9HR0VSLmRlYnVnKCJzdGFydF90aW1lOiAlcyAodHlwZTogJXMsIHR6aW5mbzogJXMpIiwgDQogICAgICAgICAgICAgICAgICAgIHN0YXJ0X3RpbWUsIHR5cGUoc3RhcnRfdGltZSksIGdldGF0dHIoc3RhcnRfdGltZSwgJ3R6aW5mbycsIE5vbmUpKQ0KICAgICAgICBMT0dHRVIuZGVidWcoImVuZF90aW1lOiAlcyAodHlwZTogJXMsIHR6aW5mbzogJXMpIiwgDQogICAgICAgICAgICAgICAgICAgIGVuZF90aW1lLCB0eXBlKGVuZF90aW1lKSwgZ2V0YXR0cihlbmRfdGltZSwgJ3R6aW5mbycsIE5vbmUpKQ0KICAgICAgICANCiAgICAgICAgcmV0dXJuIHRpbWVsaW5lDQoNCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgIExPR0dFUi5lcnJvcigiRXJyb3IgcXVlcnlpbmcgbG9hZF9zdGF0dXMgdGltZWxpbmU6ICVzIiwgZSkNCiAgICAgICAgcmV0dXJuIFtdDQogICAgZmluYWxseToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgY2xpZW50LmNsb3NlKCkNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoNCiAgICAgICAgICAgIHBhc3MNCg0KDQpkZWYgX2NhbGN1bGF0ZV9lZmZlY3RpdmVfdGltZV9wb2ludHMoDQogICAgc3RhcnRfdGltZTogZGF0ZXRpbWUsDQogICAgZW5kX3RpbWU6IGRhdGV0aW1lLA0KICAgIHRpbWVfc2xvdHM6IExpc3Rbc3RyXSwNCiAgICBpbmZsdXhfY29uZmlnOiBEaWN0W3N0ciwgc3RyXQ0KKSAtPiBEaWN0W3N0ciwgT3B0aW9uYWxbZGF0ZXRpbWVdXToNCiAgICAiIiLorqHnrpfln7rkuo7mnInmlYjov5DooYzml7bpl7TntK/orqHnmoTnnJ/lrp7ml7bpl7TngrkiIiINCiAgICANCiAgICAjIDEuIOiOt+WPlmxvYWRfc3RhdHVz5pe26Ze057q/DQogICAgdGltZWxpbmUgPSBfcXVlcnlfbG9hZF9zdGF0dXNfdGltZWxpbmUoDQogICAgICAgIHN0YXJ0X3RpbWUsIGVuZF90aW1lLA0KICAgICAgICBpbmZsdXhfY29uZmlnWyd1cmwnXSwgaW5mbHV4X2NvbmZpZ1snb3JnJ10sIGluZmx1eF9jb25maWdbJ3Rva2VuJ10sDQogICAgICAgIGluZmx1eF9jb25maWdbJ2J1Y2tldCddLCBpbmZsdXhfY29uZmlnWydtZWFzdXJlbWVudCddDQogICAgKQ0KICAgIA0KICAgIGlmIG5vdCB0aW1lbGluZToNCiAgICAgICAgTE9HR0VSLndhcm5pbmcoIk5vIGxvYWRfc3RhdHVzIHRpbWVsaW5lIGRhdGEsIGZhbGxiYWNrIHRvIG9yaWdpbmFsIHRpbWUgY2FsY3VsYXRpb24iKQ0KICAgICAgICAjIOWbnumAgOWIsOWOn+Wni+aXtumXtOiuoeeulw0KICAgICAgICByZXN1bHQgPSB7fQ0KICAgICAgICBmb3Igc2xvdF9zdHIgaW4gdGltZV9zbG90czoNCiAgICAgICAgICAgIHNsb3RfaG91cnMgPSBfcGFyc2VfdGltZV9zbG90KHNsb3Rfc3RyKQ0KICAgICAgICAgICAgcmVzdWx0W3Nsb3Rfc3RyXSA9IHN0YXJ0X3RpbWUgKyB0aW1lZGVsdGEoaG91cnM9c2xvdF9ob3VycykNCiAgICAgICAgcmV0dXJuIHJlc3VsdA0KICAgIA0KICAgICMgMi4g6K6h566X5pyJ5pWI6L+Q6KGM5pe26Ze05q61DQogICAgZWZmZWN0aXZlX3BlcmlvZHMgPSBbXQ0KICAgIGN1cnJlbnRfcGVyaW9kX3N0YXJ0ID0gTm9uZQ0KICAgIA0KICAgIGZvciBpLCBwb2ludCBpbiBlbnVtZXJhdGUodGltZWxpbmUpOg0KICAgICAgICBpZiBwb2ludFsnbG9hZF9zdGF0dXMnXSA9PSAxLjA6DQogICAgICAgICAgICBpZiBjdXJyZW50X3BlcmlvZF9zdGFydCBpcyBOb25lOg0KICAgICAgICAgICAgICAgIGN1cnJlbnRfcGVyaW9kX3N0YXJ0ID0gcG9pbnRbJ3RpbWUnXQ0KICAgICAgICBlbHNlOiAgIyBsb2FkX3N0YXR1cyAhPSAxLjANCiAgICAgICAgICAgIGlmIGN1cnJlbnRfcGVyaW9kX3N0YXJ0IGlzIG5vdCBOb25lOg0KICAgICAgICAgICAgICAgIGVmZmVjdGl2ZV9wZXJpb2RzLmFwcGVuZCh7DQogICAgICAgICAgICAgICAgICAgICdzdGFydCc6IGN1cnJlbnRfcGVyaW9kX3N0YXJ0LA0KICAgICAgICAgICAgICAgICAgICAnZW5kJzogcG9pbnRbJ3RpbWUnXSwNCiAgICAgICAgICAgICAgICAgICAgJ2R1cmF0aW9uX2hvdXJzJzogKHBvaW50Wyd0aW1lJ10gLSBjdXJyZW50X3BlcmlvZF9zdGFydCkudG90YWxfc2Vjb25kcygpIC8gMzYwMC4wDQogICAgICAgICAgICAgICAgfSkNCiAgICAgICAgICAgICAgICBjdXJyZW50X3BlcmlvZF9zdGFydCA9IE5vbmUNCiAgICANCiAgICAjIOWkhOeQhuacgOWQjuS4gOS4quWRqOacn++8iOWmguaenOWunumqjOe7k+adn+aXtuS7jeWcqOi/kOihjO+8iQ0KICAgIGlmIGN1cnJlbnRfcGVyaW9kX3N0YXJ0IGlzIG5vdCBOb25lOg0KICAgICAgICBlZmZlY3RpdmVfcGVyaW9kcy5hcHBlbmQoew0KICAgICAgICAgICAgJ3N0YXJ0JzogY3VycmVudF9wZXJpb2Rfc3RhcnQsDQogICAgICAgICAgICAnZW5kJzogZW5kX3RpbWUsDQogICAgICAgICAgICAnZHVyYXRpb25faG91cnMnOiAoZW5kX3RpbWUgLSBjdXJyZW50X3BlcmlvZF9zdGFydCkudG90YWxfc2Vjb25kcygpIC8gMzYwMC4wDQogICAgICAgIH0pDQogICAgDQogICAgdG90YWxfZWZmZWN0aXZlX2hvdXJzID0gc3VtKHBlcmlvZFsnZHVyYXRpb25faG91cnMnXSBmb3IgcGVyaW9kIGluIGVmZmVjdGl2ZV9wZXJpb2RzKQ0KICAgIExPR0dFUi5pbmZvKCJFZmZlY3RpdmUgcnVubmluZyBwZXJpb2RzOiAlZCBwZXJpb2RzLCB0b3RhbCAlLjNmIGhvdXJzIiwgDQogICAgICAgICAgICAgICBsZW4oZWZmZWN0aXZlX3BlcmlvZHMpLCB0b3RhbF9lZmZlY3RpdmVfaG91cnMpDQogICAgDQogICAgZm9yIHBlcmlvZCBpbiBlZmZlY3RpdmVfcGVyaW9kczoNCiAgICAgICAgTE9HR0VSLmRlYnVnKCJFZmZlY3RpdmUgcGVyaW9kOiAlcyDihpIgJXMgKCUuM2YgaG91cnMpIiwNCiAgICAgICAgICAgICAgICAgICAgcGVyaW9kWydzdGFydCddLnN0cmZ0aW1lKCclSDolTTolUycpLA0KICAgICAgICAgICAgICAgICAgICBwZXJpb2RbJ2VuZCddLnN0cmZ0aW1lKCclSDolTTolUycpLA0KICAgICAgICAgICAgICAgICAgICBwZXJpb2RbJ2R1cmF0aW9uX2hvdXJzJ10pDQogICAgDQogICAgIyAzLiDorqHnrpfmr4/kuKrml7bpl7Tmp73lr7nlupTnmoTnnJ/lrp7ml7bpl7TngrkNCiAgICBlZmZlY3RpdmVfdGltZV9wb2ludHMgPSB7fQ0KICAgIA0KICAgIGZvciBzbG90X3N0ciBpbiB0aW1lX3Nsb3RzOg0KICAgICAgICB0YXJnZXRfZWZmZWN0aXZlX2hvdXJzID0gX3BhcnNlX3RpbWVfc2xvdChzbG90X3N0cikNCiAgICAgICAgDQogICAgICAgIGlmIHRhcmdldF9lZmZlY3RpdmVfaG91cnMgPD0gMDoNCiAgICAgICAgICAgIGVmZmVjdGl2ZV90aW1lX3BvaW50c1tzbG90X3N0cl0gPSBOb25lDQogICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICANCiAgICAgICAgaWYgdGFyZ2V0X2VmZmVjdGl2ZV9ob3VycyA+IHRvdGFsX2VmZmVjdGl2ZV9ob3VyczoNCiAgICAgICAgICAgIExPR0dFUi53YXJuaW5nKCJUYXJnZXQgZWZmZWN0aXZlIHRpbWUgJS4zZmggZXhjZWVkcyB0b3RhbCBlZmZlY3RpdmUgdGltZSAlLjNmaCBmb3Igc2xvdCAlcyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIHRhcmdldF9lZmZlY3RpdmVfaG91cnMsIHRvdGFsX2VmZmVjdGl2ZV9ob3Vycywgc2xvdF9zdHIpDQogICAgICAgICAgICBlZmZlY3RpdmVfdGltZV9wb2ludHNbc2xvdF9zdHJdID0gTm9uZQ0KICAgICAgICAgICAgY29udGludWUNCiAgICAgICAgDQogICAgICAgICMg5Zyo5pyJ5pWI5pe26Ze05q615Lit5p+l5om+57Sv6K6h6L+Q6KGMdGFyZ2V0X2VmZmVjdGl2ZV9ob3Vyc+Wwj+aXtueahOaXtumXtOeCuQ0KICAgICAgICBjdW11bGF0aXZlX2hvdXJzID0gMC4wDQogICAgICAgIHRhcmdldF90aW1lX3BvaW50ID0gTm9uZQ0KICAgICAgICANCiAgICAgICAgZm9yIHBlcmlvZCBpbiBlZmZlY3RpdmVfcGVyaW9kczoNCiAgICAgICAgICAgIHBlcmlvZF9kdXJhdGlvbiA9IHBlcmlvZFsnZHVyYXRpb25faG91cnMnXQ0KICAgICAgICAgICAgDQogICAgICAgICAgICBpZiBjdW11bGF0aXZlX2hvdXJzICsgcGVyaW9kX2R1cmF0aW9uID49IHRhcmdldF9lZmZlY3RpdmVfaG91cnM6DQogICAgICAgICAgICAgICAgIyDnm67moIfml7bpl7TngrnlnKjov5nkuKrlkajmnJ/lhoUNCiAgICAgICAgICAgICAgICByZW1haW5pbmdfaG91cnMgPSB0YXJnZXRfZWZmZWN0aXZlX2hvdXJzIC0gY3VtdWxhdGl2ZV9ob3Vycw0KICAgICAgICAgICAgICAgIHRhcmdldF90aW1lX3BvaW50ID0gcGVyaW9kWydzdGFydCddICsgdGltZWRlbHRhKGhvdXJzPXJlbWFpbmluZ19ob3VycykNCiAgICAgICAgICAgICAgICBicmVhaw0KICAgICAgICAgICAgZWxzZToNCiAgICAgICAgICAgICAgICBjdW11bGF0aXZlX2hvdXJzICs9IHBlcmlvZF9kdXJhdGlvbg0KICAgICAgICANCiAgICAgICAgZWZmZWN0aXZlX3RpbWVfcG9pbnRzW3Nsb3Rfc3RyXSA9IHRhcmdldF90aW1lX3BvaW50DQogICAgICAgIA0KICAgICAgICBpZiB0YXJnZXRfdGltZV9wb2ludDoNCiAgICAgICAgICAgIExPR0dFUi5pbmZvKCJTbG90ICVzOiBlZmZlY3RpdmUgJS4zZmgg4oaSIGFjdHVhbCB0aW1lICVzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgc2xvdF9zdHIsIHRhcmdldF9lZmZlY3RpdmVfaG91cnMsIHRhcmdldF90aW1lX3BvaW50LnN0cmZ0aW1lKCclSDolTTolUycpKQ0KICAgICAgICBlbHNlOg0KICAgICAgICAgICAgTE9HR0VSLndhcm5pbmcoIkNvdWxkIG5vdCBjYWxjdWxhdGUgZWZmZWN0aXZlIHRpbWUgcG9pbnQgZm9yIHNsb3QgJXMiLCBzbG90X3N0cikNCiAgICANCiAgICByZXR1cm4gZWZmZWN0aXZlX3RpbWVfcG9pbnRzDQoNCg0KZGVmIF9xdWVyeV9pbmZsdXhkYl9yYW5nZV93aXRoX2xvYWRfc3RhdHVzKA0KICAgIGZpZWxkX25hbWU6IHN0ciwNCiAgICBzdGFydF90aW1lOiBkYXRldGltZSwNCiAgICBlbmRfdGltZTogZGF0ZXRpbWUsDQogICAgaW5mbHV4X3VybDogc3RyLA0KICAgIGluZmx1eF9vcmc6IHN0ciwNCiAgICBpbmZsdXhfdG9rZW46IHN0ciwNCiAgICBpbmZsdXhfYnVja2V0OiBzdHIsDQogICAgaW5mbHV4X21lYXN1cmVtZW50OiBzdHIsDQogICAgZmlsdGVyczogT3B0aW9uYWxbRGljdFtzdHIsIHN0cl1dID0gTm9uZSwNCikgLT4gT3B0aW9uYWxbZmxvYXRdOg0KICAgICIiIuafpeivoiBJbmZsdXhEQiDojrflj5bmjIflrprlrZfmrrXlnKjml7bpl7TojIPlm7TlhoXnmoTlubPlnYflgLzvvIjku4XlvZMgbG9hZF9zdGF0dXMgPSAxIOaXtu+8iSIiIg0KICAgIHRyeToNCiAgICAgICAgZnJvbSBpbmZsdXhkYl9jbGllbnQgaW1wb3J0IEluZmx1eERCQ2xpZW50DQogICAgICAgIGltcG9ydCBwYW5kYXMgYXMgcGQNCiAgICAgICAgaW1wb3J0IHdhcm5pbmdzDQogICAgICAgIGZyb20gaW5mbHV4ZGJfY2xpZW50LmNsaWVudC53YXJuaW5ncyBpbXBvcnQgTWlzc2luZ1Bpdm90RnVuY3Rpb24NCiAgICBleGNlcHQgSW1wb3J0RXJyb3I6DQogICAgICAgIExPR0dFUi53YXJuaW5nKCJJbmZsdXhEQiBjbGllbnQgbm90IGF2YWlsYWJsZSwgc2tpcCBxdWVyeSBmb3IgZmllbGQ9JXMiLCBmaWVsZF9uYW1lKQ0KICAgICAgICByZXR1cm4gTm9uZQ0KDQogICAgdHJ5Og0KICAgICAgICBjbGllbnQgPSBJbmZsdXhEQkNsaWVudCh1cmw9aW5mbHV4X3VybCwgb3JnPWluZmx1eF9vcmcsIHRva2VuPWluZmx1eF90b2tlbikNCiAgICAgICAgcXVlcnlfYXBpID0gY2xpZW50LnF1ZXJ5X2FwaSgpDQoNCiAgICAgICAgIyDnoa7kv53kvb/nlKhVVEPml7bpl7TmoLzlvI8NCiAgICAgICAgc3RhcnRfcmZjID0gc3RhcnRfdGltZS5zdHJmdGltZSgnJVktJW0tJWRUJUg6JU06JVNaJykNCiAgICAgICAgZW5kX3JmYyA9IGVuZF90aW1lLnN0cmZ0aW1lKCclWS0lbS0lZFQlSDolTTolU1onKQ0KICAgICAgICANCiAgICAgICAgTE9HR0VSLmRlYnVnKCLmn6Xor6LlrZfmrrUgJXMg5pe26Ze06IyD5Zu0OiAlcyDliLAgJXMiLCBmaWVsZF9uYW1lLCBzdGFydF9yZmMsIGVuZF9yZmMpDQoNCiAgICAgICAgIyDmnoTlu7rov4fmu6TmnaHku7YNCiAgICAgICAgdGFnX2ZpbHRlcnMgPSAiIg0KICAgICAgICBpZiBmaWx0ZXJzOg0KICAgICAgICAgICAgZm9yIGtleSwgdmFsdWUgaW4gZmlsdGVycy5pdGVtcygpOg0KICAgICAgICAgICAgICAgIHRhZ19maWx0ZXJzICs9IGYnXG4gIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIntrZXl9Il0gPT0gInt2YWx1ZX0iKScNCg0KICAgICAgICAjIOWvueS6jueOr+Wig+a4qeW6pu+8jOWPluWFqOmDqOmdnjDmlbDmja7nmoTlnYflgLzvvJvlhbbku5blrZfmrrXku43pnIBsb2FkX3N0YXR1cz0x562b6YCJDQogICAgICAgIGlmIGZpZWxkX25hbWUgPT0gIueOr+Wig+a4qeW6piI6DQogICAgICAgICAgICBmbHV4ID0gZicnJw0KZnJvbShidWNrZXQ6ICJ7aW5mbHV4X2J1Y2tldH0iKQ0KICB8PiByYW5nZShzdGFydDoge3N0YXJ0X3JmY30sIHN0b3A6IHtlbmRfcmZjfSkNCiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsiX21lYXN1cmVtZW50Il0gPT0gIntpbmZsdXhfbWVhc3VyZW1lbnR9IikNCiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsiX2ZpZWxkIl0gPT0gIntmaWVsZF9uYW1lfSIpDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl92YWx1ZSJdICE9IDAuMCl7dGFnX2ZpbHRlcnN9DQogIHw+IG1lYW4oKQ0KICB8PiB5aWVsZChuYW1lOiAibWVhbl9ub25femVybyIpDQonJycuc3RyaXAoKQ0KICAgICAgICBlbHNlOg0KICAgICAgICAgICAgZmx1eCA9IGYnJycNCmZyb20oYnVja2V0OiAie2luZmx1eF9idWNrZXR9IikNCiAgfD4gcmFuZ2Uoc3RhcnQ6IHtzdGFydF9yZmN9LCBzdG9wOiB7ZW5kX3JmY30pDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9tZWFzdXJlbWVudCJdID09ICJ7aW5mbHV4X21lYXN1cmVtZW50fSIpDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9maWVsZCJdID09ICJ7ZmllbGRfbmFtZX0iKXt0YWdfZmlsdGVyc30NCiAgfD4gbWVhbigpDQogIHw+IHlpZWxkKG5hbWU6ICJtZWFuX3RlbXBlcmF0dXJlX2RhdGEiKQ0KJycnLnN0cmlwKCkNCg0KICAgICAgICBMT0dHRVIuZGVidWcoIkZsdXjmn6Xor6Lor63lj6UgKHJhbmdlKTpcbiVzIiwgZmx1eCkNCg0KICAgICAgICB3aXRoIHdhcm5pbmdzLmNhdGNoX3dhcm5pbmdzKCk6DQogICAgICAgICAgICB3YXJuaW5ncy5zaW1wbGVmaWx0ZXIoImlnbm9yZSIsIE1pc3NpbmdQaXZvdEZ1bmN0aW9uKQ0KICAgICAgICAgICAgZnJhbWVzID0gcXVlcnlfYXBpLnF1ZXJ5X2RhdGFfZnJhbWUoZmx1eCkNCiAgICAgICAgDQogICAgICAgIGlmIGlzaW5zdGFuY2UoZnJhbWVzLCBsaXN0KToNCiAgICAgICAgICAgIGRmID0gcGQuY29uY2F0KGZyYW1lcywgaWdub3JlX2luZGV4PVRydWUpIGlmIGZyYW1lcyBlbHNlIHBkLkRhdGFGcmFtZSgpDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBkZiA9IGZyYW1lcw0KDQogICAgICAgIGlmIGRmLmVtcHR5IG9yICdfdmFsdWUnIG5vdCBpbiBkZi5jb2x1bW5zOg0KICAgICAgICAgICAgaWYgZmllbGRfbmFtZSA9PSAi546v5aKD5rip5bqmIjoNCiAgICAgICAgICAgICAgICBMT0dHRVIuZGVidWcoIk5vIHZhbGlkIHJhbmdlIGRhdGEgZm91bmQgZm9yIGZpZWxkPSVzIChub24temVybyBkYXRhKSIsIGZpZWxkX25hbWUpDQogICAgICAgICAgICBlbHNlOg0KICAgICAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygiTm8gdmFsaWQgcmFuZ2UgZGF0YSBmb3VuZCBmb3IgZmllbGQ9JXMiLCBmaWVsZF9uYW1lKQ0KICAgICAgICAgICAgcmV0dXJuIE5vbmUNCiAgICAgICAgICAgIA0KICAgICAgICBtZWFuX3ZhbHVlID0gZGZbJ192YWx1ZSddLmlsb2NbMF0NCiAgICAgICAgaWYgcGQuaXNuYShtZWFuX3ZhbHVlKToNCiAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygiTWVhbiB2YWx1ZSBpcyBOYU4gZm9yIGZpZWxkPSVzIiwgZmllbGRfbmFtZSkNCiAgICAgICAgICAgIHJldHVybiBOb25lDQoNCiAgICAgICAgdmFsdWUgPSBmbG9hdChtZWFuX3ZhbHVlKQ0KICAgICAgICBpZiBmaWVsZF9uYW1lID09ICLnjq/looPmuKnluqYiOg0KICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCJGaWVsZD0lcyByYW5nZV9tZWFuX3ZhbHVlPSUuM2YgKG5vbi16ZXJvIGRhdGEpIiwgZmllbGRfbmFtZSwgdmFsdWUpDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBMT0dHRVIuZGVidWcoIkZpZWxkPSVzIHJhbmdlX21lYW5fdmFsdWU9JS4zZiIsIGZpZWxkX25hbWUsIHZhbHVlKQ0KICAgICAgICByZXR1cm4gdmFsdWUNCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgIExPR0dFUi5lcnJvcigiRXJyb3IgcXVlcnlpbmcgSW5mbHV4REIgcmFuZ2UgZm9yIGZpZWxkPSVzOiAlcyIsIGZpZWxkX25hbWUsIGUpDQogICAgICAgIHJldHVybiBOb25lDQogICAgZmluYWxseToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgY2xpZW50LmNsb3NlKCkNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoNCiAgICAgICAgICAgIHBhc3MNCg0KDQpkZWYgX3F1ZXJ5X2luZmx1eGRiX3dpdGhfbG9hZF9zdGF0dXMoDQogICAgZmllbGRfbmFtZTogc3RyLA0KICAgIHRhcmdldF90aW1lOiBkYXRldGltZSwNCiAgICBpbmZsdXhfdXJsOiBzdHIsDQogICAgaW5mbHV4X29yZzogc3RyLA0KICAgIGluZmx1eF90b2tlbjogc3RyLA0KICAgIGluZmx1eF9idWNrZXQ6IHN0ciwNCiAgICBpbmZsdXhfbWVhc3VyZW1lbnQ6IHN0ciwNCiAgICBmaWx0ZXJzOiBPcHRpb25hbFtEaWN0W3N0ciwgc3RyXV0gPSBOb25lLA0KKSAtPiBPcHRpb25hbFtmbG9hdF06DQogICAgIiIi5p+l6K+iIEluZmx1eERCIOiOt+WPluaMh+WumuWtl+auteWcqOaMh+WumuaXtumXtOeCueeahOeerOaXtuWAvO+8iOS7heW9kyBsb2FkX3N0YXR1cyA9IDEg5pe277yJIiIiDQogICAgdHJ5Og0KICAgICAgICBmcm9tIGluZmx1eGRiX2NsaWVudCBpbXBvcnQgSW5mbHV4REJDbGllbnQNCiAgICAgICAgaW1wb3J0IHBhbmRhcyBhcyBwZA0KICAgICAgICBpbXBvcnQgd2FybmluZ3MNCiAgICAgICAgZnJvbSBpbmZsdXhkYl9jbGllbnQuY2xpZW50Lndhcm5pbmdzIGltcG9ydCBNaXNzaW5nUGl2b3RGdW5jdGlvbg0KICAgIGV4Y2VwdCBJbXBvcnRFcnJvcjoNCiAgICAgICAgTE9HR0VSLndhcm5pbmcoIkluZmx1eERCIGNsaWVudCBub3QgYXZhaWxhYmxlLCBza2lwIHF1ZXJ5IGZvciBmaWVsZD0lcyIsIGZpZWxkX25hbWUpDQogICAgICAgIHJldHVybiBOb25lDQoNCiAgICB0cnk6DQogICAgICAgIGNsaWVudCA9IEluZmx1eERCQ2xpZW50KHVybD1pbmZsdXhfdXJsLCBvcmc9aW5mbHV4X29yZywgdG9rZW49aW5mbHV4X3Rva2VuKQ0KICAgICAgICBxdWVyeV9hcGkgPSBjbGllbnQucXVlcnlfYXBpKCkNCg0KICAgICAgICAjIOehruS/neS9v+eUqFVUQ+aXtumXtA0KICAgICAgICB0YXJnZXRfdGltZV9yZmMgPSB0YXJnZXRfdGltZS5zdHJmdGltZSgnJVktJW0tJWRUJUg6JU06JVNaJykNCiAgICAgICAgDQogICAgICAgIExPR0dFUi5kZWJ1ZygNCiAgICAgICAgICAgICLmn6Xor6LlrZfmrrU9JXMg55uu5qCH5pe26Ze0PSVzIChVVEMpIOi/h+a7pOWZqD0lcyIsDQogICAgICAgICAgICBmaWVsZF9uYW1lLA0KICAgICAgICAgICAgdGFyZ2V0X3RpbWVfcmZjLA0KICAgICAgICAgICAgZmlsdGVycyBvciB7fSwNCiAgICAgICAgKQ0KDQogICAgICAgICMg5L2/55So5pe26Ze056qX5Y+j5p+l5om+5pyA5o6l6L+R55qE5pWw5o2u54K5DQogICAgICAgIHdpbmRvd19taW51dGVzID0gMTANCiAgICAgICAgDQogICAgICAgIHF1ZXJ5X3N0YXJ0ID0gdGFyZ2V0X3RpbWUgLSB0aW1lZGVsdGEobWludXRlcz13aW5kb3dfbWludXRlcykNCiAgICAgICAgcXVlcnlfZW5kID0gdGFyZ2V0X3RpbWUgKyB0aW1lZGVsdGEobWludXRlcz13aW5kb3dfbWludXRlcykNCiAgICAgICAgDQogICAgICAgIHF1ZXJ5X3N0YXJ0X3JmYyA9IHF1ZXJ5X3N0YXJ0LnN0cmZ0aW1lKCclWS0lbS0lZFQlSDolTTolU1onKQ0KICAgICAgICBxdWVyeV9lbmRfcmZjID0gcXVlcnlfZW5kLnN0cmZ0aW1lKCclWS0lbS0lZFQlSDolTTolU1onKQ0KICAgICAgICANCiAgICAgICAgTE9HR0VSLmRlYnVnKCLmn6Xor6Lnqpflj6M6ICVzIOWIsCAlcyIsIHF1ZXJ5X3N0YXJ0X3JmYywgcXVlcnlfZW5kX3JmYykNCg0KICAgICAgICAjIOaehOW7uui/h+a7pOadoeS7tg0KICAgICAgICB0YWdfZmlsdGVycyA9ICIiDQogICAgICAgIGlmIGZpbHRlcnM6DQogICAgICAgICAgICBmb3Iga2V5LCB2YWx1ZSBpbiBmaWx0ZXJzLml0ZW1zKCk6DQogICAgICAgICAgICAgICAgdGFnX2ZpbHRlcnMgKz0gZidcbiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsie2tleX0iXSA9PSAie3ZhbHVlfSIpJw0KDQogICAgICAgICMg5p+l6K+i5rip5bqm5pWw5o2u77yI5LiN6ZyA6KaBbG9hZF9zdGF0dXPnrZvpgInvvIzlm6DkuLrlt7Lnu4/ln7rkuo7mnInmlYjml7bpl7Tngrnmn6Xor6LvvIkNCiAgICAgICAgZmx1eCA9IGYnJycNCmZyb20oYnVja2V0OiAie2luZmx1eF9idWNrZXR9IikNCiAgfD4gcmFuZ2Uoc3RhcnQ6IHtxdWVyeV9zdGFydF9yZmN9LCBzdG9wOiB7cXVlcnlfZW5kX3JmY30pDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9tZWFzdXJlbWVudCJdID09ICJ7aW5mbHV4X21lYXN1cmVtZW50fSIpDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9maWVsZCJdID09ICJ7ZmllbGRfbmFtZX0iKXt0YWdfZmlsdGVyc30NCiAgfD4gc29ydChjb2x1bW5zOiBbIl90aW1lIl0pDQogIHw+IGxhc3QoKQ0KICB8PiB5aWVsZChuYW1lOiAiaW5zdGFudGFuZW91c19hdF9lZmZlY3RpdmVfdGltZSIpDQonJycuc3RyaXAoKQ0KDQogICAgICAgIExPR0dFUi5kZWJ1ZygiRmx1eOafpeivouivreWPpTpcbiVzIiwgZmx1eCkNCg0KICAgICAgICB3aXRoIHdhcm5pbmdzLmNhdGNoX3dhcm5pbmdzKCk6DQogICAgICAgICAgICB3YXJuaW5ncy5zaW1wbGVmaWx0ZXIoImlnbm9yZSIsIE1pc3NpbmdQaXZvdEZ1bmN0aW9uKQ0KICAgICAgICAgICAgZnJhbWVzID0gcXVlcnlfYXBpLnF1ZXJ5X2RhdGFfZnJhbWUoZmx1eCkNCiAgICAgICAgDQogICAgICAgIGlmIGlzaW5zdGFuY2UoZnJhbWVzLCBsaXN0KToNCiAgICAgICAgICAgIGRmID0gcGQuY29uY2F0KGZyYW1lcywgaWdub3JlX2luZGV4PVRydWUpIGlmIGZyYW1lcyBlbHNlIHBkLkRhdGFGcmFtZSgpDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBkZiA9IGZyYW1lcw0KDQogICAgICAgICMg6I635Y+W556s5pe25YC877yI5pyA6L+R55qE5LiA5Liq5pyJ5pWI5pWw5o2u54K577yJDQogICAgICAgIGlmIGRmLmVtcHR5IG9yICdfdmFsdWUnIG5vdCBpbiBkZi5jb2x1bW5zOg0KICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCJObyB2YWxpZCBkYXRhIGZvdW5kIGZvciBmaWVsZD0lcyBhdCBlZmZlY3RpdmUgdGltZSBwb2ludCIsIGZpZWxkX25hbWUpDQogICAgICAgICAgICByZXR1cm4gTm9uZQ0KICAgICAgICAgICAgDQogICAgICAgICMg5Y+W56ys5LiA6KGM55qE5YC877yI5Zug5Li65p+l6K+i5bey57uP5o6S5bqP5bm25Y+W5LqGbGFzdCgp77yJDQogICAgICAgIGluc3RhbnRfdmFsdWUgPSBkZlsnX3ZhbHVlJ10uaWxvY1swXQ0KICAgICAgICBpZiBwZC5pc25hKGluc3RhbnRfdmFsdWUpOg0KICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCJJbnN0YW50YW5lb3VzIHZhbHVlIGlzIE5hTiBmb3IgZmllbGQ9JXMiLCBmaWVsZF9uYW1lKQ0KICAgICAgICAgICAgcmV0dXJuIE5vbmUNCg0KICAgICAgICB2YWx1ZSA9IGZsb2F0KGluc3RhbnRfdmFsdWUpDQogICAgICAgIA0KICAgICAgICAjIOWmguaenOacieaXtumXtOS/oeaBr++8jOiusOW9leWunumZheeahOaVsOaNruaXtumXtOeCuQ0KICAgICAgICBpZiAnX3RpbWUnIGluIGRmLmNvbHVtbnM6DQogICAgICAgICAgICBhY3R1YWxfdGltZSA9IGRmWydfdGltZSddLmlsb2NbMF0NCiAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygiRmllbGQ9JXMgaW5zdGFudGFuZW91c192YWx1ZT0lLjNmIGFjdHVhbF90aW1lPSVzIChhdCBlZmZlY3RpdmUgdGltZSkiLCANCiAgICAgICAgICAgICAgICAgICAgICAgIGZpZWxkX25hbWUsIHZhbHVlLCBhY3R1YWxfdGltZSkNCiAgICAgICAgZWxzZToNCiAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygiRmllbGQ9JXMgaW5zdGFudGFuZW91c192YWx1ZT0lLjNmIChhdCBlZmZlY3RpdmUgdGltZSkiLCBmaWVsZF9uYW1lLCB2YWx1ZSkNCiAgICAgICAgICAgIA0KICAgICAgICByZXR1cm4gdmFsdWUNCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgIExPR0dFUi5lcnJvcigiRXJyb3IgcXVlcnlpbmcgSW5mbHV4REIgZm9yIGZpZWxkPSVzOiAlcyIsIGZpZWxkX25hbWUsIGUpDQogICAgICAgIHJldHVybiBOb25lDQogICAgZmluYWxseToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgY2xpZW50LmNsb3NlKCkNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoNCiAgICAgICAgICAgIHBhc3MNCg0KDQpkZWYgX2xvYWRfdGVtcGVyYXR1cmVfZGF0YV93aXRoX2xvYWRfc3RhdHVzKA0KICAgIHRpbWVfc2xvdHM6IExpc3Rbc3RyXSwNCiAgICBzZWN0aW9uczogTGlzdFtEaWN0W3N0ciwgQW55XV0sDQogICAgc3RhcnRfdGltZTogT3B0aW9uYWxbZGF0ZXRpbWVdLA0KICAgIGVuZF90aW1lOiBPcHRpb25hbFtkYXRldGltZV0sDQopIC0+IERpY3Rbc3RyLCBEaWN0W3N0ciwgZmxvYXRdXToNCiAgICAiIiLku44gSW5mbHV4REIg5p+l6K+i5omA5pyJ5rWL6K+V6YOo5L2N5Zyo5ZCE5pe26Ze054K555qE556s5pe25rip5bqm5YC877yI5LuF5b2TIGxvYWRfc3RhdHVzID0gMSDml7bvvIkiIiINCiAgICBpZiBub3Qgc3RhcnRfdGltZSBvciBub3QgZW5kX3RpbWU6DQogICAgICAgIExPR0dFUi5pbmZvKCJTa2lwIGRhdGEgcXVlcnk6IG1pc3Npbmcgc3RhcnQvZW5kICglcywgJXMpIiwgc3RhcnRfdGltZSwgZW5kX3RpbWUpDQogICAgICAgIHJldHVybiB7fQ0KICAgIA0KICAgIGluZmx1eF9jb25maWcgPSBfZ2V0X2luZmx1eF9jb25maWcoKQ0KICAgIA0KICAgIGlmIG5vdCBhbGwoW2luZmx1eF9jb25maWdbJ3VybCddLCBpbmZsdXhfY29uZmlnWydvcmcnXSwgaW5mbHV4X2NvbmZpZ1sndG9rZW4nXSwgDQogICAgICAgICAgICAgICAgaW5mbHV4X2NvbmZpZ1snYnVja2V0J10sIGluZmx1eF9jb25maWdbJ21lYXN1cmVtZW50J11dKToNCiAgICAgICAgTE9HR0VSLndhcm5pbmcoDQogICAgICAgICAgICAiU2tpcCBkYXRhIHF1ZXJ5OiBtaXNzaW5nIEluZmx1eCBjb25maWcgdXJsPSVzIGJ1Y2tldD0lcyBtZWFzdXJlbWVudD0lcyIsDQogICAgICAgICAgICBpbmZsdXhfY29uZmlnWyd1cmwnXSBvciAiPGVtcHR5PiIsDQogICAgICAgICAgICBpbmZsdXhfY29uZmlnWydidWNrZXQnXSBvciAiPGVtcHR5PiIsDQogICAgICAgICAgICBpbmZsdXhfY29uZmlnWydtZWFzdXJlbWVudCddIG9yICI8ZW1wdHk+IiwNCiAgICAgICAgKQ0KICAgICAgICByZXR1cm4ge30NCiAgICANCiAgICAjIOiuoeeul+aAu+aXtumVv++8iOWwj+aXtu+8iQ0KICAgIHRvdGFsX2R1cmF0aW9uID0gKGVuZF90aW1lIC0gc3RhcnRfdGltZSkudG90YWxfc2Vjb25kcygpIC8gMzYwMC4wDQogICAgTE9HR0VSLmluZm8oDQogICAgICAgICJGZXRjaCBpbnN0YW50YW5lb3VzIHRlbXBlcmF0dXJlIGRhdGEgKGxvYWRfc3RhdHVzPTEpIHdpbmRvdz0lc+KGkiVzIHRvdGFsX2hvdXJzPSUuM2YgdGltZV9wb2ludHM9JXMiLA0KICAgICAgICBzdGFydF90aW1lLmlzb2Zvcm1hdCgpLA0KICAgICAgICBlbmRfdGltZS5pc29mb3JtYXQoKSwNCiAgICAgICAgdG90YWxfZHVyYXRpb24sDQogICAgICAgICIsIi5qb2luKHRpbWVfc2xvdHMpLA0KICAgICkNCiAgICANCiAgICAjIOaUtumbhuaJgOaciemcgOimgeafpeivoueahOWtl+autQ0KICAgIHF1ZXJ5X3RhcmdldHM6IExpc3RbdHVwbGVbc3RyLCBEaWN0W3N0ciwgQW55XV1dID0gW10NCiAgICBmb3Igc2VjdGlvbiBpbiBzZWN0aW9uczoNCiAgICAgICAgZW50cmllcyA9IHNlY3Rpb24uZ2V0KCJlbnRyaWVzIikgb3IgW10NCiAgICAgICAgZm9yIGVudHJ5IGluIGVudHJpZXM6DQogICAgICAgICAgICBpZiBpc2luc3RhbmNlKGVudHJ5LCBkaWN0KToNCiAgICAgICAgICAgICAgICBmaWVsZF9uYW1lID0gZW50cnkuZ2V0KCJmaWVsZCIsICIiKQ0KICAgICAgICAgICAgICAgIGlmIGZpZWxkX25hbWU6DQogICAgICAgICAgICAgICAgICAgIHF1ZXJ5X3RhcmdldHMuYXBwZW5kKChmaWVsZF9uYW1lLCBlbnRyeSkpDQoNCiAgICBpZiBub3QgcXVlcnlfdGFyZ2V0czoNCiAgICAgICAgcmV0dXJuIHt9DQogICAgDQogICAgIyDorqHnrpfln7rkuo7mnInmlYjov5DooYzml7bpl7TntK/orqHnmoTnnJ/lrp7ml7bpl7TngrkNCiAgICBMT0dHRVIuaW5mbygiPT09IOW8gOWni+iuoeeul+acieaViOaXtumXtOeCuSA9PT0iKQ0KICAgIGVmZmVjdGl2ZV90aW1lX3BvaW50cyA9IF9jYWxjdWxhdGVfZWZmZWN0aXZlX3RpbWVfcG9pbnRzKA0KICAgICAgICBzdGFydF90aW1lLCBlbmRfdGltZSwgdGltZV9zbG90cywgaW5mbHV4X2NvbmZpZw0KICAgICkNCiAgICANCiAgICAjIOS4uuavj+S4quacieaViOaXtumXtOeCueafpeivoua4qeW6puaVsOaNrg0KICAgIHRlbXBlcmF0dXJlX2RhdGE6IERpY3Rbc3RyLCBEaWN0W3N0ciwgZmxvYXRdXSA9IHt9DQogICAgDQogICAgZm9yIGlkeCwgc2xvdF9zdHIgaW4gZW51bWVyYXRlKHRpbWVfc2xvdHMpOg0KICAgICAgICB0YXJnZXRfdGltZV9wb2ludCA9IGVmZmVjdGl2ZV90aW1lX3BvaW50cy5nZXQoc2xvdF9zdHIpDQogICAgICAgIA0KICAgICAgICBpZiB0YXJnZXRfdGltZV9wb2ludCBpcyBOb25lOg0KICAgICAgICAgICAgTE9HR0VSLndhcm5pbmcoIk5vIGVmZmVjdGl2ZSB0aW1lIHBvaW50IGZvciBzbG90ICVzLCB1c2luZyBzaW1wbGUgb2Zmc2V0Iiwgc2xvdF9zdHIpDQogICAgICAgICAgICBzbG90X2hvdXJzID0gX3BhcnNlX3RpbWVfc2xvdChzbG90X3N0cikNCiAgICAgICAgICAgIHRhcmdldF90aW1lX3BvaW50ID0gc3RhcnRfdGltZSArIHRpbWVkZWx0YShob3Vycz1zbG90X2hvdXJzKQ0KICAgICAgICAgICAgaWYgdGFyZ2V0X3RpbWVfcG9pbnQgPiBlbmRfdGltZToNCiAgICAgICAgICAgICAgICBMT0dHRVIud2FybmluZygiVGltZSBwb2ludCAlcyBleGNlZWRzIGVuZCB0aW1lLCBza2lwcGluZyIsIHNsb3Rfc3RyKQ0KICAgICAgICAgICAgICAgIGNvbnRpbnVlDQogICAgICAgIA0KICAgICAgICBMT0dHRVIuZGVidWcoIlByb2Nlc3Npbmcgc2xvdCAlcyBhdCBlZmZlY3RpdmUgdGltZSBwb2ludCAlcyIsIA0KICAgICAgICAgICAgICAgICAgICBzbG90X3N0ciwgdGFyZ2V0X3RpbWVfcG9pbnQuc3RyZnRpbWUoJyVZLSVtLSVkICVIOiVNOiVTJykpDQogICAgICAgIA0KICAgICAgICBmb3IgZmllbGRfbmFtZSwgZW50cnkgaW4gcXVlcnlfdGFyZ2V0czoNCiAgICAgICAgICAgIHJlc3VsdF9rZXkgPSBlbnRyeS5nZXQoInJlc3VsdF9rZXkiKSBvciBmaWVsZF9uYW1lDQogICAgICAgICAgICBpZiBub3QgcmVzdWx0X2tleToNCiAgICAgICAgICAgICAgICByZXN1bHRfa2V5ID0gZmllbGRfbmFtZQ0KICAgICAgICAgICAgZW50cnlfZmlsdGVycyA9IGVudHJ5LmdldCgiZmlsdGVycyIpIGlmIGlzaW5zdGFuY2UoZW50cnksIGRpY3QpIGVsc2UgTm9uZQ0KICAgICAgICAgICAgaWYgcmVzdWx0X2tleSBub3QgaW4gdGVtcGVyYXR1cmVfZGF0YToNCiAgICAgICAgICAgICAgICB0ZW1wZXJhdHVyZV9kYXRhW3Jlc3VsdF9rZXldID0ge30NCg0KICAgICAgICAgICAgIyDkvb/nlKjntKLlvJXkvZzkuLprZXnvvIzlm6DkuLrlj6/og73mnInph43lpI3nmoTml7bpl7TliLvluqYNCiAgICAgICAgICAgIHNsb3Rfa2V5ID0gZiJ7aWR4fV97c2xvdF9zdHJ9IiAgIyDkvb/nlKjntKLlvJUr5pe26Ze05Yi75bqm5L2c5Li65ZSv5LiAa2V5DQoNCiAgICAgICAgICAgICMg5p+l6K+i556s5pe25YC877yI5Zyo5pyJ5pWI5pe26Ze054K577yJDQogICAgICAgICAgICB2YWx1ZSA9IF9xdWVyeV9pbmZsdXhkYl93aXRoX2xvYWRfc3RhdHVzKA0KICAgICAgICAgICAgICAgIGZpZWxkX25hbWUsDQogICAgICAgICAgICAgICAgdGFyZ2V0X3RpbWVfcG9pbnQsDQogICAgICAgICAgICAgICAgaW5mbHV4X2NvbmZpZ1sndXJsJ10sDQogICAgICAgICAgICAgICAgaW5mbHV4X2NvbmZpZ1snb3JnJ10sDQogICAgICAgICAgICAgICAgaW5mbHV4X2NvbmZpZ1sndG9rZW4nXSwNCiAgICAgICAgICAgICAgICBpbmZsdXhfY29uZmlnWydidWNrZXQnXSwNCiAgICAgICAgICAgICAgICBpbmZsdXhfY29uZmlnWydtZWFzdXJlbWVudCddLA0KICAgICAgICAgICAgICAgIGZpbHRlcnM9ZW50cnlfZmlsdGVycyBpZiBlbnRyeV9maWx0ZXJzIGVsc2UgTm9uZSwNCiAgICAgICAgICAgICkNCg0KICAgICAgICAgICAgaWYgdmFsdWUgaXMgbm90IE5vbmU6DQogICAgICAgICAgICAgICAgdGVtcGVyYXR1cmVfZGF0YVtyZXN1bHRfa2V5XVtzbG90X2tleV0gPSB2YWx1ZQ0KICAgICAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygNCiAgICAgICAgICAgICAgICAgICAgIlNsb3Q9JXMgZmllbGQ9JXMgdmFsdWU9JS4zZiBhdCBlZmZlY3RpdmVfdGltZT0lcyIsDQogICAgICAgICAgICAgICAgICAgIHNsb3Rfa2V5LA0KICAgICAgICAgICAgICAgICAgICByZXN1bHRfa2V5LA0KICAgICAgICAgICAgICAgICAgICB2YWx1ZSwNCiAgICAgICAgICAgICAgICAgICAgdGFyZ2V0X3RpbWVfcG9pbnQuc3RyZnRpbWUoJyVIOiVNOiVTJykNCiAgICAgICAgICAgICAgICApDQogICAgICAgICAgICBlbHNlOg0KICAgICAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygNCiAgICAgICAgICAgICAgICAgICAgIlNsb3Q9JXMgZmllbGQ9JXMgbm9fZGF0YSBhdCBlZmZlY3RpdmVfdGltZT0lcyIsDQogICAgICAgICAgICAgICAgICAgIHNsb3Rfa2V5LA0KICAgICAgICAgICAgICAgICAgICByZXN1bHRfa2V5LA0KICAgICAgICAgICAgICAgICAgICB0YXJnZXRfdGltZV9wb2ludC5zdHJmdGltZSgnJUg6JU06JVMnKQ0KICAgICAgICAgICAgICAgICkNCg0KICAgIHJldHVybiB0ZW1wZXJhdHVyZV9kYXRhDQoNCg0KZGVmIF9idWlsZF9jZWxsc193aXRoX2xvYWRfc3RhdHVzKA0KICAgIHRpbWVfc2xvdHM6IExpc3Rbc3RyXSwNCiAgICBzZWN0aW9uczogTGlzdFtEaWN0W3N0ciwgQW55XV0sDQogICAgbW90b3Jfc3BlZWQ6IHN0ciwNCiAgICBzdGFydF90aW1lOiBPcHRpb25hbFtkYXRldGltZV0sDQogICAgZW5kX3RpbWU6IE9wdGlvbmFsW2RhdGV0aW1lXSwNCiAgICB0ZW1wZXJhdHVyZV9kYXRhOiBEaWN0W3N0ciwgRGljdFtzdHIsIGZsb2F0XV0sDQogICAgdXNlX2RlZmF1bHRzOiBib29sID0gRmFsc2UsDQopIC0+IExpc3RbRGljdFtzdHIsIEFueV1dOg0KICAgICIiIuaehOW7uuWNleWFg+agvOaVsOaNru+8iOWfuuS6jiBsb2FkX3N0YXR1cyA9IDEg55qE5pyJ5pWI5pWw5o2u77yJLSDkuI7ljp/lp4vohJrmnKznu5PmnoTlrozlhajkuIDoh7QiIiINCiAgICBjZWxsczogTGlzdFtEaWN0W3N0ciwgQW55XV0gPSBbXQ0KDQogICAgZGVmIGFkZF9jZWxsKHJvdzogaW50LCBjb2w6IGludCwgdmFsdWU6IHN0ciA9ICIiLCByb3dzcGFuOiBpbnQgPSAxLCBjb2xzcGFuOiBpbnQgPSAxKSAtPiBOb25lOg0KICAgICAgICBwYXlsb2FkOiBEaWN0W3N0ciwgQW55XSA9IHsicm93Ijogcm93LCAiY29sIjogY29sLCAidmFsdWUiOiB2YWx1ZX0NCiAgICAgICAgaWYgcm93c3BhbiA+IDE6DQogICAgICAgICAgICBwYXlsb2FkWyJyb3dzcGFuIl0gPSByb3dzcGFuDQogICAgICAgIGlmIGNvbHNwYW4gPiAxOg0KICAgICAgICAgICAgcGF5bG9hZFsiY29sc3BhbiJdID0gY29sc3Bhbg0KICAgICAgICBjZWxscy5hcHBlbmQocGF5bG9hZCkNCg0KICAgICMg5qih5p2/5bem5L6n5qCH6aKY5YiX5bey57uP5Y676Zmk77yM6L+Z6YeM5LuF55Sf5oiQ57qv5pWw5o2u5Yy677yM5LuOICgwLDApIOW8gOWni+Whq+WFpeaVsOWAvOOAgg0KICAgICMgY3VycmVudF9yb3cg5a+55bqU5qih5p2/5Lit55qE5a6e6ZmF5pWw5o2u6KGM57Si5byV44CCDQogICAgY3VycmVudF9yb3cgPSAwDQogICAgZm9yIHNlY3Rpb24gaW4gc2VjdGlvbnM6DQogICAgICAgIGVudHJpZXMgPSBzZWN0aW9uLmdldCgiZW50cmllcyIpIG9yIFtdDQogICAgICAgIGlmIG5vdCBlbnRyaWVzOg0KICAgICAgICAgICAgY29udGludWUNCiAgICAgICAgIyDmr4/kuKrmtYvor5Xpg6jkvY3lrZDpobnlr7nlupTmqKHmnb/kuK3nmoTkuIDooYwNCiAgICAgICAgZm9yIGVudHJ5IGluIGVudHJpZXM6DQogICAgICAgICAgICAjIOaUr+aMgeaWsOagvOW8j++8iOW4piBmaWVsZCDmmKDlsITvvInlkozml6fmoLzlvI/vvIjnuq/lrZfnrKbkuLLvvIkNCiAgICAgICAgICAgIGlmIGlzaW5zdGFuY2UoZW50cnksIGRpY3QpOg0KICAgICAgICAgICAgICAgIGZpZWxkX25hbWUgPSBlbnRyeS5nZXQoImZpZWxkIiwgIiIpDQogICAgICAgICAgICAgICAgZW50cnlfZmlsdGVycyA9IGVudHJ5LmdldCgiZmlsdGVycyIpDQogICAgICAgICAgICAgICAgZW50cnlfa2V5ID0gZW50cnkuZ2V0KCJyZXN1bHRfa2V5Iikgb3IgZmllbGRfbmFtZQ0KICAgICAgICAgICAgZWxzZToNCiAgICAgICAgICAgICAgICBmaWVsZF9uYW1lID0gIiINCiAgICAgICAgICAgICAgICBlbnRyeV9maWx0ZXJzID0gTm9uZQ0KICAgICAgICAgICAgICAgIGVudHJ5X2tleSA9ICIiDQoNCiAgICAgICAgICAgICMg5LuF6L6T5Ye65pWw5YC85YiX77ya5YiX57Si5byV55u05o6l5a+55bqU5pe26Ze05q61DQogICAgICAgICAgICAjIOW8uuWItuWhq+WFheaJgOacieWIl++8jOS8mOWFiOS9v+eUqOafpeivouaVsOaNru+8jOWQpuWImeS9v+eUqOm7mOiupOWAvA0KICAgICAgICAgICAgaWYgZmllbGRfbmFtZToNCiAgICAgICAgICAgICAgICB0YXJnZXRfa2V5ID0gZW50cnlfa2V5IG9yIGZpZWxkX25hbWUNCg0KICAgICAgICAgICAgICAgICMg6YGN5Y6G5omA5pyJ5pe26Ze05q615YiX77yM56Gu5L+d5q+P5LiA5YiX6YO95pyJ5pWw5o2uDQogICAgICAgICAgICAgICAgZm9yIGNvbF9pZHgsIHNsb3QgaW4gZW51bWVyYXRlKHRpbWVfc2xvdHMpOg0KICAgICAgICAgICAgICAgICAgICB2YWx1ZSA9IE5vbmUNCg0KICAgICAgICAgICAgICAgICAgICAjIOS8mOWFiOS9v+eUqOafpeivouWIsOeahOaVsOaNrg0KICAgICAgICAgICAgICAgICAgICBpZiB0ZW1wZXJhdHVyZV9kYXRhOg0KICAgICAgICAgICAgICAgICAgICAgICAgc2xvdF9kYXRhID0gdGVtcGVyYXR1cmVfZGF0YS5nZXQodGFyZ2V0X2tleSwge30pDQogICAgICAgICAgICAgICAgICAgICAgICBpZiBzbG90X2RhdGE6DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgc2xvdF9rZXkgPSBmIntjb2xfaWR4fV97c2xvdH0iDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWUgPSBzbG90X2RhdGEuZ2V0KHNsb3Rfa2V5KQ0KDQogICAgICAgICAgICAgICAgICAgIGlmIHZhbHVlIGlzIE5vbmUgYW5kIHVzZV9kZWZhdWx0czoNCiAgICAgICAgICAgICAgICAgICAgICAgICMg5L2/55So5Z+656GA6buY6K6k5YC8ICsg5pe26Ze05q615YGP56e777yI5q+P5Liq5pe26Ze05q615aKe5YqgMC4x5bqm77yJDQogICAgICAgICAgICAgICAgICAgICAgICBkZWZhdWx0X2Jhc2VfdmFsdWUgPSAyNS4wICAjIOeugOWMlueahOm7mOiupOWAvA0KICAgICAgICAgICAgICAgICAgICAgICAgdGltZV9vZmZzZXQgPSBjb2xfaWR4ICogMC4xDQogICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZSA9IGRlZmF1bHRfYmFzZV92YWx1ZSArIHRpbWVfb2Zmc2V0DQoNCiAgICAgICAgICAgICAgICAgICAgaWYgdmFsdWUgaXMgTm9uZToNCiAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlX3N0ciA9ICIiDQogICAgICAgICAgICAgICAgICAgIGVsc2U6DQogICAgICAgICAgICAgICAgICAgICAgICAjIOagvOW8j+WMluS4uuWtl+espuS4su+8iOS/neeVmTHkvY3lsI/mlbDvvIkNCiAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlX3N0ciA9IGYie3ZhbHVlOi4xZn0iDQoNCiAgICAgICAgICAgICAgICAgICAgYWRkX2NlbGwoY3VycmVudF9yb3csIGNvbF9pZHgsIHZhbHVlX3N0cikNCiAgICAgICAgICAgIGVsc2U6DQogICAgICAgICAgICAgICAgIyDlpoLmnpzmsqHmnInlrZfmrrXlkI3vvIzloavlhYXnqbrlrZfnrKbkuLINCiAgICAgICAgICAgICAgICBmb3IgY29sX2lkeCBpbiByYW5nZShsZW4odGltZV9zbG90cykpOg0KICAgICAgICAgICAgICAgICAgICBhZGRfY2VsbChjdXJyZW50X3JvdywgY29sX2lkeCwgIiIpDQogICAgICAgICAgICBjdXJyZW50X3JvdyArPSAxDQoNCiAgICByZXR1cm4gY2VsbHMNCg0KDQpkZWYgYnVpbGRfdGVtcGVyYXR1cmVfdGFibGVfd2l0aF9sb2FkX3N0YXR1cyhfOiBEaWN0W3N0ciwgQW55XSkgLT4gRGljdFtzdHIsIEFueV06DQogICAgIiIi5p6E5bu65rip5bqm6KGo5qC85pWw5o2u77yI5LuF5L2/55SoIGxvYWRfc3RhdHVzID0gMSDnmoTmnInmlYjmlbDmja7vvIkiIiINCiAgICBfc2V0dXBfbG9nZ2luZygpDQogICAgDQogICAgdG9rZW4gPSBvcy5lbnZpcm9uLmdldCgiVEFCTEVfVE9LRU4iLCAic2NyaXB0VGFibGUxIikNCiAgICByb3dfb2Zmc2V0ID0gaW50KG9zLmVudmlyb24uZ2V0KCJUQUJMRV9TVEFSVF9ST1ciLCAiMCIpIG9yIDApDQogICAgY29sX29mZnNldCA9IGludChvcy5lbnZpcm9uLmdldCgiVEFCTEVfU1RBUlRfQ09MIiwgIjAiKSBvciAwKQ0KICAgIG1vdG9yX3NwZWVkID0gb3MuZW52aXJvbi5nZXQoIlRBQkxFX01PVE9SX1NQRUVEIiwgIjk4MFJQTSIpDQogICAgDQogICAgIyDop6PmnpDlrp7pqozml7bpl7TojIPlm7QNCiAgICBzdGFydF90aW1lLCBlbmRfdGltZSA9IF9wYXJzZV9leHBlcmltZW50X3RpbWVzKCkNCiAgICANCiAgICB0aW1lX3Nsb3RzID0gX3RpbWVfc2xvdHMoKQ0KICAgIHNlY3Rpb25zID0gX2RlZmF1bHRfc2VjdGlvbnMoKQ0KICAgIA0KICAgICMg5p+l6K+i5rip5bqm5pWw5o2u77yI5LuF5b2TIGxvYWRfc3RhdHVzID0gMSDml7bvvIkNCiAgICB0ZW1wZXJhdHVyZV9kYXRhID0gX2xvYWRfdGVtcGVyYXR1cmVfZGF0YV93aXRoX2xvYWRfc3RhdHVzKHRpbWVfc2xvdHMsIHNlY3Rpb25zLCBzdGFydF90aW1lLCBlbmRfdGltZSkNCiAgICANCiAgICAjIOWni+e7iOemgeatoum7mOiupOaVsOaNru+8jOS/neivgeafpeivouS4jeWIsOWAvOaXtuS/neaMgeepuueZvQ0KICAgIHVzZV9kZWZhdWx0cyA9IEZhbHNlDQogICAgDQogICAgY2VsbHMgPSBfYnVpbGRfY2VsbHNfd2l0aF9sb2FkX3N0YXR1cygNCiAgICAgICAgdGltZV9zbG90cywgDQogICAgICAgIHNlY3Rpb25zLCANCiAgICAgICAgbW90b3Jfc3BlZWQsIA0KICAgICAgICBzdGFydF90aW1lLCANCiAgICAgICAgZW5kX3RpbWUsIA0KICAgICAgICB0ZW1wZXJhdHVyZV9kYXRhLA0KICAgICAgICB1c2VfZGVmYXVsdHM9dXNlX2RlZmF1bHRzDQogICAgKQ0KICAgIA0KICAgICMg5bqU55So6KGM5YGP56e7DQogICAgZm9yIGNlbGwgaW4gY2VsbHM6DQogICAgICAgIGNlbGxbInJvdyJdICs9IDQNCiAgICANCiAgICAjIOa3u+WKoOWunumqjOaXtumXtOS/oeaBr++8iOS4juWOn+Wni+iEmuacrOWujOWFqOS4gOiHtOeahOmAu+i+ke+8iQ0KICAgIHN0YXJ0X3RpbWVfcm93ID0gMQ0KICAgIHN0YXJ0X3RpbWVfdmFsdWVfY29sID0gMQ0KICAgIGVuZF90aW1lX3ZhbHVlX2NvbCA9IDMNCiAgICANCiAgICAjIOiOt+WPluWOn+Wni+aXtumXtOWtl+espuS4sui/m+ihjOWkhOeQhu+8iOS4juWOn+Wni+iEmuacrOS/neaMgeS4gOiHtO+8iQ0KICAgIHN0YXJ0X3N0ciA9IG9zLmVudmlyb24uZ2V0KCJFWFBFUklNRU5UX1NUQVJUIiwgIiIpLnN0cmlwKCkNCiAgICBpZiBzdGFydF9zdHIgYW5kIHN0YXJ0X3RpbWU6DQogICAgICAgIHRyeToNCiAgICAgICAgICAgICMg5bCd6K+V5bim5pe25Yy65ZKM5LiN5bim5pe25Yy65Lik56eN5qC85byPDQogICAgICAgICAgICB0cnk6DQogICAgICAgICAgICAgICAgdXRjX2F3YXJlX2R0ID0gZGF0ZXRpbWUuc3RycHRpbWUoc3RhcnRfc3RyLCAiJVktJW0tJWRUJUg6JU06JVMleiIpDQogICAgICAgICAgICAgICAgbG9jYWxfZHQxID0gdXRjX2F3YXJlX2R0LmFzdGltZXpvbmUodHo9Tm9uZSkNCiAgICAgICAgICAgIGV4Y2VwdCBWYWx1ZUVycm9yOg0KICAgICAgICAgICAgICAgICMg5LiN5bim5pe25Yy677yM55u05o6l6Kej5p6Q5Li65pys5Zyw5pe26Ze0DQogICAgICAgICAgICAgICAgbG9jYWxfZHQxID0gZGF0ZXRpbWUuc3RycHRpbWUoc3RhcnRfc3RyLCAiJVktJW0tJWRUJUg6JU06JVMiKQ0KICAgICAgICAgICAgDQogICAgICAgICAgICBsb2NhbF9kdDIgPSBsb2NhbF9kdDEgKyB0aW1lZGVsdGEoaG91cnM9My41KQ0KICAgICAgICAgICAgc3RhcnRfdGltZV92YWx1ZSA9IGxvY2FsX2R0MS5zdHJmdGltZSgiJVktJW0tJWQgJUg6JU06JVMiKQ0KICAgICAgICAgICAgZW5kX3RpbWVfdmFsdWUgPSBsb2NhbF9kdDIuc3RyZnRpbWUoIiVZLSVtLSVkICVIOiVNOiVTIikNCiAgICAgICAgICAgIGNlbGxzLmFwcGVuZCh7InJvdyI6IHN0YXJ0X3RpbWVfcm93LCAiY29sIjogc3RhcnRfdGltZV92YWx1ZV9jb2wsICJ2YWx1ZSI6IHN0YXJ0X3RpbWVfdmFsdWV9KQ0KICAgICAgICAgICAgY2VsbHMuYXBwZW5kKHsicm93Ijogc3RhcnRfdGltZV9yb3csICJjb2wiOiBlbmRfdGltZV92YWx1ZV9jb2wsICJ2YWx1ZSI6IGVuZF90aW1lX3ZhbHVlfSkNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOg0KICAgICAgICAgICAgTE9HR0VSLndhcm5pbmcoIkZhaWxlZCB0byBwcm9jZXNzIGV4cGVyaW1lbnQgdGltZSBzdHJpbmdzOiAlcyIsIGUpDQogICAgDQogICAgIyDmn6Xor6Lnjq/looPmuKnluqbvvIjkuI7ljp/lp4vohJrmnKzlrozlhajkuIDoh7TnmoTpgLvovpHvvIkNCiAgICBpbmZsdXhfdXJsID0gb3MuZW52aXJvbi5nZXQoIklORkxVWF9VUkwiLCAiIikuc3RyaXAoKQ0KICAgIGluZmx1eF9vcmcgPSBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX09SRyIsICIiKS5zdHJpcCgpDQogICAgaW5mbHV4X3Rva2VuID0gb3MuZW52aXJvbi5nZXQoIklORkxVWF9UT0tFTiIsICIiKS5zdHJpcCgpDQogICAgaW5mbHV4X2J1Y2tldCA9IG9zLmVudmlyb24uZ2V0KCJJTkZMVVhfQlVDS0VUIiwgIlBDTSIpLnN0cmlwKCkNCiAgICBpbmZsdXhfbWVhc3VyZW1lbnQgPSBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX01FQVNVUkVNRU5UIiwgIlBDTV9NZWFzdXJlbWVudCIpLnN0cmlwKCkNCiAgICANCiAgICBpZiBzdGFydF90aW1lIGFuZCBlbmRfdGltZToNCiAgICAgICAgIyDlr7nkuo7njq/looPmuKnluqbvvIzkvb/nlKjml7bpl7TojIPlm7Tmn6Xor6LvvIjkuI7ljp/lp4vohJrmnKzpgLvovpHkuIDoh7TvvIkNCiAgICAgICAgdmFsdWUgPSBfcXVlcnlfaW5mbHV4ZGJfcmFuZ2Vfd2l0aF9sb2FkX3N0YXR1cygNCiAgICAgICAgICAgICLnjq/looPmuKnluqYiLA0KICAgICAgICAgICAgc3RhcnRfdGltZSwNCiAgICAgICAgICAgIGVuZF90aW1lLA0KICAgICAgICAgICAgaW5mbHV4X3VybCwNCiAgICAgICAgICAgIGluZmx1eF9vcmcsDQogICAgICAgICAgICBpbmZsdXhfdG9rZW4sDQogICAgICAgICAgICBpbmZsdXhfYnVja2V0LA0KICAgICAgICAgICAgaW5mbHV4X21lYXN1cmVtZW50LA0KICAgICAgICAgICAgZmlsdGVycz17ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LA0KICAgICAgICApDQogICAgICAgICMg56Gu5L+ddmFsdWXkuI3mmK9Ob25l77yM6YG/5YWNV29yZCBDT03mk43kvZzlvILluLjvvIjkuI7ljp/lp4vohJrmnKzkuIDoh7TvvIkNCiAgICAgICAgaWYgdmFsdWUgaXMgbm90IE5vbmU6DQogICAgICAgICAgICBjZWxscy5hcHBlbmQoeyJyb3ciOiAwLCAiY29sIjogMSwgInZhbHVlIjogZiJ7dmFsdWU6LjFmfeKEgyJ9KQ0KICAgICAgICBlbHNlOg0KICAgICAgICAgICAgY2VsbHMuYXBwZW5kKHsicm93IjogMCwgImNvbCI6IDEsICJ2YWx1ZSI6ICIifSkNCiAgICANCiAgICBMT0dHRVIuaW5mbygNCiAgICAgICAgIlRlbXBlcmF0dXJlIHRhYmxlIGJ1aWx0IHdpdGggbG9hZF9zdGF0dXM9MSBmaWx0ZXI6IHRva2VuPSVzIGNlbGxzPSVkIHRpbWVfc2xvdHM9JXMiLA0KICAgICAgICB0b2tlbiwNCiAgICAgICAgbGVuKGNlbGxzKSwNCiAgICAgICAgIiwiLmpvaW4odGltZV9zbG90cyksDQogICAgKQ0KICAgIA0KICAgIHJldHVybiB7DQogICAgICAgICJ0b2tlbiI6IHRva2VuLA0KICAgICAgICAic3RhcnRSb3ciOiByb3dfb2Zmc2V0LA0KICAgICAgICAic3RhcnRDb2wiOiBjb2xfb2Zmc2V0LA0KICAgICAgICAiY2VsbHMiOiBjZWxscywNCiAgICB9DQoNCg0KZGVmIF9sb2FkX3BheWxvYWQoKSAtPiBEaWN0W3N0ciwgQW55XToNCiAgICAiIiLku47moIflh4bovpPlhaXmiJbnjq/looPlj5jph4/liqDovb1wYXlsb2Fk5pWw5o2uIiIiDQogICAgdHJ5Og0KICAgICAgICAjIOWwneivleS7juagh+WHhui+k+WFpeivu+WPlkpTT04NCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgaW1wb3J0IHNlbGVjdA0KICAgICAgICAgICAgaWYgc2VsZWN0LnNlbGVjdChbc3lzLnN0ZGluXSwgW10sIFtdLCAwLjApWzBdOg0KICAgICAgICAgICAgICAgIHBheWxvYWRfc3RyID0gc3lzLnN0ZGluLnJlYWQoKS5zdHJpcCgpDQogICAgICAgICAgICAgICAgaWYgcGF5bG9hZF9zdHI6DQogICAgICAgICAgICAgICAgICAgIHJldHVybiBqc29uLmxvYWRzKHBheWxvYWRfc3RyKQ0KICAgICAgICBleGNlcHQgSW1wb3J0RXJyb3I6DQogICAgICAgICAgICAjIFdpbmRvd3PkuIpzZWxlY3Tlj6/og73kuI3lj6/nlKjvvIzlsJ3or5Xnm7TmjqXor7vlj5YNCiAgICAgICAgICAgIGltcG9ydCBtc3ZjcnQNCiAgICAgICAgICAgIGlmIG1zdmNydC5rYmhpdCgpOg0KICAgICAgICAgICAgICAgIHBheWxvYWRfc3RyID0gc3lzLnN0ZGluLnJlYWQoKS5zdHJpcCgpDQogICAgICAgICAgICAgICAgaWYgcGF5bG9hZF9zdHI6DQogICAgICAgICAgICAgICAgICAgIHJldHVybiBqc29uLmxvYWRzKHBheWxvYWRfc3RyKQ0KICAgIGV4Y2VwdCBFeGNlcHRpb246DQogICAgICAgIHBhc3MNCiAgICANCiAgICAjIOWmguaenOayoeacieagh+WHhui+k+WFpe+8jOi/lOWbnuepuuWtl+WFuA0KICAgIHJldHVybiB7fQ0KDQoNCmRlZiBfbG9nX2Vudmlyb25tZW50X3ZhcmlhYmxlcygpIC0+IE5vbmU6DQogICAgIiIi6K6w5b2V55u45YWz546v5aKD5Y+Y6YePIiIiDQogICAgZW52X3ZhcnMgPSBbDQogICAgICAgICJUQUJMRV9UT0tFTiIsICJUQUJMRV9TVEFSVF9ST1ciLCAiVEFCTEVfU1RBUlRfQ09MIiwgIlRBQkxFX1RJTUVfU0xPVFMiLCAiVEFCTEVfTU9UT1JfU1BFRUQiLA0KICAgICAgICAiRVhQRVJJTUVOVF9TVEFSVCIsICJFWFBFUklNRU5UX0VORCIsDQogICAgICAgICJJTkZMVVhfVVJMIiwgIklORkxVWF9PUkciLCAiSU5GTFVYX1RPS0VOIiwgIklORkxVWF9CVUNLRVQiLCAiSU5GTFVYX01FQVNVUkVNRU5UIg0KICAgIF0NCiAgICANCiAgICBmb3IgdmFyIGluIGVudl92YXJzOg0KICAgICAgICB2YWx1ZSA9IG9zLmVudmlyb24uZ2V0KHZhciwgIiIpDQogICAgICAgIGlmICJUT0tFTiIgaW4gdmFyIGFuZCB2YWx1ZToNCiAgICAgICAgICAgIHZhbHVlID0gX21hc2tfc2VjcmV0KHZhbHVlKQ0KICAgICAgICBMT0dHRVIuZGVidWcoIkVOViAlcz0lcyIsIHZhciwgdmFsdWUgb3IgIjxlbXB0eT4iKQ0KDQoNCmRlZiBtYWluKCkgLT4gaW50Og0KICAgIHRyeToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgaWYgbm90IGxvZ2dpbmcuZ2V0TG9nZ2VyKCkuaGFuZGxlcnM6DQogICAgICAgICAgICAgICAgbG9nX2xldmVsX25hbWUgPSBvcy5lbnZpcm9uLmdldCgiVEFCTEVfTE9HX0xFVkVMIiwgIkRFQlVHIikuc3RyaXAoKSBvciAiREVCVUciDQogICAgICAgICAgICAgICAgbG9nX2xldmVsID0gZ2V0YXR0cihsb2dnaW5nLCBsb2dfbGV2ZWxfbmFtZS51cHBlcigpLCBsb2dnaW5nLkRFQlVHKQ0KICAgICAgICAgICAgICAgIGxvZ19maWxlX3JhdyA9IG9zLmVudmlyb24uZ2V0KCJUQUJMRV9MT0dfRklMRSIsICJ0ZXN0LmxvZyIpLnN0cmlwKCkgb3IgInRlc3QubG9nIg0KICAgICAgICAgICAgICAgIGxvZ19maWxlID0gb3MucGF0aC5hYnNwYXRoKGxvZ19maWxlX3JhdykNCg0KICAgICAgICAgICAgICAgIGxvZ2dpbmcuYmFzaWNDb25maWcoDQogICAgICAgICAgICAgICAgICAgIGxldmVsPWxvZ19sZXZlbCwNCiAgICAgICAgICAgICAgICAgICAgZm9ybWF0PSIlKGFzY3RpbWUpcyBbJShsZXZlbG5hbWUpc10gJShuYW1lKXM6ICUobWVzc2FnZSlzIiwNCiAgICAgICAgICAgICAgICAgICAgaGFuZGxlcnM9Ww0KICAgICAgICAgICAgICAgICAgICAgICAgbG9nZ2luZy5GaWxlSGFuZGxlcihsb2dfZmlsZSwgZW5jb2Rpbmc9InV0Zi04IiksDQogICAgICAgICAgICAgICAgICAgICAgICBsb2dnaW5nLlN0cmVhbUhhbmRsZXIoc3lzLnN0ZGVyciksDQogICAgICAgICAgICAgICAgICAgIF0sDQogICAgICAgICAgICAgICAgKQ0KICAgICAgICAgICAgICAgIExPR0dFUi5pbmZvKCJMb2dnaW5nIGluaXRpYWxpemVkIC0+IGZpbGU9JXMgbGV2ZWw9JXMiLCBsb2dfZmlsZSwgbG9nZ2luZy5nZXRMZXZlbE5hbWUobG9nX2xldmVsKSkNCiAgICAgICAgICAgICAgICBfbG9nX2Vudmlyb25tZW50X3ZhcmlhYmxlcygpDQogICAgICAgICAgICBzeXMuc3Rkb3V0LnJlY29uZmlndXJlKGVuY29kaW5nPSJ1dGYtOCIpICAjIHR5cGU6IGlnbm9yZVthdHRyLWRlZmluZWRdDQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246DQogICAgICAgICAgICBwYXNzDQogICAgICAgIA0KICAgICAgICBwYXlsb2FkID0gX2xvYWRfcGF5bG9hZCgpDQogICAgICAgIHRhYmxlX3NwZWMgPSBidWlsZF90ZW1wZXJhdHVyZV90YWJsZV93aXRoX2xvYWRfc3RhdHVzKHBheWxvYWQpDQogICAgICAgIHJlc3VsdCA9IHsidGFibGVzIjogW3RhYmxlX3NwZWNdfQ0KICAgICAgICBwcmludChqc29uLmR1bXBzKHJlc3VsdCwgZW5zdXJlX2FzY2lpPUZhbHNlKSkNCiAgICAgICAgcmV0dXJuIDANCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGV4YzoNCiAgICAgICAgcHJpbnQoZiJlcnJvcjoge2V4Y30iLCBmaWxlPXN5cy5zdGRlcnIpDQogICAgICAgIHJldHVybiAxDQoNCg0KaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoNCiAgICBzeXMuZXhpdChtYWluKCkpDQo=", "scriptName": "table.py", "remark": "PCM性能测试实验" }, diff --git a/configs/600泵/table.py b/configs/600泵/table.py index 7cfe8b3..bcbc588 100644 --- a/configs/600泵/table.py +++ b/configs/600泵/table.py @@ -98,50 +98,40 @@ def _get_influx_config() -> Dict[str, str]: def _parse_experiment_times() -> tuple[Optional[datetime], Optional[datetime]]: - """解析实验时间,前端传入本地时间,转换为UTC用于InfluxDB查询""" - from datetime import timezone, timedelta - + """解析实验时间""" start_str = os.environ.get("EXPERIMENT_START", "").strip() end_str = os.environ.get("EXPERIMENT_END", "").strip() - LOGGER.debug("原始时间字符串: START=%s, END=%s", start_str, end_str) - start_time: Optional[datetime] = None end_time: Optional[datetime] = None if start_str: try: - for fmt in ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f"]: + for fmt in ["%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S%z"]: try: start_time = datetime.strptime(start_str, fmt) - # 本地时间-8小时=UTC - start_time = start_time - timedelta(hours=8) - start_time = start_time.replace(tzinfo=timezone.utc) - LOGGER.debug("解析START: 本地=%s → UTC=%s", start_str, start_time) + if start_time.tzinfo is not None: + # 转换为本地时间并去除时区信息 + start_time = start_time.astimezone(tz=None).replace(tzinfo=None) break except ValueError: continue - if start_time is None: - LOGGER.warning("无法解析EXPERIMENT_START: %s", start_str) except Exception as e: - LOGGER.error("解析EXPERIMENT_START失败 '%s': %s", start_str, e) + print(f"Warning: Failed to parse EXPERIMENT_START '{start_str}': {e}", file=sys.stderr) if end_str: try: - for fmt in ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f"]: + for fmt in ["%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S%z"]: try: end_time = datetime.strptime(end_str, fmt) - # 本地时间-8小时=UTC - end_time = end_time - timedelta(hours=8) - end_time = end_time.replace(tzinfo=timezone.utc) - LOGGER.debug("解析END: 本地=%s → UTC=%s", end_str, end_time) + if end_time.tzinfo is not None: + # 转换为本地时间并去除时区信息 + end_time = end_time.astimezone(tz=None).replace(tzinfo=None) break except ValueError: continue - if end_time is None: - LOGGER.warning("无法解析EXPERIMENT_END: %s", end_str) except Exception as e: - LOGGER.error("解析EXPERIMENT_END失败 '%s': %s", end_str, e) + print(f"Warning: Failed to parse EXPERIMENT_END '{end_str}': {e}", file=sys.stderr) return start_time, end_time @@ -185,11 +175,15 @@ def _default_sections() -> List[Dict[str, Any]]: {"label": "#2", "field": "主轴承#2", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#2"}, {"label": "#3", "field": "主轴承#3", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#3"}, {"label": "#4", "field": "主轴承#4", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#4"}, + {"label": "#5", "field": "主轴承#5", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#5"}, + {"label": "#6", "field": "主轴承#6", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#6"}, ]}, {"name": "十字头", "entries": [ {"label": "#1", "field": "十字头#1", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#1"}, {"label": "#2", "field": "十字头#2", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#2"}, {"label": "#3", "field": "十字头#3", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#3"}, + {"label": "#4", "field": "十字头#4", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#4"}, + {"label": "#5", "field": "十字头#5", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#5"}, ]}, {"name": "减速箱小轴承", "entries": [ {"label": "#1(输入法兰端)", "field": "减速箱小轴承1", "filters": {"data_type": "LSDAQ"}, "result_key": "减速箱小轴承#1"}, @@ -222,20 +216,18 @@ def _query_load_status_timeline( import pandas as pd import warnings from influxdb_client.client.warnings import MissingPivotFunction - except ImportError as e: - LOGGER.error("InfluxDB客户端导入失败: %s,请安装: pip install influxdb-client pandas", e) + except ImportError: + LOGGER.warning("InfluxDB client not available, skip load_status timeline query") return [] try: client = InfluxDBClient(url=influx_url, org=influx_org, token=influx_token) query_api = client.query_api() - # 确保使用UTC时间格式查询 start_rfc = start_time.strftime('%Y-%m-%dT%H:%M:%SZ') end_rfc = end_time.strftime('%Y-%m-%dT%H:%M:%SZ') - - LOGGER.debug("查询load_status时间范围: %s 到 %s", start_rfc, end_rfc) + # 查询load_status字段的所有数据点(在Breaker数据类型中) flux = f''' from(bucket: "{influx_bucket}") |> range(start: {start_rfc}, stop: {end_rfc}) @@ -261,20 +253,21 @@ from(bucket: "{influx_bucket}") LOGGER.warning("No load_status timeline data found") return [] - # 转换为时间线数据,保持UTC时区 - from datetime import timezone + # 转换为时间线数据,确保时区一致性 timeline = [] for _, row in df.iterrows(): time_obj = pd.to_datetime(row['_time']) - # 确保转换为UTC时区的datetime对象 - if hasattr(time_obj, 'tz_localize'): - if time_obj.tz is None: - time_obj = time_obj.tz_localize(timezone.utc) - else: - time_obj = time_obj.tz_convert(timezone.utc) - - if hasattr(time_obj, 'to_pydatetime'): + # 转换为本地时间,去除时区信息,与start_time/end_time保持一致 + if hasattr(time_obj, 'tz') and time_obj.tz is not None: + # 对于pandas Timestamp,先转换为本地时区再转为Python datetime + time_obj = time_obj.tz_convert(None).to_pydatetime() + elif hasattr(time_obj, 'to_pydatetime'): + # 转换为Python datetime对象 time_obj = time_obj.to_pydatetime() + + # 确保没有时区信息 + if hasattr(time_obj, 'tzinfo') and time_obj.tzinfo is not None: + time_obj = time_obj.replace(tzinfo=None) timeline.append({ 'time': time_obj, @@ -375,10 +368,24 @@ def _calculate_effective_time_points( effective_time_points[slot_str] = None continue - if target_effective_hours > total_effective_hours: - LOGGER.warning("Target effective time %.3fh exceeds total effective time %.3fh for slot %s", - target_effective_hours, total_effective_hours, slot_str) - effective_time_points[slot_str] = None + # 如果目标时间 >= 总有效时间(允许小的浮点误差),使用最后一个有效时间段的结束时间 + # 这样可以处理边界情况:实验正好运行了目标时长,但由于浮点精度可能略小于目标值 + tolerance = 0.01 # 允许 0.01 小时的容差 + if target_effective_hours >= total_effective_hours - tolerance: + if effective_periods: + # 使用最后一个有效时间段的结束时间 + last_period = effective_periods[-1] + target_time_point = last_period['end'] + effective_time_points[slot_str] = target_time_point + LOGGER.info("Slot %s: effective %.3fh >= total %.3fh, using last period end time %s", + slot_str, target_effective_hours, total_effective_hours, + target_time_point.strftime('%H:%M:%S')) + else: + # 如果没有有效时间段,使用实验结束时间 + effective_time_points[slot_str] = end_time + LOGGER.info("Slot %s: effective %.3fh >= total %.3fh, using experiment end time %s", + slot_str, target_effective_hours, total_effective_hours, + end_time.strftime('%H:%M:%S') if end_time else "N/A") continue # 在有效时间段中查找累计运行target_effective_hours小时的时间点 @@ -432,11 +439,8 @@ def _query_influxdb_range_with_load_status( client = InfluxDBClient(url=influx_url, org=influx_org, token=influx_token) query_api = client.query_api() - # 确保使用UTC时间格式 start_rfc = start_time.strftime('%Y-%m-%dT%H:%M:%SZ') end_rfc = end_time.strftime('%Y-%m-%dT%H:%M:%SZ') - - LOGGER.debug("查询字段 %s 时间范围: %s 到 %s", field_name, start_rfc, end_rfc) # 构建过滤条件 tag_filters = "" @@ -528,26 +532,29 @@ def _query_influxdb_with_load_status( client = InfluxDBClient(url=influx_url, org=influx_org, token=influx_token) query_api = client.query_api() - # 确保使用UTC时间 - target_time_rfc = target_time.strftime('%Y-%m-%dT%H:%M:%SZ') - LOGGER.debug( - "查询字段=%s 目标时间=%s (UTC) 过滤器=%s", + "Querying field=%s measurement=%s target_time=%s filters=%s (with load_status=1)", field_name, - target_time_rfc, + influx_measurement, + target_time.strftime('%Y-%m-%dT%H:%M:%SZ'), filters or {}, ) - # 使用时间窗口查找最接近的数据点 - window_minutes = 10 + # 查询逻辑:查询目标时间点之前(包含目标时间点)的数据,获取最接近目标时间点的瞬时值 + # 使用实验开始时间作为查询起点,目标时间点作为查询终点,确保获取该时间点的瞬时数值 + # 需要从实验开始时间查询,因为有效时间点是基于累计运行时间计算的 + + # 获取实验开始时间(需要从环境变量或传入参数获取) + # 为了简化,我们使用一个合理的时间窗口:从目标时间点往前推足够长的时间 + # 但为了精确,我们应该查询到目标时间点为止,取最后一条 + window_minutes = 60 # 往前查询60分钟,确保能覆盖到数据 query_start = target_time - timedelta(minutes=window_minutes) - query_end = target_time + timedelta(minutes=window_minutes) + # 查询终点设置为目标时间点,确保获取的是该时间点或之前的数据 + query_end = target_time query_start_rfc = query_start.strftime('%Y-%m-%dT%H:%M:%SZ') query_end_rfc = query_end.strftime('%Y-%m-%dT%H:%M:%SZ') - - LOGGER.debug("查询窗口: %s 到 %s", query_start_rfc, query_end_rfc) # 构建过滤条件 tag_filters = "" @@ -555,7 +562,7 @@ def _query_influxdb_with_load_status( for key, value in filters.items(): tag_filters += f'\n |> filter(fn: (r) => r["{key}"] == "{value}")' - # 查询温度数据(不需要load_status筛选,因为已经基于有效时间点查询) + # 查询温度数据:查询到目标时间点为止,取最后一条(最接近目标时间点的瞬时值) flux = f''' from(bucket: "{influx_bucket}") |> range(start: {query_start_rfc}, stop: {query_end_rfc}) diff --git a/diagnose_word_com.py b/diagnose_word_com.py new file mode 100644 index 0000000..2c7d0e1 --- /dev/null +++ b/diagnose_word_com.py @@ -0,0 +1,243 @@ +""" +Word COM 诊断和修复工具 +用于诊断和解决Word COM组件实例化问题 +""" +import sys +import os +import subprocess +import winreg +import pythoncom +import win32com.client +from pathlib import Path + +def check_word_installation(): + """检查Word是否已安装""" + print("\n=== 检查Word安装 ===") + try: + # 检查注册表中的Word安装信息 + key_paths = [ + r"SOFTWARE\Microsoft\Office\16.0\Word\InstallRoot", + r"SOFTWARE\Microsoft\Office\15.0\Word\InstallRoot", + r"SOFTWARE\Microsoft\Office\14.0\Word\InstallRoot", + ] + + for key_path in key_paths: + try: + key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path) + path, _ = winreg.QueryValueEx(key, "Path") + winreg.CloseKey(key) + print(f"✓ 找到Word安装: {path}") + return True + except: + continue + + print("✗ 未在注册表中找到Word安装信息") + return False + except Exception as e: + print(f"✗ 检查失败: {e}") + return False + +def check_word_com_registration(): + """检查Word COM组件注册""" + print("\n=== 检查Word COM注册 ===") + try: + key = winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r"Word.Application") + winreg.CloseKey(key) + print("✓ Word.Application COM类已注册") + return True + except: + print("✗ Word.Application COM类未注册") + return False + +def test_word_creation_methods(): + """测试不同的Word实例创建方法""" + print("\n=== 测试Word实例创建方法 ===") + + methods = [ + ("pythoncom.CoInitialize + Dispatch", lambda: test_with_coinit_dispatch()), + ("pythoncom.CoInitializeEx(COINIT_APARTMENTTHREADED)", lambda: test_with_coinit_apartment()), + ("pythoncom.CoInitializeEx(COINIT_MULTITHREADED)", lambda: test_with_coinit_multi()), + ("DispatchEx (新实例)", lambda: test_dispatchex()), + ("EnsureDispatch (缓存)", lambda: test_ensure_dispatch()), + ] + + success_methods = [] + + for method_name, test_func in methods: + try: + print(f"\n测试: {method_name}") + result = test_func() + if result: + print(f" ✓ 成功") + success_methods.append(method_name) + else: + print(f" ✗ 失败") + except Exception as e: + print(f" ✗ 异常: {e}") + + return success_methods + +def test_with_coinit_dispatch(): + """使用CoInitialize + Dispatch""" + try: + pythoncom.CoInitialize() + word = win32com.client.Dispatch("Word.Application") + version = word.Version + word.Quit() + pythoncom.CoUninitialize() + return True + except: + try: + pythoncom.CoUninitialize() + except: + pass + return False + +def test_with_coinit_apartment(): + """使用CoInitializeEx APARTMENTTHREADED""" + try: + pythoncom.CoInitializeEx(pythoncom.COINIT_APARTMENTTHREADED) + word = win32com.client.Dispatch("Word.Application") + version = word.Version + word.Quit() + pythoncom.CoUninitialize() + return True + except: + try: + pythoncom.CoUninitialize() + except: + pass + return False + +def test_with_coinit_multi(): + """使用CoInitializeEx MULTITHREADED""" + try: + pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED) + word = win32com.client.Dispatch("Word.Application") + version = word.Version + word.Quit() + pythoncom.CoUninitialize() + return True + except: + try: + pythoncom.CoUninitialize() + except: + pass + return False + +def test_dispatchex(): + """使用DispatchEx""" + try: + pythoncom.CoInitialize() + word = win32com.client.DispatchEx("Word.Application") + version = word.Version + word.Quit() + pythoncom.CoUninitialize() + return True + except: + try: + pythoncom.CoUninitialize() + except: + pass + return False + +def test_ensure_dispatch(): + """使用EnsureDispatch""" + try: + pythoncom.CoInitialize() + word = win32com.client.gencache.EnsureDispatch("Word.Application") + version = word.Version + word.Quit() + pythoncom.CoUninitialize() + return True + except: + try: + pythoncom.CoUninitialize() + except: + pass + return False + +def check_dcom_permissions(): + """检查DCOM权限配置""" + print("\n=== 检查DCOM权限 ===") + print("提示: 需要管理员权限才能修改DCOM设置") + print("\n手动检查步骤:") + print("1. Win+R 运行 'dcomcnfg'") + print("2. 组件服务 -> 计算机 -> 我的电脑 -> DCOM配置") + print("3. 找到 'Microsoft Word 97-2003 文档' 或 'Microsoft Word Document'") + print("4. 右键 -> 属性 -> 安全") + print("5. 确保当前用户有 '启动和激活' 权限") + +def generate_fix_script(): + """生成修复脚本""" + print("\n=== 生成修复脚本 ===") + + fix_script = """@echo off +echo 修复Word COM权限问题 +echo 需要管理员权限运行此脚本 +echo. + +REM 重新注册Word COM组件 +echo 正在重新注册Word... +for %%i in (WINWORD.EXE) do set WORD_PATH=%%~$PATH:i +if defined WORD_PATH ( + "%WORD_PATH%" /regserver + echo Word COM组件已重新注册 +) else ( + echo 未找到Word可执行文件 +) + +echo. +echo 修复完成,请重新运行程序 +pause +""" + + script_path = Path("fix_word_com.bat") + script_path.write_text(fix_script, encoding='gbk') + print(f"✓ 已生成修复脚本: {script_path.absolute()}") + print(" 请右键以管理员身份运行此脚本") + +def main(): + print("=" * 60) + print("Word COM 诊断工具") + print("=" * 60) + + # 1. 检查Word安装 + word_installed = check_word_installation() + + # 2. 检查COM注册 + com_registered = check_word_com_registration() + + # 3. 测试创建方法 + success_methods = test_word_creation_methods() + + # 4. DCOM权限提示 + check_dcom_permissions() + + # 5. 生成修复脚本 + if not success_methods: + generate_fix_script() + + # 总结 + print("\n" + "=" * 60) + print("诊断总结") + print("=" * 60) + print(f"Word已安装: {'是' if word_installed else '否'}") + print(f"COM已注册: {'是' if com_registered else '否'}") + print(f"成功的创建方法: {len(success_methods)}") + + if success_methods: + print("\n✓ 找到可用的创建方法:") + for method in success_methods: + print(f" - {method}") + print("\n建议: 在代码中使用上述成功的方法") + else: + print("\n✗ 所有创建方法都失败") + print("\n建议的解决步骤:") + print("1. 以管理员身份运行 fix_word_com.bat") + print("2. 检查DCOM权限配置 (运行 dcomcnfg)") + print("3. 确保Word没有被杀毒软件阻止") + print("4. 尝试修复Office安装") + +if __name__ == "__main__": + main() diff --git a/experiments.db1 b/experiments.db1 new file mode 100644 index 0000000000000000000000000000000000000000..9f3e0a2fcf3d02174ef182240ff444717a874b2b GIT binary patch literal 1212416 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 zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr zKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{ z0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2J zBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZrKmsH{0wh2JBtQZr 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

(IZL&M3HANs>jUJY--#b`1z)|dTWZVs^-Km0 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%0AI&ZyDQQ%zhTSW~}un=7sA2{e35)>q5WV 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%0AbAp}9uVZc;vuZEq>N!{9Gttm6f2)`g7w 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 None: - global _PROGRESs_CB - _PROGRESs_CB = cb - - -def _progress(msg: str, cur: int, total: int) -> None: - if _PROGRESs_CB: - try: - _PROGRESs_CB(msg, cur, total) - except Exception: - pass - - -def _build_influx_service(cfg: AppConfig) -> InfluxService: - params = InfluxConnectionParams(url=cfg.influx.url, org=cfg.influx.org, token=cfg.influx.token) - return InfluxService(params) - - -def _execute_db_query(ph: PlaceholderConfig, db_cfg: Optional[DbConnectionConfig]) -> str: - """ - 执行数据库查询并返回单个值(字符串) - - Args: - ph: 数据库文本占位符配置 - db_cfg: 数据库连接配置 - - Returns: - 查询结果的字符串表示,如果查询失败或没有结果则返回空字符串 - - Raises: - Exception: 如果查询执行失败 - """ +def _execute_db_query(ph, db_cfg): query = (ph.dbQuery or "").strip() - if not query: - logger.warning("Empty query for placeholder %s", ph.key) - return "" - - if db_cfg is None: - db_cfg = DbConnectionConfig() - engine = (db_cfg.engine or "").lower() - if not engine: - engine = "mysql" - - logger.debug("Executing %s query for placeholder %s", engine, ph.key) - + if not query: return "" + if not db_cfg: db_cfg = DbConnectionConfig() + engine = (db_cfg.engine or "mysql").lower() + if engine in ("sqlite", "sqlite3"): - db_path = db_cfg.database or None - return _execute_sqlite_query(query, db_path) - if engine == "mysql": - return _execute_mysql_query(query, db_cfg) - if engine in ("sqlserver", "mssql"): - return _execute_sqlserver_query(query, db_cfg) + import sqlite3 + conn = sqlite3.connect(db_cfg.database or str(Path(__file__).parent / "experiments.db")) + result = conn.execute(query).fetchone() + conn.close() + return str(result[0]) if result and result[0] else "" + elif engine == "mysql": + import pymysql + conn = pymysql.connect(host=getattr(db_cfg, "host", "localhost"), port=int(getattr(db_cfg, "port", 3306)), + user=getattr(db_cfg, "username", ""), password=getattr(db_cfg, "password", ""), + database=getattr(db_cfg, "database", ""), charset="utf8mb4") + with conn.cursor() as cursor: + cursor.execute(query) + result = cursor.fetchone() + conn.close() + return str(result[0]) if result and result[0] else "" + return "" - error_msg = f"不支持的数据库类型 '{engine}' (占位符: {ph.key})" - logger.warning(error_msg) - raise Exception(error_msg) - - -def _execute_sqlite_query(query: str, db_path: Optional[str] = None) -> str: +def _load_script_data_from_db(experiment_id): try: import sqlite3 - - if db_path is None: - app_dir = Path(__file__).resolve().parent - db_path = str(app_dir / "experiments.db") - - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - try: - cursor.execute(query) - result = cursor.fetchone() - finally: - cursor.close() - conn.close() - if not result: - return "" - if len(result) == 1: - return "" if result[0] is None else str(result[0]) - for val in result: - if val is not None: - return str(val) - return "" + conn = sqlite3.connect(str(Path(__file__).parent / "experiments.db")) + result = conn.execute("SELECT script_data FROM experiments WHERE id=?", (experiment_id,)).fetchone() + conn.close() + if result and result[0]: + logger.info("从数据库加载脚本数据,实验ID: %d", experiment_id) + return json.loads(result[0]) except Exception as e: - logger.error("SQLite query failed: %s", e) - return "" + logger.error("加载脚本数据失败: %s", e) + return None - -def _execute_mysql_query(query: str, db_cfg: Optional[Any]) -> str: +def _load_experiment_info(experiment_id): + """加载实验信息,判断是否正常(有脚本数据即为正常)""" try: - import pymysql # type: ignore + import sqlite3 + conn = sqlite3.connect(str(Path(__file__).parent / "experiments.db")) + result = conn.execute("SELECT script_data FROM experiments WHERE id=?", (experiment_id,)).fetchone() + conn.close() + if result: + # 如果有脚本数据(不为None且不为空),就认为是正常的 + script_data = result[0] + is_normal = script_data is not None and str(script_data).strip() != "" + return {'is_normal': is_normal} except Exception as e: - logger.error("MySQL driver (pymysql) not available: %s", e) - raise Exception(f"MySQL驱动不可用: {e}") + logger.error("加载实验信息失败: %s", e) + return None - host = getattr(db_cfg, "host", "localhost") or "localhost" - username = getattr(db_cfg, "username", "") or "" - password = getattr(db_cfg, "password", "") or "" - database = getattr(db_cfg, "database", "") or "" - try: - port = int(getattr(db_cfg, "port", 3306) or 3306) - except Exception: - port = 3306 +def _parse_script_tables(script_data): + tables = {} + if isinstance(script_data, dict) and "tables" in script_data: + for item in script_data["tables"]: + key = item.get("token") or item.get("key") + if key: tables[str(key)] = item + return tables - if not database: - error_msg = "MySQL数据库名未配置" - logger.warning(error_msg) - raise Exception(error_msg) - - # 清理查询语句:移除多余的空白字符,但保留必要的空格 - query = " ".join(query.split()) - - conn = None - try: - logger.debug("Connecting to MySQL: %s@%s:%d/%s", username, host, port, database) - conn = pymysql.connect( - host=host, - port=port, - user=username, - password=password, - database=database, - charset="utf8mb4", - cursorclass=pymysql.cursors.Cursor, - ) - with conn.cursor() as cursor: - logger.debug("Executing query: %s", query) - cursor.execute(query) - result = cursor.fetchone() - logger.debug("Query result: %s", result) - if not result: - logger.debug("Query returned no rows") - return "" - if len(result) == 1: - value = result[0] - if value is None: - logger.debug("Query returned NULL") - return "" - logger.debug("Query returned single value: %s", value) - return str(value) - # 多列情况:返回第一个非空值 - for val in result: - if val is not None: - logger.debug("Query returned value from multiple columns: %s", val) - return str(val) - logger.debug("Query returned all NULL values") - return "" - except Exception as e: - error_msg = f"MySQL查询失败: {e}" - logger.error("%s (query: %s)", error_msg, query) - raise Exception(error_msg) - finally: - try: - if conn is not None: - conn.close() - except Exception: - pass - - -def _execute_sqlserver_query(query: str, db_cfg: Optional[Any]) -> str: - try: - import pyodbc # type: ignore - except Exception as e: - logger.error("SQL Server driver (pyodbc) not available: %s", e) - return "" - - host = getattr(db_cfg, "host", "localhost") or "localhost" - username = getattr(db_cfg, "username", "") or "" - password = getattr(db_cfg, "password", "") or "" - database = getattr(db_cfg, "database", "") or "" - try: - port = int(getattr(db_cfg, "port", 1433) or 1433) - except Exception: - port = 1433 - - if not database: - logger.warning("SQL Server database name missing; skip query") - return "" - - driver_candidates = [ - "ODBC Driver 18 for SQL Server", - "ODBC Driver 17 for SQL Server", - "ODBC Driver 13 for SQL Server", - "SQL Server", - ] - connection = None - last_error: Optional[Exception] = None - for driver in driver_candidates: - conn_str = ( - f"DRIVER={{{driver}}};SERVER={host},{port};DATABASE={database};" - f"UID={username};PWD={password};TrustServerCertificate=yes" - ) - try: - connection = pyodbc.connect(conn_str, timeout=5) - break - except Exception as e: - last_error = e - continue - if connection is None: - logger.error("SQL Server connection failed: %s", last_error) - return "" - - try: - cursor = connection.cursor() - try: - cursor.execute(query) - result = cursor.fetchone() - finally: - cursor.close() - if not result: - return "" - if len(result) == 1: - return "" if result[0] is None else str(result[0]) - for val in result: - if val is not None: - return str(val) - return "" - except Exception as e: - logger.error("SQL Server query failed: %s", e) - return "" - finally: - try: - connection.close() - except Exception: - pass - - -def _query_df(influx: InfluxService, ph: PlaceholderConfig) -> pd.DataFrame: - if not ph.influx: - logger.warning("No influx config for %s", ph.key) - return pd.DataFrame() - if not ph.influx.bucket or not ph.influx.measurement: - logger.warning("Skip query for %s due to missing bucket/measurement", ph.key) - return pd.DataFrame() - try: - return influx.query( - bucket=ph.influx.bucket, - measurement=ph.influx.measurement, - fields=ph.influx.fields, - filters=ph.influx.filters, - time_range=ph.influx.timeRange, - aggregate=ph.influx.aggregate, - window_period=getattr(ph.influx, 'windowPeriod', '') or '' - ) - except Exception as e: - logger.error("Query failed for %s: %s", ph.key, e) - return pd.DataFrame() - - -def _replace_texts_word(doc, constants, mapping: Dict[str, str]) -> None: - story_types = [ - constants.wdMainTextStory, - constants.wdPrimaryHeaderStory, - constants.wdEvenPagesHeaderStory, - constants.wdFirstPageHeaderStory, - constants.wdPrimaryFooterStory, - constants.wdEvenPagesFooterStory, - constants.wdFirstPageFooterStory, - constants.wdTextFrameStory, - ] - def _all_story_ranges(): - for sid in story_types: - try: - rng = doc.StoryRanges(sid) - except Exception: - rng = None - while rng is not None: - yield rng - try: - rng = rng.NextStoryRange - except Exception: - rng = None - for key, val in mapping.items(): - token = '{' + key + '}' - # Single pass over story ranges (covers headers/footers body/textframes stories) - for rng in _all_story_ranges(): - try: - find = rng.Find - find.ClearFormatting(); find.Replacement.ClearFormatting() - find.Text = token - find.Replacement.Text = val or '' - find.Forward = True - find.Wrap = constants.wdFindContinue - find.Format = False - find.MatchCase = False - find.MatchWholeWord = False - find.MatchByte = False - find.MatchWildcards = False - find.MatchSoundsLike = False - find.MatchAllWordForms = False - find.Execute(Replace=constants.wdReplaceAll) - except Exception: - continue - # Additionally replace text inside header/footer shapes' TextFrame (not covered by main stories on some docs) - try: - for sec in doc.Sections: - for hf_type in (constants.wdHeaderFooterPrimary, constants.wdHeaderFooterFirstPage, constants.wdHeaderFooterEvenPages): - for container in (sec.Headers(hf_type), sec.Footers(hf_type)): - try: - for sh in container.Shapes: - try: - if getattr(sh, 'TextFrame', None) and sh.TextFrame.HasText: - tr = sh.TextFrame.TextRange - f2 = tr.Find; f2.ClearFormatting(); f2.Replacement.ClearFormatting() - f2.Text = token; f2.Replacement.Text = val or '' - f2.Wrap = constants.wdFindStop; f2.Forward = True; f2.Format = False - f2.MatchWildcards = False; f2.MatchCase = False; f2.MatchWholeWord = False - f2.Execute(Replace=constants.wdReplaceAll) - except Exception: - continue - except Exception: - continue - except Exception: - pass - - -def _format_numeric_columns(df: pd.DataFrame, exclude_cols: List[str]) -> pd.DataFrame: - if df is None or df.empty: - return df - result = df.copy() - exclude = set(exclude_cols or []) - for col in result.columns: - if col in exclude: - continue - # try to round numeric values to 2 decimals - series = result[col] - try: - numeric = pd.to_numeric(series, errors="coerce") - if numeric.notna().any(): - rounded = numeric.round(2) - # keep original non-numeric entries untouched - result[col] = series.where(numeric.isna(), rounded) - except Exception: - pass +def _replace_global_params(text, cfg): + """替换文本中的 @参数名 为全局参数的值""" + if not text or '@' not in text: return text + result = text + if hasattr(cfg, 'globalParameters') and hasattr(cfg.globalParameters, 'parameters'): + import re + for param_name in re.findall(r'@(\w+)', text): + if param_name in cfg.globalParameters.parameters: + result = result.replace(f'@{param_name}', cfg.globalParameters.parameters[param_name]) return result - -def _insert_table_at_range_word(doc, rng, df: pd.DataFrame, constants, title: str = "") -> None: - if title: - rng.InsertParagraphBefore() - rng.Paragraphs(1).Range.Text = title - rng.Collapse(constants.wdCollapseEnd) - rows = len(df) + 1 if not df.empty else 1 - cols = len(df.columns) if not df.empty else 1 - tbl = doc.Tables.Add(Range=rng, NumRows=rows, NumColumns=cols) - # header - if not df.empty and cols > 0: - for ci, col in enumerate(df.columns, start=1): - tbl.Cell(1, ci).Range.Text = str(col) - else: - tbl.Cell(1, 1).Range.Text = "无数据" - if not df.empty: - for ri in range(len(df)): - for ci, col in enumerate(df.columns, start=1): - val = df.iloc[ri][col] - try: - v = f"{float(val):.2f}" - except Exception: - v = str(val) - tbl.Cell(ri + 2, ci).Range.Text = v - - -def _insert_picture_at_range_word(rng, image_path: Path, title: str = "") -> None: - if title: - rng.InsertParagraphBefore() - rng.Paragraphs(1).Range.Text = title - rng.Collapse(0) - rng.InlineShapes.AddPicture(FileName=str(image_path), LinkToFile=False, SaveWithDocument=True) - - -def _delete_token_range_word(rng) -> None: - try: - rng.Text = "" - except Exception: - pass - - -def _find_token_ranges_word(doc, constants, token: str): - """主要的token查找方法,使用Word的Find功能""" - report_debug_logger.info("=== 开始查找token: %s ===", token) - results = [] - - try: - # 简化的查找方法,避免复杂的循环 - report_debug_logger.info("使用简化查找方法...") - - # 创建查找范围 - rng = doc.Content.Duplicate - find = rng.Find - - # 配置查找 - find.ClearFormatting() - find.Text = token - find.Forward = True - find.Wrap = constants.wdFindStop - - # 执行单次查找 - if find.Execute(): - report_debug_logger.info("找到token,位置: %d-%d", rng.Start, rng.End) - results.append(rng.Duplicate) - else: - report_debug_logger.info("未找到token") - - return results - - except Exception as e: - report_debug_logger.error("查找token时发生异常: %s", e) - # 直接抛出异常,让调用方处理 - raise - - -def _find_token_ranges_word_fallback(doc, constants, token: str): - """备用的token查找方法,适用于Office兼容性问题""" - report_debug_logger.info("=== 使用备用token查找方法 ===") - results = [] - - try: - # 简化方法: 直接遍历所有表格单元格 - report_debug_logger.info("遍历表格单元格查找token...") - - table_count = doc.Tables.Count - report_debug_logger.info("文档中有 %d 个表格", table_count) - - for table_idx in range(1, table_count + 1): - try: - table = doc.Tables(table_idx) - row_count = table.Rows.Count - report_debug_logger.info("表格 %d 有 %d 行", table_idx, row_count) - - for row_idx in range(1, row_count + 1): - try: - row = table.Rows(row_idx) - cell_count = row.Cells.Count - - for col_idx in range(1, cell_count + 1): - try: - cell = table.Cell(row_idx, col_idx) - cell_text = cell.Range.Text - - if token in cell_text: - report_debug_logger.info("在表格 %d 单元格 (%d,%d) 中找到token", - table_idx, row_idx, col_idx) - # 返回整个单元格范围 - results.append(cell.Range) - - except Exception as e: - # 单元格访问失败,跳过 - continue - - except Exception as e: - # 行访问失败,跳过 - continue - - except Exception as e: - report_debug_logger.warning("检查表格 %d 失败: %s", table_idx, e) - continue - - report_debug_logger.info("备用查找完成,总共找到 %d 个结果", len(results)) - return results - - except Exception as e: - report_debug_logger.error("备用查找方法失败: %s", e) - return [] - - -def _make_seconds_index(df: pd.DataFrame) -> pd.Series: +def _make_seconds_index(df): if "_time" in df.columns: - t = pd.to_datetime(df["_time"]) # type: ignore - s = (t - t.iloc[0]).dt.total_seconds().round().astype(int) - return s - # fallback + t = pd.to_datetime(df["_time"]) + return (t - t.iloc[0]).dt.total_seconds().round().astype(int) return pd.Series(range(len(df))) +def _format_numeric_columns(df, exclude_cols): + if df is None or df.empty: return df + result = df.copy() + for col in result.columns: + if col not in exclude_cols: + try: + numeric = pd.to_numeric(result[col], errors="coerce") + if numeric.notna().any(): result[col] = numeric.round(2) + except: pass + return result -def _setup_matplotlib_cn_font() -> None: - try: - import matplotlib - # Prefer common CJK-capable fonts on Windows/macOS/Linux - preferred = ['Microsoft YaHei', 'SimHei', 'Arial Unicode MS', 'Noto Sans CJK SC', 'WenQuanYi Micro Hei'] - # Prepend preferred to current sans-serif list to increase hit rate - current = list(matplotlib.rcParams.get('font.sans-serif', [])) - matplotlib.rcParams['font.sans-serif'] = preferred + [f for f in current if f not in preferred] - matplotlib.rcParams['axes.unicode_minus'] = False - except Exception: - pass - - -def _to_wide_table(df: pd.DataFrame, fields: List[str], first_column: str, titles_map: Dict[str, str], first_title: str | None = None) -> pd.DataFrame: - if df.empty: - return pd.DataFrame() +def _to_wide_table(df, fields, first_column, titles_map, first_title=None): + if df.empty: return pd.DataFrame() work = df.copy() - if "_time" not in work.columns or "_value" not in work.columns: - return work - # limit to selected fields if provided - if fields: - if "_field" in work.columns: - work = work[work["_field"].isin(fields)] - # select first column + if "_time" not in work.columns or "_value" not in work.columns: return work + if fields and "_field" in work.columns: work = work[work["_field"].isin(fields)] + if first_column == "seconds": idx = _make_seconds_index(work) work = work.assign(__index__=idx) - index_col = "__index__" - index_title = first_title or "秒" + index_col, index_title = "__index__", first_title or "秒" else: - index_col = "_time" - index_title = first_title or "时间" - # pivot to wide + index_col, index_title = "_time", first_title or "时间" + if "_field" in work.columns: wide = work.pivot_table(index=index_col, columns="_field", values="_value", aggfunc="last") else: - # no _field column; just one value series wide = work.set_index(index_col)[["_value"]] wide.columns = ["value"] + wide = wide.sort_index() wide.reset_index(inplace=True) - # rename index column wide.rename(columns={index_col: index_title}, inplace=True) - # apply titles map for f, title in titles_map.items(): - if f in wide.columns: - wide.rename(columns={f: title}, inplace=True) - # round numeric columns to 2 decimals except time column - wide = _format_numeric_columns(wide, exclude_cols=[index_title]) - return wide + if f in wide.columns: wide.rename(columns={f: title}, inplace=True) + return _format_numeric_columns(wide, exclude_cols=[index_title]) -def _clear_paragraph_text(paragraph) -> None: - for run in paragraph.runs: - run.text = "" - if paragraph.text: - paragraph.text = "" +# ============================================================ +# 核心:跨 run 占位符替换(处理 Word 将 {token} 拆分到多个 run 的情况) +# ============================================================ + +def _replace_token_across_runs(paragraph, token, replacement): + """在段落中替换占位符,处理 token 被拆分到多个 run 的情况。 + + 例如 Word 可能将 {text4} 拆分成: Run('{')+Run('text4')+Run('}') + 或将 {isNormal} 拆分成: Run('{isNormal')+Run('}') + """ + runs = paragraph.runs + if not runs: + return False + + # 快速路径:token 在单个 run 中 + for run in runs: + if token in run.text: + run.text = run.text.replace(token, replacement) + return True + + # 慢速路径:token 跨越多个 run + texts = [r.text for r in runs] + full = ''.join(texts) + idx = full.find(token) + if idx < 0: + return False + + token_end = idx + len(token) + + # 计算每个 run 的字符区间 [start, end) + boundaries = [] + pos = 0 + for t in texts: + boundaries.append((pos, pos + len(t))) + pos += len(t) + + # 找到 token 覆盖的第一个和最后一个 run + first_ri = last_ri = -1 + for i, (s, e) in enumerate(boundaries): + if s <= idx < e and first_ri < 0: + first_ri = i + if s < token_end <= e: + last_ri = i + break + + if first_ri < 0 or last_ri < 0: + return False + + # 保留 token 前后的文本 + before = texts[first_ri][:idx - boundaries[first_ri][0]] + after = texts[last_ri][token_end - boundaries[last_ri][0]:] + + # 将替换内容写入第一个受影响的 run + runs[first_ri].text = before + replacement + after + + # 清空中间和最后受影响的 run + for i in range(first_ri + 1, last_ri + 1): + runs[i].text = '' + + return True -def _find_enclosing_table_and_pos(doc: Document, paragraph) -> Tuple[Optional[DocxTable], int, int]: - for tbl in doc.tables: - for ri, row in enumerate(tbl.rows): - for ci, cell in enumerate(row.cells): - # Paragraph objects are recreated frequently, compare by text and index - for para in cell.paragraphs: - if para is paragraph: - return tbl, ri, ci - return None, -1, -1 +def _replace_texts_docx(doc, mapping): + """替换文档中所有的 {key} 占位符,包括段落和表格单元格""" + for key, val in mapping.items(): + token = '{' + key + '}' + replacement = val or '' + + # 替换正文段落 + for para in doc.paragraphs: + if token in para.text: + _replace_token_across_runs(para, token, replacement) + + # 替换表格单元格(跳过合并单元格的重复项) + for table in doc.tables: + for row in table.rows: + seen_tc = set() + for cell in row.cells: + tc_id = id(cell._tc) + if tc_id in seen_tc: + continue + seen_tc.add(tc_id) + for para in cell.paragraphs: + if token in para.text: + _replace_token_across_runs(para, token, replacement) -def _fill_manual_table_at_token_word(doc, constants, token: str, grid: List[List[str]]) -> None: - for rng in _find_token_ranges_word(doc, constants, token): - try: - if rng.Information(constants.wdWithInTable): - tbl = rng.Tables(1) - start_row = rng.Information(constants.wdStartOfRangeRowNumber) - start_col = rng.Information(constants.wdStartOfRangeColumnNumber) - # ensure enough rows - need_rows = start_row + len(grid) - 1 - while tbl.Rows.Count < need_rows: - tbl.Rows.Add() - for i, row_vals in enumerate(grid): - for j, val in enumerate(row_vals): - try: - cell = tbl.Cell(start_row + i, start_col + j) - if val is None or (isinstance(val, str) and val.strip() == ""): - continue - # don't overwrite existing non-empty cell text - try: - existing = (cell.Range.Text or "").replace("\r", "").replace("\x07", "").strip() - except Exception: - existing = "" - if existing: - continue - cell.Range.Text = str(val) - except Exception: - continue - _delete_token_range_word(rng) - except Exception: - continue +# ============================================================ +# 核心:表格数据填充(正确处理合并单元格的坐标映射) +# ============================================================ - -def _rows_to_cells(headers: List[Any], rows: List[List[Any]]) -> List[Dict[str, Any]]: - cells: List[Dict[str, Any]] = [] - cursor = 0 - if headers: - for ci, value in enumerate(headers): - cells.append({"row": cursor, "col": ci, "value": value}) - cursor += 1 - for ri, row in enumerate(rows): - if not isinstance(row, list): - row = [row] - for ci, value in enumerate(row): - cells.append({"row": cursor + ri, "col": ci, "value": value}) +def _get_unique_cells(row): + """获取行中的唯一单元格列表(合并单元格只返回一次)""" + seen = set() + cells = [] + for cell in row.cells: + tc_id = id(cell._tc) + if tc_id not in seen: + seen.add(tc_id) + cells.append(cell) return cells -def _parse_script_tables(script_data) -> Dict[str, Dict]: - tables: Dict[str, Dict] = {} - if script_data is None: - return tables - if isinstance(script_data, dict): - if ("token" in script_data) and ( - isinstance(script_data.get("cells"), list) or isinstance(script_data.get("values"), list) - ): - key = script_data.get("token") or script_data.get("placeholder") or script_data.get("key") - if key: - tables[str(key)] = script_data - return tables - if "tables" in script_data and isinstance(script_data["tables"], list): - for item in script_data["tables"]: - if not isinstance(item, dict): - continue - key = item.get("token") or item.get("placeholder") or item.get("key") - if key: - tables[str(key)] = item - else: - # treat dict as mapping token -> spec - has_cells = all( - isinstance(v, dict) and ( - "cells" in v or "grid" in v or "values" in v - ) - for v in script_data.values() - ) - if has_cells: - for key, item in script_data.items(): - if isinstance(item, dict): - tables[str(key)] = item - elif "rows" in script_data and isinstance(script_data.get("rows"), list): - token = script_data.get("token") or script_data.get("placeholder") or script_data.get("key") or "experimentProcess" - headers = script_data.get("headers") if isinstance(script_data.get("headers"), list) else [] - cells = _rows_to_cells(headers, script_data.get("rows") or []) - tables[str(token)] = { - "token": token, - "startRow": int(script_data.get("startRow", 0) or 0), - "startCol": int(script_data.get("startCol", 0) or 0), - "cells": cells, - } - elif isinstance(script_data, list): - for item in script_data: - if not isinstance(item, dict): - continue - key = item.get("token") or item.get("placeholder") or item.get("key") - if key: - tables[str(key)] = item - return tables - - -def _parse_script_charts(script_data) -> Dict[str, Dict]: - charts: Dict[str, Dict] = {} - if script_data is None: - return charts - if isinstance(script_data, dict): - if "charts" in script_data and isinstance(script_data["charts"], list): - for item in script_data["charts"]: - if not isinstance(item, dict): - continue - key = item.get("token") or item.get("placeholder") or item.get("key") - if key: - charts[str(key)] = item - elif "series" in script_data and isinstance(script_data.get("series"), list): - key = script_data.get("token") or script_data.get("placeholder") or script_data.get("key") - if key: - charts[str(key)] = script_data - else: - candidate: Dict[str, Dict] = {} - for key, val in script_data.items(): - if isinstance(val, dict) and isinstance(val.get("series"), list): - candidate[str(key)] = {"token": key, **val} - charts.update(candidate) - elif isinstance(script_data, list): - for item in script_data: - if not isinstance(item, dict): - continue - key = item.get("token") or item.get("placeholder") or item.get("key") - if key and isinstance(item.get("series"), list): - charts[str(key)] = item - return charts - - -def _fill_script_table_at_token_word(doc, constants, token: str, table_spec: Dict) -> None: - report_debug_logger.info("=== 开始填充脚本表格: %s ===", token) +def _fill_script_table_docx(doc, token, table_spec): + """填充脚本表格数据到 Word 文档中。 - cells = table_spec.get("cells") or table_spec.get("values") or [] - if not isinstance(cells, list): - report_debug_logger.error("表格 %s 没有有效的单元格列表", token) - logger.warning("Script table %s has no cells list", token) - return - - report_debug_logger.info("单元格总数: %d", len(cells)) - start_row_offset = int(table_spec.get("startRow", 0) or 0) - start_col_offset = int(table_spec.get("startCol", 0) or 0) - report_debug_logger.info("起始偏移: 行=%d, 列=%d", start_row_offset, start_col_offset) - - report_debug_logger.info("查找文档中的token: %s", token) - - # 直接尝试查找,如果失败则使用备用方法 - token_ranges = [] - try: - report_debug_logger.info("尝试主要查找方法...") - token_ranges = list(_find_token_ranges_word(doc, constants, token)) - report_debug_logger.info("主要方法成功,找到 %d 个token范围", len(token_ranges)) - except Exception as e: - report_debug_logger.error("主要查找方法失败: %s", e) - report_debug_logger.info("尝试备用查找方法...") - try: - token_ranges = _find_token_ranges_word_fallback(doc, constants, token) - report_debug_logger.info("备用方法完成,找到 %d 个token范围", len(token_ranges)) - except Exception as e2: - report_debug_logger.error("备用查找方法也失败: %s", e2) - - if not token_ranges: - report_debug_logger.error("未在文档中找到token: %s", token) - report_debug_logger.info("可能的原因:") - report_debug_logger.info("1. 模板中没有 %s 标记", token) - report_debug_logger.info("2. WPS与Office的兼容性问题") - report_debug_logger.info("3. 文档格式问题") - - # 尝试最后的解决方案:直接在第一个表格中填充数据 - report_debug_logger.info("=== 尝试最后的解决方案:直接填充第一个表格 ===") - try: - if doc.Tables.Count > 0: - report_debug_logger.info("文档中有 %d 个表格,尝试填充第一个", doc.Tables.Count) - table = doc.Tables(1) - _fill_table_directly(table, cells, start_row_offset, start_col_offset) - report_debug_logger.info("直接填充表格成功") - return - else: - report_debug_logger.error("文档中没有表格可以填充") - except Exception as e: - report_debug_logger.error("直接填充表格也失败: %s", e) - + 坐标系统说明: + - 脚本数据中的 row 是表格的绝对行号 + - 脚本数据中的 col 是相对于 token 所在"唯一单元格"位置的偏移 + - 合并单元格被视为一个单元格,因此 col=1 跳过合并区域到达下一个唯一单元格 + """ + cells_data = table_spec.get("cells") or [] + if not cells_data: return - for rng_idx, rng in enumerate(token_ranges): - report_debug_logger.info("--- 处理token范围 %d ---", rng_idx + 1) - try: - report_debug_logger.info("检查token是否在表格中...") - if not rng.Information(constants.wdWithInTable): - report_debug_logger.warning("Token %s 不在表格中,跳过", token) - logger.warning("Placeholder %s not in table; skip script table fill", token) - continue - - report_debug_logger.info("获取表格对象...") - tbl = rng.Tables(1) - - current_rows = tbl.Rows.Count - current_cols = tbl.Columns.Count if hasattr(tbl, 'Columns') else 0 - report_debug_logger.info("当前表格尺寸: %d 行 x %d 列", current_rows, current_cols) - - start_row = rng.Information(constants.wdStartOfRangeRowNumber) + start_row_offset - start_col = rng.Information(constants.wdStartOfRangeColumnNumber) + start_col_offset - report_debug_logger.info("计算起始位置: 行=%d, 列=%d", start_row, start_col) - - # remove placeholder token text before filling - report_debug_logger.info("清理token文本...") - try: - anchor_cell = tbl.Cell(rng.Information(constants.wdStartOfRangeRowNumber), rng.Information(constants.wdStartOfRangeColumnNumber)) - _clear_token_in_cell(anchor_cell, token) - except Exception as e: - report_debug_logger.warning("清理anchor_cell失败: %s", e) - try: - _delete_token_range_word(rng) - except Exception as e: - report_debug_logger.warning("删除token范围失败: %s", e) - - # Determine required rows - report_debug_logger.info("计算所需行数...") - max_row_needed = start_row - for cell_info in cells: - if not isinstance(cell_info, dict): - continue - row_off = int(cell_info.get("row", 0) or 0) - row_span = int(cell_info.get("rowspan", cell_info.get("rowSpan", 1)) or 1) - if row_span < 1: - row_span = 1 - row_end = start_row + row_off + row_span - 1 - if row_end > max_row_needed: - max_row_needed = row_end - - report_debug_logger.info("需要最大行数: %d, 当前行数: %d", max_row_needed, tbl.Rows.Count) - - rows_to_add = max_row_needed - tbl.Rows.Count - if rows_to_add > 0: - report_debug_logger.info("需要添加 %d 行", rows_to_add) - for i in range(rows_to_add): - tbl.Rows.Add() - if (i + 1) % 10 == 0: - report_debug_logger.info("已添加 %d/%d 行", i + 1, rows_to_add) - report_debug_logger.info("行添加完成,当前行数: %d", tbl.Rows.Count) - - executed_merges: set[tuple[int, int, int, int]] = set() - processed_cells = 0 - skipped_empty_cells = 0 - - report_debug_logger.info("=== 开始处理单元格数据 ===") - report_debug_logger.info("总单元格数: %d", len(cells)) - - for cell_idx, cell_info in enumerate(cells): - # 进度报告 - if cell_idx % 20 == 0: - report_debug_logger.info("处理进度: %d/%d (%.1f%%)", cell_idx, len(cells), cell_idx/len(cells)*100) - - if not isinstance(cell_info, dict): - continue - - # 提前检查并跳过空单元格以提高性能 - value = cell_info.get("value", "") - if value is None: - skipped_empty_cells += 1 - continue - text = str(value) - if text.strip() == "" and not cell_info.get("keepBlank", False): - skipped_empty_cells += 1 - continue - - row_off = int(cell_info.get("row", 0) or 0) - col_off = int(cell_info.get("col", cell_info.get("column", 0)) or 0) - row_span = int(cell_info.get("rowspan", cell_info.get("rowSpan", 1)) or 1) - col_span = int(cell_info.get("colspan", cell_info.get("colSpan", 1)) or 1) - if row_span < 1: - row_span = 1 - if col_span < 1: - col_span = 1 - abs_row = start_row + row_off - abs_col = start_col + col_off - try: - if col_span > 1: - # ensure there are enough columns - total_cols = tbl.Rows(1).Cells.Count - if abs_col + col_span - 1 > total_cols: - logger.warning("Script table %s col span exceeds template columns", token) - continue - if row_span > 1: - total_rows = tbl.Rows.Count - if abs_row + row_span - 1 > total_rows: - logger.warning("Script table %s row span exceeds table rows", token) - continue - except Exception: - pass - - merge_key = (abs_row, abs_col, row_span, col_span) - - # 详细记录第一个和每10个单元格的处理 - if cell_idx == 0 or cell_idx % 10 == 0: - report_debug_logger.info("处理单元格 %d: 行=%d, 列=%d, 值=%s", cell_idx, abs_row, abs_col, str(value)[:20]) - - try: - cell_obj = tbl.Cell(abs_row, abs_col) - except Exception as e: - report_debug_logger.warning("获取单元格失败 (%d,%d): %s", abs_row, abs_col, e) - logger.warning("Script table %s: cell (%d,%d) not available", token, abs_row, abs_col) - continue - - if (row_span > 1 or col_span > 1) and merge_key not in executed_merges: - try: - target = tbl.Cell(abs_row + row_span - 1, abs_col + col_span - 1) - cell_obj.Merge(target) - except Exception as mergerr: - logger.warning("Script table %s merge failed at (%d,%d): %s", token, abs_row, abs_col, mergerr) - executed_merges.add(merge_key) - try: - cell_obj = tbl.Cell(abs_row, abs_col) - except Exception: - pass - - value = cell_info.get("value", "") - if value is None: - continue - text = str(value) - if text.strip() == "" and not cell_info.get("keepBlank", False): - continue - - # 关键的文本写入操作 - 这里最可能卡住 - try: - if cell_idx == 0 or cell_idx % 10 == 0: - report_debug_logger.info("写入单元格 %d: (%d,%d) = '%s'", cell_idx, abs_row, abs_col, text[:30]) - - cell_obj.Range.Text = text - processed_cells += 1 - - if cell_idx == 0 or cell_idx % 10 == 0: - report_debug_logger.info("单元格 %d 写入成功", cell_idx) - - except Exception as e: - report_debug_logger.error("写入单元格失败 (%d,%d): %s", abs_row, abs_col, e) - logger.warning("Failed to write text to cell (%d,%d) for token %s", abs_row, abs_col, token) - continue - - report_debug_logger.info("=== 单元格处理完成 ===") - report_debug_logger.info("成功处理: %d 个单元格", processed_cells) - report_debug_logger.info("跳过空单元格: %d 个", skipped_empty_cells) - logger.info("Script table %s: processed %d cells, skipped %d empty cells", - token, processed_cells, skipped_empty_cells) - - except Exception as exc: - report_debug_logger.error("填充脚本表格时发生异常: %s", exc) - logger.error("Failed to fill script table %s: %s", token, exc) - continue + token_with_braces = '{' + token + '}' + table_found = None + token_row = 0 + token_unique_col = 0 - report_debug_logger.info("=== 脚本表格填充完成: %s ===", token) - - -def _fill_table_directly(table, cells, start_row_offset, start_col_offset): - """直接填充表格,不依赖token查找""" - report_debug_logger.info("=== 开始直接填充表格 ===") - - try: - # 计算需要的行数 - max_row_needed = 1 - for cell_info in cells: - if isinstance(cell_info, dict): - row_off = int(cell_info.get("row", 0) or 0) - row_needed = 1 + start_row_offset + row_off - if row_needed > max_row_needed: - max_row_needed = row_needed - - # 确保表格有足够的行 - current_rows = table.Rows.Count - report_debug_logger.info("当前表格行数: %d, 需要行数: %d", current_rows, max_row_needed) - - while table.Rows.Count < max_row_needed: - table.Rows.Add() - - report_debug_logger.info("表格行数调整完成,当前行数: %d", table.Rows.Count) - - # 填充单元格 - processed_cells = 0 - for cell_idx, cell_info in enumerate(cells): - if not isinstance(cell_info, dict): - continue - - value = cell_info.get("value", "") - if value is None: - continue - - text = str(value) - if text.strip() == "": - continue - - row_off = int(cell_info.get("row", 0) or 0) - col_off = int(cell_info.get("col", 0) or 0) - abs_row = 1 + start_row_offset + row_off # Word表格从1开始 - abs_col = 1 + start_col_offset + col_off # Word表格从1开始 - - try: - if cell_idx % 20 == 0: - report_debug_logger.info("直接填充进度: %d/%d", cell_idx, len(cells)) - - cell_obj = table.Cell(abs_row, abs_col) - cell_obj.Range.Text = text - processed_cells += 1 - - except Exception as e: - report_debug_logger.warning("直接填充单元格 (%d,%d) 失败: %s", abs_row, abs_col, e) - continue - - report_debug_logger.info("直接填充完成,成功处理 %d 个单元格", processed_cells) - - except Exception as e: - report_debug_logger.error("直接填充表格失败: %s", e) - raise - - -def _clear_token_in_cell(cell, token: str) -> None: - try: - for para in cell.paragraphs: - if token in para.text: - para.text = para.text.replace(token, "") - except Exception: - try: - cell.text = cell.text.replace(token, "") - except Exception: - pass - - -def _apply_run_font(src_run: Optional[Run], dst_run: Run) -> None: - try: - if src_run is None: - return - # Prefer cloning rPr to keep eastAsia font and weight - try: - src_rPr = src_run._r.rPr - if src_rPr is not None: - # Remove existing rPr - try: - dst_rPr = dst_run._r.rPr - if dst_rPr is not None: - dst_run._r.remove(dst_rPr) - except Exception: - pass - dst_run._r.append(deepcopy(src_rPr)) - return - except Exception: - pass - # Fallback: copy common font props - sf = src_run.font - df = dst_run.font - df.name = sf.name - df.size = sf.size - df.bold = sf.bold if sf.bold is not None else df.bold - df.italic = sf.italic - df.underline = sf.underline - try: - df.color.rgb = getattr(sf.color, 'rgb', None) - except Exception: - pass - except Exception: - pass - - -def _write_cell_text_preserve_style(cell, text: str, ref_run: Optional[Run]) -> Optional[Run]: - # Prefer cell's own first run as style ref; fallback to provided ref_run - try: - own_ref = None - if cell.paragraphs and cell.paragraphs[0].runs: - own_ref = cell.paragraphs[0].runs[0] - except Exception: - own_ref = None - style_ref = own_ref or ref_run - # clear content and write with copied font - try: - cell.text = "" - except Exception: - pass - try: - if not cell.paragraphs: - p = cell.add_paragraph() - else: - p = cell.paragraphs[0] - run = p.add_run() - _apply_run_font(style_ref, run) - run.text = str(text) - return run - except Exception: - # fallback - cell.text = str(text) - return None - - -def _force_cn_font(run: Optional[Run], name: str = "楷体", bold: bool = True) -> None: - if run is None: - return - try: - f = run.font - if bold is not None: - f.bold = bold - if name: - f.name = name - # set eastAsia font in rFonts - r = run._element - rPr = r.rPr - if rPr is None: - rPr = OxmlElement('w:rPr') - r.append(rPr) - rFonts = rPr.rFonts - if rFonts is None: - rFonts = OxmlElement('w:rFonts') - rPr.append(rFonts) - rFonts.set(qn('w:eastAsia'), name) - except Exception: - pass - - -def _center_cell(cell) -> None: - try: - if not cell.paragraphs: - cell.add_paragraph() - for p in cell.paragraphs: - p.alignment = WD_ALIGN_PARAGRAPH.CENTER - except Exception: - pass - - -def _fill_table_from_grid_using_token(doc: Document, token: str, grid: List[List[str]]) -> None: - table, r0, c0 = _find_table_cell_by_token(doc, token) - if table is None: - logger.warning("Token %s not found in any table cell; skip manual table fill", token) - return - # Capture anchor cell style BEFORE clearing - try: - pre_anchor_ref_run = None - ac_pre = table.cell(r0, c0) - if ac_pre.paragraphs and ac_pre.paragraphs[0].runs: - runs = list(ac_pre.paragraphs[0].runs) - # Prefer a run whose text is not a placeholder token like {tb1} - chosen = None - for run in runs: - t = (run.text or '').strip() - if not t: - continue - if t.startswith('{') and t.endswith('}'): - # token run; skip as style source - continue - chosen = run - break - pre_anchor_ref_run = chosen or runs[0] - except Exception: - pre_anchor_ref_run = None - - _clear_token_in_cell(table.cell(r0, c0), token) - - # Start writing at the anchor cell position (r0, c0) - start_row = r0 - start_col = c0 - - # Ensure there are enough rows; we do not change columns to respect the template - need_rows = start_row + len(grid) - while len(table.rows) < need_rows: - table.add_row() - - total_cols = len(table.rows[start_row].cells) - max_writable_cols = max(0, total_cols - start_col) - - # Reference run style from the anchor cell if available - anchor_ref_run = pre_anchor_ref_run - - # Table level reference run (prefer a bold/eastAsia run if available) - table_ref_run = None - try: - for tr in table.rows: - for tc in tr.cells: - for para in tc.paragraphs: - for run in para.runs: - table_ref_run = run - # Prefer runs with explicit rPr eastAsia or bold - try: - rp = run._r.rPr - if rp is not None and (getattr(run.font, 'bold', None) or rp.xpath('.//w:eastAsia', namespaces={'w':'http://schemas.openxmlformats.org/wordprocessingml/2006/main'})): - raise StopIteration - except Exception: - pass - if table_ref_run: - break - if table_ref_run: + # 在表格中查找 token + for table in doc.tables: + for ri, row in enumerate(table.rows): + unique = _get_unique_cells(row) + for uci, cell in enumerate(unique): + if token_with_braces in cell.text: + table_found = table + token_row = ri + token_unique_col = uci break - if table_ref_run: + if table_found: break - except StopIteration: - pass - - # If anchor cell still has residual text (e.g., labels or spaces before token), restyle it with reference style - try: - ac_after = table.cell(r0, c0) - residual = ac_after.text or "" - if residual != "": - txt = residual # keep whitespace exactly as user typed - # rebuild with style - for p in list(ac_after.paragraphs): - try: - for r in list(p.runs): - r.text = "" - except Exception: - pass - run = _write_cell_text_preserve_style(ac_after, txt, anchor_ref_run or table_ref_run) - # Force KaiTi bold for anchor label cell as requested - _force_cn_font(run, name="楷体", bold=True) - # center align the anchor cell paragraphs - _center_cell(ac_after) - except Exception: - pass - - # Build a column-wise de-duplication mask: only keep the first occurrence - rows_n = len(grid) - cols_n = max(len(r) for r in grid) if rows_n else 0 - write_mask: List[List[bool]] = [[False] * cols_n for _ in range(rows_n)] - for j in range(cols_n): - last_val: Optional[str] = None - for i in range(rows_n): - v = grid[i][j] if j < len(grid[i]) else None - if v is None: - write_mask[i][j] = False - continue - sv = str(v).strip() - if sv == "": - write_mask[i][j] = False - last_val = None - else: - if last_val is None or sv != last_val: - write_mask[i][j] = True # first of a block - last_val = sv - else: - write_mask[i][j] = False # duplicate in the block; let merged cell show the first one - - for i in range(len(grid) - 1, -1, -1): - row_vals = grid[i] - row_index = start_row + i - if row_index >= len(table.rows): - table.add_row() - for j in range(min(len(row_vals), max_writable_cols) - 1, -1, -1): - val = row_vals[j] - col_index = start_col + j - cell = table.cell(row_index, col_index) - try: - if _is_vertical_merge_continuation(cell): - # respect template: don't write into continuation cells of a vertical merge - continue - except Exception: - pass - # Skip empty values to keep the template content intact - if val is None: - continue - if isinstance(val, str) and val.strip() == "": - continue - # Skip repeated labels for the same column; only write first of a consecutive block - try: - if j < len(write_mask[0]) and not write_mask[i][j]: - continue - except Exception: - pass - # Do not overwrite template's existing non-empty text - try: - if (cell.text or '').strip(): - continue - except Exception: - pass - ref = None - # use cell's own first run if present to keep local style - try: - if cell.paragraphs and cell.paragraphs[0].runs: - ref = cell.paragraphs[0].runs[0] - except Exception: - ref = None - if ref is None: - # For anchor cell, prefer pre-capture style - if row_index == r0 and col_index == c0 and anchor_ref_run is not None: - ref = anchor_ref_run - else: - ref = table_ref_run or anchor_ref_run - _write_cell_text_preserve_style(cell, val, ref) - - -def _insert_script_chart(doc, constants, token: str, chart_spec: Dict) -> None: - series = chart_spec.get("series") - if not isinstance(series, list) or not series: - logger.warning("Script chart %s has no series", token) + if table_found: + break + + if not table_found: + logger.warning("未找到 token: %s", token_with_braces) return - width = float(chart_spec.get("width", 6) or 6) - height = float(chart_spec.get("height", 3.5) or 3.5) - kind = (chart_spec.get("kind") or "line").lower() - use_grid = bool(chart_spec.get("grid", True)) - title = chart_spec.get("title") or "" - x_label = chart_spec.get("xLabel") or chart_spec.get("xlabel") or "" - y_label = chart_spec.get("yLabel") or chart_spec.get("ylabel") or "" - - import matplotlib.pyplot as plt - _setup_matplotlib_cn_font() - - with tempfile.TemporaryDirectory() as td: - img_path = Path(td) / f"{token.strip('{}')}.png" - fig, ax = plt.subplots(figsize=(width, height)) - legend_labels: List[str] = [] - for idx, serie in enumerate(series): - if not isinstance(serie, dict): + + logger.info("找到 token %s 在表格 row=%d, unique_col=%d", token_with_braces, token_row, token_unique_col) + + # 清除 token 文本(保留同一单元格中的其他文字如"环境温度") + target_cell = _get_unique_cells(table_found.rows[token_row])[token_unique_col] + for para in target_cell.paragraphs: + _replace_token_across_runs(para, token_with_braces, '') + + # 填充数据 + for cell_info in cells_data: + if not isinstance(cell_info, dict): + continue + value = cell_info.get("value") + if value is None: + continue + + data_row = int(cell_info.get("row", 0)) + data_col = int(cell_info.get("col", 0)) + + try: + if data_row >= len(table_found.rows): + logger.warning("行 %d 超出表格范围 (%d行)", data_row, len(table_found.rows)) continue - y_vals = serie.get("y") or serie.get("values") - if not isinstance(y_vals, (list, tuple)) or not y_vals: + + unique = _get_unique_cells(table_found.rows[data_row]) + target_idx = token_unique_col + data_col + + if target_idx >= len(unique): + logger.warning("列 %d (target_idx=%d) 超出范围 (%d列)", data_col, target_idx, len(unique)) continue - x_vals = serie.get("x") or serie.get("time") - if not isinstance(x_vals, (list, tuple)) or len(x_vals) != len(y_vals): - x_vals = list(range(1, len(y_vals) + 1)) - label = serie.get("label") or f"series{idx + 1}" - if kind == "bar": - ax.bar(x_vals, y_vals, label=label) + + cell = unique[target_idx] + para = cell.paragraphs[0] if cell.paragraphs else None + + if para is None: + cell.text = str(value) + elif para.runs: + # 有现有 run,修改第一个 run 的文本 + para.runs[0].text = str(value) + # 清空其余 run + for r in para.runs[1:]: + r.text = '' else: - ax.plot(x_vals, y_vals, label=label, marker=serie.get("marker", "")) - legend_labels.append(label) - if not legend_labels: - logger.warning("Script chart %s has no plottable series", token) - return - if title: - ax.set_title(str(title)) - if x_label: - ax.set_xlabel(str(x_label)) - if y_label: - ax.set_ylabel(str(y_label)) - if use_grid: - ax.grid(True, alpha=0.3) - if len(legend_labels) > 1 or chart_spec.get("showLegend", True): - ax.legend(loc=chart_spec.get("legendLoc", "best")) - fig.tight_layout() - fig.savefig(img_path, dpi=int(chart_spec.get("dpi", 150) or 150)) - plt.close(fig) - - for rng in _find_token_ranges_word(doc, constants, token): - _delete_token_range_word(rng) - _insert_picture_at_range_word(rng, img_path, "") + # 没有 run,添加一个新 run + para.add_run(str(value)) + + # 设置居中对齐 + if para is not None: + para.alignment = WD_ALIGN_PARAGRAPH.CENTER + + except Exception as e: + logger.warning("填充失败 row=%d col=%d: %s", data_row, data_col, e) -def _load_script_data_from_db(experiment_id: int) -> Optional[Dict]: - """ - 从数据库加载已保存的脚本数据 +# ============================================================ +# 报告生成入口 +# ============================================================ + +def render_report(template_path, cfg, output_path, experiment_id=None): + logger.info("=== 开始生成报告 ===") + _progress("加载数据", 0, 5) - Args: - experiment_id: 实验记录ID - - Returns: - 脚本数据字典,如果没有数据返回None - """ - try: - import sqlite3 - import json - from pathlib import Path - - db_path = Path(__file__).parent / "experiments.db" - - conn = sqlite3.connect(str(db_path)) - cursor = conn.cursor() - - cursor.execute( - "SELECT script_data FROM experiments WHERE id=?", - (experiment_id,) - ) - result = cursor.fetchone() - conn.close() - - if result and result[0]: - script_data_json = result[0] - script_data = json.loads(script_data_json) - logger.info("Loaded script data from database for experiment %d", experiment_id) - return script_data - else: - logger.info("No script data found in database for experiment %d", experiment_id) - return None + # 加载脚本数据和实验信息 + script_data = _load_script_data_from_db(experiment_id) if experiment_id else None + script_tables = _parse_script_tables(script_data) + logger.info("脚本表格: %s", list(script_tables.keys())) - except Exception as e: - logger.error("Failed to load script data from database: %s", e, exc_info=True) - return None - - + # 打开模板 + doc = Document(str(template_path)) + _progress("替换文本", 1, 5) + + # 构建文本映射 + text_map = {} + if hasattr(cfg, 'placeholders'): + placeholders = cfg.placeholders if isinstance(cfg.placeholders, dict) else {} + for key, ph in placeholders.items(): + if hasattr(ph, 'type'): + if ph.type == "text" and hasattr(ph, 'value'): + text_map[key] = _replace_global_params(ph.value or '', cfg) + elif ph.type == "dbText" and hasattr(ph, 'dbQuery'): + text_map[key] = _execute_db_query(ph, getattr(cfg, 'db', None)) + + # 添加实验信息占位符(isNormal 打勾) + # 无论如何都要添加,避免占位符未被替换 + is_normal_checked = '' + if experiment_id: + exp_info = _load_experiment_info(experiment_id) + if exp_info and exp_info.get('is_normal'): + is_normal_checked = '\u2611' + text_map['isNormal'] = is_normal_checked + + logger.info("文本映射: %d 个, keys=%s", len(text_map), list(text_map.keys())) + _replace_texts_docx(doc, text_map) + + # 填充脚本表格数据 + _progress("填充表格", 2, 5) + for token, spec in script_tables.items(): + _fill_script_table_docx(doc, token, spec) + + # 保存 + _progress("保存", 4, 5) + doc.save(str(output_path)) + _progress("完成", 5, 5) + logger.info("=== 报告生成完成: %s ===", output_path) + return output_path def _execute_experiment_script(cfg: AppConfig) -> Optional[Dict]: """ 执行实验流程中的Python脚本 @@ -1399,17 +389,14 @@ def _execute_experiment_script(cfg: AppConfig) -> Optional[Dict]: Returns: 脚本返回的JSON数据,如果没有脚本或执行失败返回None """ - report_debug_logger.info("=== 开始执行实验脚本 ===") + logger.info("_execute_experiment_script invoked") if not cfg.experimentProcess.scriptFile: - report_debug_logger.info("没有配置实验脚本") + logger.info("No experiment script configured") return None - report_debug_logger.info("实验脚本配置存在,脚本名称: %s", cfg.experimentProcess.scriptName) - report_debug_logger.info("脚本文件大小: %d 字符", len(cfg.experimentProcess.scriptFile)) - try: import base64 import json @@ -1553,19 +540,10 @@ def _execute_experiment_script(cfg: AppConfig) -> Optional[Dict]: stdout_text: str = "" stderr_text: str = "" - report_debug_logger.info("准备执行实验脚本: %s", cfg.experimentProcess.scriptName) - report_debug_logger.info("环境变量设置:") - for key, value in script_env.items(): - if key.startswith(('EXPERIMENT_', 'INFLUX_')): - if 'TOKEN' in key: - report_debug_logger.info(" %s: %s****", key, value[:8] if value else '') - else: - report_debug_logger.info(" %s: %s", key, value) logger.info("Executing experiment script: %s", cfg.experimentProcess.scriptName) if experiment_start and experiment_end: logger.info("Experiment time range: %s to %s", experiment_start, experiment_end) - report_debug_logger.info("实验时间范围: %s 到 %s", experiment_start, experiment_end) used_external = False if candidates: last_err = None @@ -1596,19 +574,8 @@ def _execute_experiment_script(cfg: AppConfig) -> Optional[Dict]: stdout_text = (result.stdout or '') stderr_text = (result.stderr or '') - report_debug_logger.info("脚本执行完成 (外部进程)") - report_debug_logger.info("返回代码: %d", result.returncode) - report_debug_logger.info("标准输出长度: %d", len(stdout_text)) - report_debug_logger.info("标准错误长度: %d", len(stderr_text)) - - if stdout_text: - report_debug_logger.info("标准输出内容 (前1000字符): %s", stdout_text[:1000]) - if stderr_text: - report_debug_logger.warning("标准错误内容: %s", stderr_text) - # 增强错误处理:记录详细的错误信息 if result.returncode != 0: - report_debug_logger.error("脚本执行失败: 返回代码=%d", result.returncode) logger.error("Script execution failed (ext): return_code=%d, stdout=%s, stderr=%s", result.returncode, stdout_text, stderr_text) return None @@ -1659,43 +626,24 @@ def _execute_experiment_script(cfg: AppConfig) -> Optional[Dict]: # 解析JSON输出 output = (stdout_text or '').strip() - report_debug_logger.info("=== 解析脚本输出 ===") - report_debug_logger.info("原始输出长度: %d", len(output)) if not output: - report_debug_logger.warning("脚本没有输出,使用fallback到EXPERIMENT_JSON") logger.warning("Script executed but returned no output; applying fallback to EXPERIMENT_JSON") output = exp_json - report_debug_logger.info("准备解析的JSON长度: %d", len(output)) - report_debug_logger.info("JSON内容 (前500字符): %s", output[:500]) - try: data = json.loads(output) - report_debug_logger.info("JSON解析成功") - report_debug_logger.info("数据类型: %s", type(data).__name__) if isinstance(data, dict): - report_debug_logger.info("字典键: %s", list(data.keys())) if 'tables' in data: tables = data['tables'] - report_debug_logger.info("包含 %d 个表格", len(tables) if isinstance(tables, list) else 0) if isinstance(tables, list) and tables: first_table = tables[0] if isinstance(first_table, dict): - report_debug_logger.info("第一个表格信息:") - report_debug_logger.info(" token: %s", first_table.get('token')) - report_debug_logger.info(" startRow: %s", first_table.get('startRow')) - report_debug_logger.info(" startCol: %s", first_table.get('startCol')) cells = first_table.get('cells', []) - report_debug_logger.info(" cells数量: %d", len(cells) if isinstance(cells, list) else 0) - if isinstance(cells, list) and cells: - report_debug_logger.info(" 前3个单元格: %s", cells[:3]) except Exception as e: # 增强错误处理:提供更详细的错误信息 - report_debug_logger.error("JSON解析失败: %s", e) - report_debug_logger.error("解析失败的内容 (前1000字符): %s", output[:1000]) logger.error("Failed to parse script output as JSON: error=%s, output=%s", e, output[:1000]) return None @@ -1704,7 +652,6 @@ def _execute_experiment_script(cfg: AppConfig) -> Optional[Dict]: len(data.get('headers', []) if isinstance(data, dict) else []), len(data.get('rows', []) if isinstance(data, dict) else [])) - report_debug_logger.info("=== 脚本执行成功,返回数据 ===") return data finally: @@ -1726,455 +673,3 @@ def _execute_experiment_script(cfg: AppConfig) -> Optional[Dict]: logger.error("Failed to execute experiment script: %s", e, exc_info=True) return None - -def _handle_existing_word_processes(): - """检查并处理现有的Word进程""" - import psutil - import time - - try: - report_debug_logger.info("检查现有Word进程...") - - word_processes = [] - for proc in psutil.process_iter(['pid', 'name']): - try: - if proc.info['name'].lower() in ['winword.exe', 'word.exe']: - word_processes.append(proc) - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - - if word_processes: - report_debug_logger.warning("发现 %d 个Word进程正在运行", len(word_processes)) - for proc in word_processes: - report_debug_logger.info("Word进程: PID=%d", proc.pid) - - # 给用户一些提示,但不强制关闭进程 - report_debug_logger.info("建议: 关闭所有Word窗口以避免冲突") - report_debug_logger.info("程序将尝试创建独立的Word实例...") - - else: - report_debug_logger.info("未发现运行中的Word进程") - - except ImportError: - report_debug_logger.warning("psutil库不可用,无法检查Word进程") - except Exception as e: - report_debug_logger.warning("检查Word进程时出错: %s", e) - - -def render_report(template_path: Path, cfg: AppConfig, output_path: Path, experiment_id: Optional[int] = None) -> Path: - report_debug_logger.info("=== 开始报告生成 ===") - report_debug_logger.info("模板路径: %s", template_path) - report_debug_logger.info("输出路径: %s", output_path) - report_debug_logger.info("实验ID: %s", experiment_id) - report_debug_logger.info("模板文件存在: %s", template_path.exists() if template_path else False) - - logger.info("Render start: template=%s, output=%s, experiment_id=%s", template_path, output_path, experiment_id) - _progress("打开模板…", 0, 1) - - # 优先从数据库读取脚本数据(如果提供了experiment_id) - script_data = None - if experiment_id: - script_data = _load_script_data_from_db(experiment_id) - if script_data: - report_debug_logger.info("✅ 从数据库加载脚本数据成功") - logger.info("Script data loaded from database for experiment %d", experiment_id) - else: - report_debug_logger.info("⚠️ 数据库中没有脚本数据,将尝试执行脚本") - - # 如果数据库中没有数据,执行实验流程脚本 - if not script_data: - report_debug_logger.info("=== 步骤1: 执行实验脚本 ===") - script_data = _execute_experiment_script(cfg) - report_debug_logger.info("脚本执行结果: %s", "成功" if script_data else "失败或无数据") - - if script_data: - report_debug_logger.info("脚本数据类型: %s", type(script_data).__name__) - if isinstance(script_data, dict): - report_debug_logger.info("脚本数据键: %s", list(script_data.keys())) - - print("script_data:", script_data) - - report_debug_logger.info("=== 步骤2: 解析脚本表格数据 ===") - script_tables = _parse_script_tables(script_data) - script_charts = _parse_script_charts(script_data) - - report_debug_logger.info("解析到的表格数量: %d", len(script_tables) if script_tables else 0) - report_debug_logger.info("解析到的图表数量: %d", len(script_charts) if script_charts else 0) - - if script_tables: - logger.info("Script data available for report generation: %s", list(script_tables.keys())) - report_debug_logger.info("可用的脚本表格: %s", list(script_tables.keys())) - if script_charts: - logger.info("Script chart data available: %s", list(script_charts.keys())) - report_debug_logger.info("可用的脚本图表: %s", list(script_charts.keys())) - - report_debug_logger.info("=== 步骤3: 初始化Word应用程序 ===") - - # 检查并处理现有Word进程 - _handle_existing_word_processes() - - pythoncom.CoInitialize() - word = None - doc = None - try: - report_debug_logger.info("创建Word应用程序对象...") - - # 尝试创建新的Word实例,避免连接到现有实例 - word_creation_success = False - creation_methods = [ - ("DispatchEx新实例", lambda: win32.client.DispatchEx('Word.Application')), - ("Dispatch标准方法", lambda: win32.client.Dispatch('Word.Application')), - ("EnsureDispatch缓存方法", lambda: win32.gencache.EnsureDispatch('Word.Application')) - ] - - for method_name, create_func in creation_methods: - try: - report_debug_logger.info("尝试方法: %s", method_name) - word = create_func() - report_debug_logger.info("Word应用程序创建成功 (%s)", method_name) - word_creation_success = True - break - except Exception as e: - report_debug_logger.warning("方法 %s 失败: %s", method_name, e) - continue - - if not word_creation_success: - report_debug_logger.error("所有Word创建方法都失败") - raise RuntimeError("无法创建Word应用程序实例,请确保Word已正确安装且没有权限问题") - - # 配置Word实例以避免与用户实例冲突 - try: - # 确保Word不可见,避免干扰用户 - word.Visible = False - report_debug_logger.info("Word设置为不可见") - - # 禁用UI更新和警告 - word.ScreenUpdating = False - word.DisplayAlerts = False - - # 设置为自动化模式,减少用户交互 - if hasattr(word, 'AutomationSecurity'): - word.AutomationSecurity = 1 # msoAutomationSecurityLow - report_debug_logger.info("Word自动化安全设置已配置") - - # 禁用启动任务窗格 - if hasattr(word, 'ShowStartupDialog'): - word.ShowStartupDialog = False - - report_debug_logger.info("Word显示和安全设置已配置") - - except Exception as e: - report_debug_logger.warning("配置Word设置失败: %s", e) - - constants = win32.constants - - report_debug_logger.info("=== 步骤4: 打开模板文档 ===") - report_debug_logger.info("尝试打开模板: %s", template_path) - - # 尝试打开文档,处理可能的冲突 - try: - # 使用只读模式打开,避免文件锁定冲突 - doc = word.Documents.Open( - FileName=str(template_path), - ReadOnly=False, - AddToRecentFiles=False, - Visible=False - ) - report_debug_logger.info("模板文档打开成功") - - except Exception as e: - report_debug_logger.error("打开模板文档失败: %s", e) - report_debug_logger.info("可能的原因:") - report_debug_logger.info("1. 模板文件被其他程序占用") - report_debug_logger.info("2. Word实例冲突") - report_debug_logger.info("3. 文件权限问题") - raise - - report_debug_logger.info("文档表格数量: %d", doc.Tables.Count) - - influx = _build_influx_service(cfg) - - # 辅助函数:处理全局参数替换 - def replace_global_params(text: str) -> str: - """替换文本中的 @参数名 为全局参数的值""" - if not text or '@' not in text: - return text - - result = text - global_params = getattr(cfg, 'globalParameters', None) - if global_params and hasattr(global_params, 'parameters'): - import re - # 查找所有 @参数名 格式的引用 - pattern = r'@(\w+)' - matches = re.findall(pattern, text) - for param_name in matches: - if param_name in global_params.parameters: - param_value = global_params.parameters[param_name] - result = result.replace(f'@{param_name}', param_value) - logger.debug("Replaced @%s with '%s'", param_name, param_value) - else: - logger.warning("Global parameter @%s not found", param_name) - return result - - # 检查数据完整性:检查脚本表格数据是否有空值 - def check_data_completeness(script_tables: Dict[str, Dict]) -> bool: - """检查脚本表格数据是否完整(无空值)""" - if not script_tables: - return False - - for table_spec in script_tables.values(): - cells = table_spec.get("cells") or table_spec.get("values") or [] - if not isinstance(cells, list): - continue - - # 检查所有单元格,如果发现空值则认为数据不完整 - for cell in cells: - if isinstance(cell, dict): - value = cell.get("value", "") - # 如果值为空字符串、None或只包含空白字符,认为数据不完整 - if not value or (isinstance(value, str) and value.strip() == ""): - logger.debug("发现空单元格: row=%s, col=%s", cell.get("row"), cell.get("col")) - return False - - return True - - # 1) 文本替换(普通文本和数据库文本) - text_map: Dict[str, str] = {} - for k, ph in cfg.placeholders.items(): - if ph.type == "text": - # 处理普通文本,支持@参数替换 - raw_value = ph.value or "" - text_map[k] = replace_global_params(raw_value) - # 数据库文本:执行查询并获取结果 - db_text_keys = [k for k, ph in cfg.placeholders.items() if ph.type == "dbText"] - for key in db_text_keys: - ph = cfg.placeholders.get(key) - if ph and ph.dbQuery: - try: - query_result = _execute_db_query(ph, cfg.db) - text_map[key] = query_result - logger.info("Database query for %s: %s", key, query_result[:100] if len(query_result) > 100 else query_result) - except Exception as e: - logger.error("Failed to execute database query for %s: %s", key, e) - text_map[key] = "" - else: - text_map[key] = "" - - # 添加数据完整性检查占位符:{isNormal} 或 {normalCheck} - # 数据完整:显示打钩符号 ☑,数据不完整:显示空框 ☐ - is_data_complete = check_data_completeness(script_tables) - text_map["isNormal"] = "☑" if is_data_complete else "☐" - text_map["normalCheck"] = "☑" if is_data_complete else "☐" - logger.info("数据完整性检查: %s (完整=%s)", "正常" if is_data_complete else "异常", is_data_complete) - - _replace_texts_word(doc, constants, text_map) - - # 2) 表格渲染(按占位符 {tableX}) - table_keys = [k for k, ph in cfg.placeholders.items() if ph.type == "table"] - chart_keys = [k for k, ph in cfg.placeholders.items() if ph.type == "chart"] - manual_keys = [k for k, ph in cfg.placeholders.items() if ph.type == "cell"] - script_table_keys = [k for k, ph in cfg.placeholders.items() if ph.type == "scriptTable"] - script_chart_keys = [k for k, ph in cfg.placeholders.items() if ph.type == "scriptChart"] - - total_steps = len(table_keys) + len(chart_keys) + len(manual_keys) + len(script_table_keys) + len(script_chart_keys) + 2 - step = 1 - for key in table_keys: - step += 1 - _progress(f"插入表格 {key}…", step, total_steps) - ph = cfg.placeholders.get(key) - if not ph: - continue - df = _query_df(influx, ph) - if not df.empty: - keep_cols = [c for c in ["_time", "_field", "_value"] if c in df.columns] - other_cols = [c for c in df.columns if c not in keep_cols and not str(c).startswith("_")] - cols = keep_cols + other_cols - df = df.loc[:, cols] - wide = _to_wide_table( - df, - ph.influx.fields if (ph.influx and ph.influx.fields) else [], - ph.table.firstColumn if ph.table else "time", - ph.table.titles if ph.table else {}, - ph.table.firstTitle if ph.table and ph.table.firstTitle else None, - ) - df_to_render = wide if not wide.empty else _format_numeric_columns(df, exclude_cols=["_time", "时间", "秒"]) - token = '{' + key + '}' - for rng in _find_token_ranges_word(doc, constants, token): - # erase token then insert table at range - _delete_token_range_word(rng) - _insert_table_at_range_word(doc, rng, df_to_render, constants, ph.title or ph.label or key) - - # 3) 手填表 {tbX} - for key in manual_keys: - step += 1 - _progress(f"插入手填表 {key}…", step, total_steps) - ph = cfg.placeholders.get(key) - if not ph: - continue - grid = ph.grid or [[""]] - token = '{' + key + '}' - _fill_manual_table_at_token_word(doc, constants, token, grid) - - # 4) 脚本驱动表 {scriptTableX} - report_debug_logger.info("=== 步骤5: 处理脚本表格 ===") - report_debug_logger.info("脚本表格键数量: %d", len(script_table_keys)) - report_debug_logger.info("脚本表格键列表: %s", script_table_keys) - - for key in script_table_keys: - step += 1 - _progress(f"插入脚本表格 {key}…", step, total_steps) - token = '{' + key + '}' - - report_debug_logger.info("--- 处理脚本表格: %s ---", key) - report_debug_logger.info("查找token: %s", token) - - table_spec = script_tables.get(key) - if not table_spec: - report_debug_logger.warning("未找到脚本表格数据: %s", key) - report_debug_logger.info("可用的脚本表格: %s", list(script_tables.keys()) if script_tables else []) - logger.warning("No script table data provided for %s", key) - continue - - report_debug_logger.info("找到表格规格: %s", key) - cells = table_spec.get('cells', []) - report_debug_logger.info("表格单元格数量: %d", len(cells)) - report_debug_logger.info("表格规格详情: token=%s, startRow=%s, startCol=%s", - table_spec.get('token'), table_spec.get('startRow'), table_spec.get('startCol')) - - logger.info("Processing script table %s with %d cells", key, len(cells)) - - try: - report_debug_logger.info("开始填充脚本表格...") - _fill_script_table_at_token_word(doc, constants, token, table_spec) - report_debug_logger.info("脚本表格填充完成: %s", key) - except Exception as e: - report_debug_logger.error("脚本表格填充失败: %s, 错误: %s", key, e) - logger.error("Failed to fill script table %s: %s", key, e) - raise - - # 5) 脚本驱动图表 {scriptChartX} - for key in script_chart_keys: - step += 1 - _progress(f"插入脚本图表 {key}…", step, total_steps) - token = '{' + key + '}' - chart_spec = script_charts.get(key) - if not chart_spec: - logger.warning("No script chart data provided for %s", key) - continue - _insert_script_chart(doc, constants, token, chart_spec) - - # 6) 图表 {chartX} - for key in chart_keys: - step += 1 - _progress(f"插入图表 {key}…", step, total_steps) - ph = cfg.placeholders.get(key) - if not ph: - continue - df = _query_df(influx, ph) - import matplotlib.pyplot as plt - _setup_matplotlib_cn_font() - with tempfile.TemporaryDirectory() as td: - img_path = Path(td) / f"{key}.png" - fig, ax = plt.subplots(figsize=(6, 3)) - if not df.empty and "_time" in df.columns and "_value" in df.columns: - fields = ph.influx.fields if (ph.influx and ph.influx.fields) else sorted([str(f) for f in df.get("_field", pd.Series(dtype=str)).unique() if pd.notna(f)]) - if not fields: - fields = ["_value"] - for field in fields: - sdf = df[df["_field"] == field] if "_field" in df.columns else df - if not sdf.empty: - label = ph.table.titles.get(field, field) if getattr(ph, 'table', None) else field - ax.plot(pd.to_datetime(sdf["_time"]), pd.to_numeric(sdf["_value"], errors="coerce"), label=str(label)) - ax.set_xlabel("Time"); ax.set_ylabel("Value"); ax.grid(True, alpha=0.3); ax.legend(loc="best") - else: - ax.text(0.5, 0.5, "无可绘制的数据", ha="center", va="center", transform=ax.transAxes) - fig.tight_layout(); fig.savefig(img_path, dpi=150); plt.close(fig) - token = '{' + key + '}' - for rng in _find_token_ranges_word(doc, constants, token): - _delete_token_range_word(rng) - # 图表不插入标题 - _insert_picture_at_range_word(rng, img_path, "") - - # 保存 - step += 1 - _progress("保存文档…", step, total_steps) - report_debug_logger.info("=== 步骤6: 保存文档 ===") - report_debug_logger.info("输出路径: %s", output_path) - - output_path.parent.mkdir(parents=True, exist_ok=True) - doc.SaveAs2(str(output_path), FileFormat=win32.constants.wdFormatXMLDocument) - - report_debug_logger.info("文档保存成功") - logger.info("Render finished: %s", output_path) - _progress("完成", total_steps, total_steps) - - report_debug_logger.info("=== 报告生成完成 ===") - return output_path - - except Exception as e: - report_debug_logger.error("=== 报告生成过程中发生错误 ===") - report_debug_logger.error("错误类型: %s", type(e).__name__) - report_debug_logger.error("错误信息: %s", str(e)) - report_debug_logger.error("错误详情:", exc_info=True) - raise - - finally: - report_debug_logger.info("=== 清理资源 ===") - - # 强制清理Word实例,避免进程残留 - cleanup_success = False - - try: - if doc is not None: - report_debug_logger.info("关闭Word文档") - doc.Close(SaveChanges=False) - doc = None - report_debug_logger.info("文档关闭成功") - except Exception as e: - report_debug_logger.warning("关闭文档失败: %s", e) - - try: - if word is not None: - report_debug_logger.info("退出Word应用程序") - - # 恢复Word设置 - try: - word.ScreenUpdating = True - word.DisplayAlerts = True - report_debug_logger.info("Word设置已恢复") - except Exception: - pass - - # 强制退出Word实例 - try: - word.Quit() - word = None - cleanup_success = True - report_debug_logger.info("Word应用程序已正常退出") - except Exception as e: - report_debug_logger.warning("正常退出Word失败: %s", e) - - # 尝试强制退出 - try: - import gc - word = None - gc.collect() - report_debug_logger.info("Word对象已强制清理") - cleanup_success = True - except Exception as e2: - report_debug_logger.error("强制清理Word失败: %s", e2) - - except Exception as e: - report_debug_logger.error("Word清理过程异常: %s", e) - - # 最终清理COM - try: - pythoncom.CoUninitialize() - report_debug_logger.info("COM已清理") - except Exception as e: - report_debug_logger.warning("COM清理失败: %s", e) - - if cleanup_success: - report_debug_logger.info("资源清理完成") - else: - report_debug_logger.warning("资源清理可能不完整,建议重启应用程序") diff --git a/report_generator.py.bak2 b/report_generator.py.bak2 new file mode 100644 index 0000000..8a8f6c1 --- /dev/null +++ b/report_generator.py.bak2 @@ -0,0 +1,240 @@ +from __future__ import annotations +import os, json, subprocess, sys +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional +import pandas as pd +from docx import Document +from config_model import AppConfig, PlaceholderConfig, DbConnectionConfig +from influx_service import InfluxConnectionParams, InfluxService +from logger import get_logger + +logger = get_logger() +_PROGRESS_CB: Optional[Callable[[str, int, int], None]] = None + +def set_progress_callback(cb): + global _PROGRESS_CB; _PROGRESS_CB = cb +def _progress(msg, cur, total): + if _PROGRESS_CB: _PROGRESS_CB(msg, cur, total) + +def _build_influx_service(cfg): + return InfluxService(InfluxConnectionParams(url=cfg.influx.url, org=cfg.influx.org, token=cfg.influx.token)) + +def _execute_db_query(ph, db_cfg): + query = (ph.dbQuery or "").strip() + if not query: return "" + if not db_cfg: db_cfg = DbConnectionConfig() + engine = (db_cfg.engine or "mysql").lower() + + if engine in ("sqlite", "sqlite3"): + import sqlite3 + conn = sqlite3.connect(db_cfg.database or str(Path(__file__).parent / "experiments.db")) + result = conn.execute(query).fetchone() + conn.close() + return str(result[0]) if result and result[0] else "" + elif engine == "mysql": + import pymysql + conn = pymysql.connect(host=getattr(db_cfg, "host", "localhost"), port=int(getattr(db_cfg, "port", 3306)), + user=getattr(db_cfg, "username", ""), password=getattr(db_cfg, "password", ""), + database=getattr(db_cfg, "database", ""), charset="utf8mb4") + with conn.cursor() as cursor: + cursor.execute(query) + result = cursor.fetchone() + conn.close() + return str(result[0]) if result and result[0] else "" + return "" + +def _load_script_data_from_db(experiment_id): + try: + import sqlite3 + conn = sqlite3.connect(str(Path(__file__).parent / "experiments.db")) + result = conn.execute("SELECT script_data FROM experiments WHERE id=?", (experiment_id,)).fetchone() + conn.close() + if result and result[0]: + logger.info("从数据库加载脚本数据,实验ID: %d", experiment_id) + return json.loads(result[0]) + except Exception as e: + logger.error("加载脚本数据失败: %s", e) + return None + +def _load_experiment_info(experiment_id): + try: + import sqlite3 + conn = sqlite3.connect(str(Path(__file__).parent / "experiments.db")) + result = conn.execute("SELECT status FROM experiments WHERE id=?", (experiment_id,)).fetchone() + conn.close() + if result: + return {'is_normal': result[0] == 'completed'} + except Exception as e: + logger.error("加载实验信息失败: %s", e) + return None + +def _parse_script_tables(script_data): + tables = {} + if isinstance(script_data, dict) and "tables" in script_data: + for item in script_data["tables"]: + key = item.get("token") or item.get("key") + if key: tables[str(key)] = item + return tables + +def _replace_global_params(text, cfg): + """替换文本中的 @参数名 为全局参数的值""" + if not text or '@' not in text: return text + result = text + if hasattr(cfg, 'globalParameters') and hasattr(cfg.globalParameters, 'parameters'): + import re + for param_name in re.findall(r'@(\w+)', text): + if param_name in cfg.globalParameters.parameters: + result = result.replace(f'@{param_name}', cfg.globalParameters.parameters[param_name]) + return result + +def _make_seconds_index(df): + if "_time" in df.columns: + t = pd.to_datetime(df["_time"]) + return (t - t.iloc[0]).dt.total_seconds().round().astype(int) + return pd.Series(range(len(df))) + +def _format_numeric_columns(df, exclude_cols): + if df is None or df.empty: return df + result = df.copy() + for col in result.columns: + if col not in exclude_cols: + try: + numeric = pd.to_numeric(result[col], errors="coerce") + if numeric.notna().any(): result[col] = numeric.round(2) + except: pass + return result + +def _to_wide_table(df, fields, first_column, titles_map, first_title=None): + if df.empty: return pd.DataFrame() + work = df.copy() + if "_time" not in work.columns or "_value" not in work.columns: return work + if fields and "_field" in work.columns: work = work[work["_field"].isin(fields)] + + if first_column == "seconds": + idx = _make_seconds_index(work) + work = work.assign(__index__=idx) + index_col, index_title = "__index__", first_title or "秒" + else: + index_col, index_title = "_time", first_title or "时间" + + if "_field" in work.columns: + wide = work.pivot_table(index=index_col, columns="_field", values="_value", aggfunc="last") + else: + wide = work.set_index(index_col)[["_value"]] + wide.columns = ["value"] + + wide = wide.sort_index() + wide.reset_index(inplace=True) + wide.rename(columns={index_col: index_title}, inplace=True) + for f, title in titles_map.items(): + if f in wide.columns: wide.rename(columns={f: title}, inplace=True) + return _format_numeric_columns(wide, exclude_cols=[index_title]) + +def _replace_texts_docx(doc, mapping): + for key, val in mapping.items(): + token = '{' + key + '}' + replacement = val or '' + for para in doc.paragraphs: + if token in para.text: + for run in para.runs: + if token in run.text: + run.text = run.text.replace(token, replacement) + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + for para in cell.paragraphs: + if token in para.text: + for run in para.runs: + if token in run.text: + run.text = run.text.replace(token, replacement) + +def _fill_script_table_docx(doc, token, table_spec): + cells = table_spec.get("cells") or [] + if not cells: return + + token_with_braces = '{' + token + '}' + table_found = None + token_row = token_col = 0 + + for table in doc.tables: + for ri, row in enumerate(table.rows): + for ci, cell in enumerate(row.cells): + if token_with_braces in cell.text: + table_found, token_row, token_col = table, ri, ci + break + if table_found: break + if table_found: break + + if not table_found: + logger.warning("未找到token: %s", token_with_braces) + return + + # 清除token + for para in table_found.rows[token_row].cells[token_col].paragraphs: + for run in para.runs: + if token_with_braces in run.text: + run.text = run.text.replace(token_with_braces, '') + + # 填充数据 - 使用绝对位置(row/col直接是表格坐标) + for cell_info in cells: + if not isinstance(cell_info, dict): continue + value = cell_info.get("value") + if value is None: continue + + abs_row = int(cell_info.get("row", 0)) + abs_col = int(cell_info.get("col", 0)) + + try: + if abs_row < len(table_found.rows) and abs_col < len(table_found.rows[abs_row].cells): + cell = table_found.rows[abs_row].cells[abs_col] + if cell.paragraphs and cell.paragraphs[0].runs: + cell.paragraphs[0].runs[0].text = str(value) + else: + cell.text = str(value) + except Exception as e: + logger.warning("填充失败 (%d,%d): %s", abs_row, abs_col, e) + +def render_report(template_path, cfg, output_path, experiment_id=None): + logger.info("=== 开始生成报告 ===") + _progress("加载数据", 0, 5) + + # 加载脚本数据和实验信息 + script_data = _load_script_data_from_db(experiment_id) if experiment_id else None + script_tables = _parse_script_tables(script_data) + logger.info("脚本表格: %s", list(script_tables.keys())) + + # 打开模板 + doc = Document(str(template_path)) + _progress("替换文本", 1, 5) + + # 构建文本映射 + text_map = {} + if hasattr(cfg, 'placeholders'): + placeholders = cfg.placeholders if isinstance(cfg.placeholders, dict) else {} + for key, ph in placeholders.items(): + if hasattr(ph, 'type'): + if ph.type == "text" and hasattr(ph, 'value'): + text_map[key] = _replace_global_params(ph.value or '', cfg) + elif ph.type == "dbText" and hasattr(ph, 'dbQuery'): + text_map[key] = _execute_db_query(ph, getattr(cfg, 'db', None)) + + # 添加实验信息占位符 + if experiment_id: + exp_info = _load_experiment_info(experiment_id) + if exp_info: + text_map['isNormal'] = '√' if exp_info.get('is_normal') else '' + + logger.info("文本映射: %d 个", len(text_map)) + _replace_texts_docx(doc, text_map) + + # 填充表格 + _progress("填充表格", 2, 5) + for token, spec in script_tables.items(): + _fill_script_table_docx(doc, token, spec) + + # 保存 + _progress("保存", 4, 5) + doc.save(str(output_path)) + _progress("完成", 5, 5) + logger.info("=== 报告生成完成 ===") + return output_path diff --git a/report_generator_docx.py b/report_generator_docx.py new file mode 100644 index 0000000..ad05eaf --- /dev/null +++ b/report_generator_docx.py @@ -0,0 +1,163 @@ +from __future__ import annotations +import os, json, subprocess, sys +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional +import pandas as pd +from docx import Document +from config_model import AppConfig, PlaceholderConfig, DbConnectionConfig +from influx_service import InfluxConnectionParams, InfluxService +from logger import get_logger + +logger = get_logger() +_PROGRESS_CB: Optional[Callable[[str, int, int], None]] = None + +def set_progress_callback(cb): + global _PROGRESS_CB; _PROGRESS_CB = cb +def _progress(msg, cur, total): + if _PROGRESS_CB: _PROGRESS_CB(msg, cur, total) + +def _build_influx_service(cfg): + return InfluxService(InfluxConnectionParams(url=cfg.influx.url, org=cfg.influx.org, token=cfg.influx.token)) + +def _execute_db_query(ph, db_cfg): + query = (ph.dbQuery or "").strip() + if not query: return "" + if not db_cfg: db_cfg = DbConnectionConfig() + engine = (db_cfg.engine or "mysql").lower() + + if engine in ("sqlite", "sqlite3"): + import sqlite3 + conn = sqlite3.connect(db_cfg.database or str(Path(__file__).parent / "experiments.db")) + result = conn.execute(query).fetchone() + conn.close() + return str(result[0]) if result and result[0] else "" + elif engine == "mysql": + import pymysql + conn = pymysql.connect(host=getattr(db_cfg, "host", "localhost"), port=int(getattr(db_cfg, "port", 3306)), + user=getattr(db_cfg, "username", ""), password=getattr(db_cfg, "password", ""), + database=getattr(db_cfg, "database", ""), charset="utf8mb4") + with conn.cursor() as cursor: + cursor.execute(query) + result = cursor.fetchone() + conn.close() + return str(result[0]) if result and result[0] else "" + return "" + +def _load_script_data_from_db(experiment_id): + try: + import sqlite3 + conn = sqlite3.connect(str(Path(__file__).parent / "experiments.db")) + result = conn.execute("SELECT script_data FROM experiments WHERE id=?", (experiment_id,)).fetchone() + conn.close() + if result and result[0]: + logger.info("从数据库加载脚本数据,实验ID: %d", experiment_id) + return json.loads(result[0]) + except Exception as e: + logger.error("加载脚本数据失败: %s", e) + return None + +def _parse_script_tables(script_data): + tables = {} + if isinstance(script_data, dict) and "tables" in script_data: + for item in script_data["tables"]: + key = item.get("token") or item.get("key") + if key: tables[str(key)] = item + return tables + +def _replace_texts_docx(doc, mapping): + for key, val in mapping.items(): + token = '{' + key + '}' + replacement = val or '' + for para in doc.paragraphs: + if token in para.text: + for run in para.runs: + if token in run.text: + run.text = run.text.replace(token, replacement) + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + for para in cell.paragraphs: + if token in para.text: + for run in para.runs: + if token in run.text: + run.text = run.text.replace(token, replacement) + +def _fill_script_table_docx(doc, token, table_spec): + cells = table_spec.get("cells") or [] + if not cells: return + + token_with_braces = '{' + token + '}' + table_found = None + token_row = token_col = 0 + + for table in doc.tables: + for ri, row in enumerate(table.rows): + for ci, cell in enumerate(row.cells): + if token_with_braces in cell.text: + table_found, token_row, token_col = table, ri, ci + break + if table_found: break + if table_found: break + + if not table_found: + logger.warning("未找到token: %s", token_with_braces) + return + + # 清除token + for para in table_found.rows[token_row].cells[token_col].paragraphs: + for run in para.runs: + run.text = run.text.replace(token_with_braces, '') + + # 填充数据 + for cell_info in cells: + if not isinstance(cell_info, dict): continue + value = cell_info.get("value") + if value is None: continue + + row = int(cell_info.get("row", 0)) + col = int(cell_info.get("col", 0)) + + try: + if row < len(table_found.rows) and col < len(table_found.rows[row].cells): + table_found.rows[row].cells[col].text = str(value) + except Exception as e: + logger.warning("填充失败 (%d,%d): %s", row, col, e) + +def render_report(template_path, cfg, output_path, experiment_id=None): + logger.info("=== 开始生成报告 ===") + _progress("加载数据", 0, 5) + + # 加载脚本数据 + script_data = _load_script_data_from_db(experiment_id) if experiment_id else None + script_tables = _parse_script_tables(script_data) + logger.info("脚本表格: %s", list(script_tables.keys())) + + # 打开模板 + doc = Document(str(template_path)) + _progress("替换文本", 1, 5) + + # 构建文本映射 + text_map = {} + if hasattr(cfg, 'placeholders'): + placeholders = cfg.placeholders if isinstance(cfg.placeholders, dict) else {} + for key, ph in placeholders.items(): + if hasattr(ph, 'type'): + if ph.type == "text" and hasattr(ph, 'value'): + text_map[key] = ph.value or '' + elif ph.type == "dbText" and hasattr(ph, 'dbQuery'): + text_map[key] = _execute_db_query(ph, getattr(cfg, 'db', None)) + + logger.info("文本映射: %d 个", len(text_map)) + _replace_texts_docx(doc, text_map) + + # 填充表格 + _progress("填充表格", 2, 5) + for token, spec in script_tables.items(): + _fill_script_table_docx(doc, token, spec) + + # 保存 + _progress("保存", 4, 5) + doc.save(str(output_path)) + _progress("完成", 5, 5) + logger.info("=== 报告生成完成 ===") + return output_path diff --git a/temperature_table_with_load_status.py b/temperature_table_with_load_status.py index bcbc588..51aa398 100644 --- a/temperature_table_with_load_status.py +++ b/temperature_table_with_load_status.py @@ -31,11 +31,57 @@ import logging import os import sys from datetime import datetime, timedelta +from pathlib import Path from typing import Any, Dict, List, Optional LOGGER = logging.getLogger(__name__) +# 详细日志记录器 - 用于记录每次查询的详细信息 +DETAIL_LOGGER = None + + +def _setup_detail_logger() -> logging.Logger: + """设置详细查询日志记录器,每次运行生成独立的日志文件""" + global DETAIL_LOGGER + + if DETAIL_LOGGER is not None: + return DETAIL_LOGGER + + # 生成带时间戳的日志文件名 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + log_dir = Path(__file__).parent / "influx_logs" + log_dir.mkdir(exist_ok=True) + log_file = log_dir / f"influx_data_{timestamp}.txt" + + DETAIL_LOGGER = logging.getLogger('influx_detail') + DETAIL_LOGGER.setLevel(logging.DEBUG) + + # 清除现有处理器 + for handler in DETAIL_LOGGER.handlers[:]: + DETAIL_LOGGER.removeHandler(handler) + + # 文件处理器 + file_handler = logging.FileHandler(log_file, encoding='utf-8', mode='w') + file_handler.setLevel(logging.DEBUG) + formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s') + file_handler.setFormatter(formatter) + DETAIL_LOGGER.addHandler(file_handler) + + DETAIL_LOGGER.info("=" * 80) + DETAIL_LOGGER.info("InfluxDB 详细查询日志") + DETAIL_LOGGER.info(f"日志文件: {log_file}") + DETAIL_LOGGER.info(f"生成时间: {datetime.now().isoformat()}") + DETAIL_LOGGER.info("=" * 80) + + return DETAIL_LOGGER + + +def _log_detail(message: str) -> None: + """记录详细信息到独立日志文件""" + logger = _setup_detail_logger() + logger.info(message) + def _mask_secret(value: Optional[str]) -> str: """掩码敏感信息""" @@ -73,6 +119,9 @@ def _setup_logging() -> None: LOGGER.info("日志文件已配置: %s", log_file) except Exception as e: LOGGER.warning("配置日志文件失败: %s", e) + + # 初始化详细日志记录器 + _setup_detail_logger() def _get_influx_config() -> Dict[str, str]: diff --git a/test_docx_fill.py b/test_docx_fill.py new file mode 100644 index 0000000..4e53ebe --- /dev/null +++ b/test_docx_fill.py @@ -0,0 +1,30 @@ +from docx import Document +from pathlib import Path + +# 打开模板 +template_path = Path(r"C:\PPRO\PCM_Report\configs\600泵\template.docx") +doc = Document(str(template_path)) + +print(f"文档中的表格数量: {len(doc.tables)}") + +# 查找 scriptTable1 +token = "scriptTable1" +found = False +for ti, table in enumerate(doc.tables): + print(f"\n表格 {ti}: {len(table.rows)} 行 x {len(table.rows[0].cells) if table.rows else 0} 列") + for ri, row in enumerate(table.rows): + for ci, cell in enumerate(row.cells): + if token in cell.text: + print(f" 找到 {token} 在行 {ri}, 列 {ci}") + print(f" 单元格文本: {cell.text[:50]}") + found = True + +if not found: + print(f"\n未找到 {token}") + print("\n检查所有单元格文本:") + for ti, table in enumerate(doc.tables): + for ri in range(min(3, len(table.rows))): + for ci in range(min(5, len(table.rows[ri].cells))): + text = table.rows[ri].cells[ci].text + if text.strip(): + print(f" 表{ti} 行{ri} 列{ci}: {text[:30]}") diff --git a/test_table_script_debug.py b/test_table_script_debug.py new file mode 100644 index 0000000..0b24625 --- /dev/null +++ b/test_table_script_debug.py @@ -0,0 +1,65 @@ +""" +测试table.py脚本的调试工具 +""" +import os +import sys +from pathlib import Path + +# 设置环境变量 +os.environ["TABLE_LOG_LEVEL"] = "DEBUG" +os.environ["TABLE_LOG_FILE"] = "table_debug.log" + +# 设置实验时间(示例) +os.environ["EXPERIMENT_START"] = "2026-03-13T15:24:08" +os.environ["EXPERIMENT_END"] = "2026-03-13T18:54:08" + +# 设置InfluxDB配置(需要根据实际情况修改) +os.environ["INFLUX_URL"] = os.environ.get("INFLUX_URL", "") +os.environ["INFLUX_ORG"] = os.environ.get("INFLUX_ORG", "") +os.environ["INFLUX_TOKEN"] = os.environ.get("INFLUX_TOKEN", "") +os.environ["INFLUX_BUCKET"] = os.environ.get("INFLUX_BUCKET", "PCM") +os.environ["INFLUX_MEASUREMENT"] = os.environ.get("INFLUX_MEASUREMENT", "PCM_Measurement") + +print("=== 测试table.py脚本 ===") +print(f"EXPERIMENT_START: {os.environ.get('EXPERIMENT_START')}") +print(f"EXPERIMENT_END: {os.environ.get('EXPERIMENT_END')}") +print(f"INFLUX_URL: {os.environ.get('INFLUX_URL', '<未设置>')}") +print() + +# 导入并执行脚本 +sys.path.insert(0, str(Path("configs/600泵"))) +from table import generate_table_data + +try: + result = generate_table_data(None) + print("\n=== 生成结果 ===") + print(f"表格数量: {len(result.get('tables', []))}") + + if result.get('tables'): + table = result['tables'][0] + cells = table.get('cells', []) + print(f"单元格数量: {len(cells)}") + + # 显示前10个单元格 + print("\n前10个单元格:") + for cell in cells[:10]: + print(f" row={cell['row']}, col={cell['col']}, value={cell.get('value', '')}") + + # 检查是否有温度数据 + temp_cells = [c for c in cells if c['row'] >= 4 and c.get('value')] + print(f"\n温度数据单元格数量: {len(temp_cells)}") + + # 检查时间戳 + time_cells = [c for c in cells if c['row'] == 1 and c['col'] in [1, 3]] + print(f"\n时间戳单元格: {time_cells}") + + # 检查环境温度 + env_temp = [c for c in cells if c['row'] == 0 and c['col'] == 1] + print(f"\n环境温度: {env_temp}") + +except Exception as e: + print(f"\n错误: {e}") + import traceback + traceback.print_exc() + +print("\n详细日志已保存到: table_debug.log") diff --git a/testdemo.py b/testdemo.py new file mode 100644 index 0000000..e69de29 diff --git a/ui_main.py b/ui_main.py index 6dca964..bb70ba5 100644 --- a/ui_main.py +++ b/ui_main.py @@ -1142,6 +1142,12 @@ class MainWindow(QMainWindow): self._timesync_timer.timeout.connect(self._on_timesync_tick) self._timesetter: SshTimeSetter | None = None QTimer.singleShot(0, self._maybe_start_time_sync) + + # Waiting state running time update timer + self._waiting_update_timer = QTimer(self) + self._waiting_update_timer.setSingleShot(False) + self._waiting_update_timer.setInterval(1000) # 每秒更新一次 + self._waiting_update_timer.timeout.connect(self._update_waiting_running_time) # Dashboard integration state self._pending_dashboard_range: Optional[Tuple[str, str]] = None @@ -1596,10 +1602,34 @@ class MainWindow(QMainWindow): if hasattr(self, '_experiment_detail_mode') and self._experiment_detail_mode: # 实验详情模式:保存到数据库 exp_id = self._experiment_detail_id - config_json = json.dumps(self.config.to_dict(), ensure_ascii=False, indent=2) + # 从数据库读取原有的工单信息 db = sqlite3.connect(str(APP_DIR / "experiments.db")) cur = db.cursor() + cur.execute( + "SELECT work_order_no, process_name, part_no, executor, start_ts FROM experiments WHERE id = ?", + (exp_id,) + ) + row = cur.fetchone() + + # 保留原有的工单信息到全局参数 + if row: + work_order_no, process_name, part_no, executor, start_ts = row + if work_order_no: + self.config.globalParameters.parameters['work_order_no'] = work_order_no + self.config.globalParameters.parameters['part_no'] = part_no or '' + self.config.globalParameters.parameters['executor'] = executor or '' + self.config.globalParameters.parameters['operator_name'] = executor or '' + self.config.globalParameters.parameters['process_name'] = process_name or '' + if start_ts: + from datetime import datetime + try: + dt = datetime.fromisoformat(start_ts) + self.config.globalParameters.parameters['runin_date'] = dt.strftime('%Y-%m-%d') + except: + pass + + config_json = json.dumps(self.config.to_dict(), ensure_ascii=False, indent=2) cur.execute( "UPDATE experiments SET config_json = ? WHERE id = ?", (config_json, exp_id) @@ -2499,12 +2529,37 @@ class MainWindow(QMainWindow): import json as json_module exp_id = self._experiment_detail_id + + # 从数据库读取原有的工单信息 + db = sqlite3.connect(str(APP_DIR / "experiments.db")) + cur = db.cursor() + cur.execute( + "SELECT work_order_no, process_name, part_no, executor, start_ts FROM experiments WHERE id = ?", + (exp_id,) + ) + row = cur.fetchone() + + # 保留原有的工单信息到全局参数 + if row: + work_order_no, process_name, part_no, executor, start_ts = row + if work_order_no: + self.config.globalParameters.parameters['work_order_no'] = work_order_no + self.config.globalParameters.parameters['part_no'] = part_no or '' + self.config.globalParameters.parameters['executor'] = executor or '' + self.config.globalParameters.parameters['operator_name'] = executor or '' + self.config.globalParameters.parameters['process_name'] = process_name or '' + if start_ts: + from datetime import datetime + try: + dt = datetime.fromisoformat(start_ts) + self.config.globalParameters.parameters['runin_date'] = dt.strftime('%Y-%m-%d') + except: + pass + config_json = json_module.dumps(self.config.to_dict(), ensure_ascii=False, indent=2) self.logger.info(f">>> 保存到数据库:实验ID = {exp_id}") - db = sqlite3.connect(str(APP_DIR / "experiments.db")) - cur = db.cursor() cur.execute( "UPDATE experiments SET config_json = ? WHERE id = ?", (config_json, exp_id) @@ -3828,10 +3883,14 @@ class MainWindow(QMainWindow): if not self.template_path: QMessageBox.warning(self, "生成报告", "请先加载 DOCX 模板") return - # Auto-generate to timestamped file without dialog + # Auto-generate to timestamped file in report/YYYYMMDD/ directory import datetime - ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - out = Path.cwd() / f"report_{ts}.docx" + now = datetime.datetime.now() + date_dir = now.strftime("%Y%m%d") + ts = now.strftime("%Y%m%d_%H%M%S") + report_dir = Path.cwd() / "report" / date_dir + report_dir.mkdir(parents=True, exist_ok=True) + out = report_dir / f"report_{ts}.docx" self.logger.info("Generate to %s", out) def task(): @@ -4896,7 +4955,7 @@ class MainWindow(QMainWindow): db = sqlite3.connect(str(APP_DIR / "experiments.db")) cur = db.cursor() cur.execute( - "SELECT config_json, start_ts, end_ts FROM experiments WHERE id=?", + "SELECT config_json, start_ts, end_ts, work_order_no, process_name, part_no, executor FROM experiments WHERE id=?", (exp_id,) ) row = cur.fetchone() @@ -4906,7 +4965,7 @@ class MainWindow(QMainWindow): QMessageBox.warning(self, "执行脚本", "未找到实验记录") return - cfg_json, start_ts, end_ts = row + cfg_json, start_ts, end_ts, work_order_no, process_name, part_no, executor = row if not end_ts: QMessageBox.warning(self, "保存数据", "实验尚未结束,无法保存数据") @@ -4920,6 +4979,23 @@ class MainWindow(QMainWindow): config = AppConfig.load(snap_path) + # 恢复工单信息到全局参数 + if work_order_no: + config.globalParameters.parameters['work_order_no'] = work_order_no + config.globalParameters.parameters['part_no'] = part_no or '' + config.globalParameters.parameters['executor'] = executor or '' + config.globalParameters.parameters['operator_name'] = executor or '' + config.globalParameters.parameters['process_name'] = process_name or '' + # 跑合日期使用实验开始时间的日期部分 + if start_ts: + from datetime import datetime + try: + dt = datetime.fromisoformat(start_ts) + config.globalParameters.parameters['runin_date'] = dt.strftime('%Y-%m-%d') + except: + pass + self.logger.info(f"[保存数据] 从数据库恢复工单信息: 工单号={work_order_no}, 零件号={part_no}, 执行人={executor}, 跑合日期={config.globalParameters.parameters.get('runin_date')}") + # 设置环境变量 normalized_start = self._normalize_dashboard_iso(start_ts) normalized_end = self._normalize_dashboard_iso(end_ts) @@ -4996,7 +5072,7 @@ class MainWindow(QMainWindow): db = sqlite3.connect(str(APP_DIR / "experiments.db")) cur = db.cursor() cur.execute( - "SELECT start_ts, end_ts, config_json, template_path FROM experiments WHERE id=?", + "SELECT start_ts, end_ts, config_json, template_path, work_order_no, process_name, part_no, executor FROM experiments WHERE id=?", (exp_id,) ) row = cur.fetchone(); db.close() @@ -5005,7 +5081,7 @@ class MainWindow(QMainWindow): if not row: QMessageBox.warning(self, "报告", "未找到记录") return - start_ts, end_ts, cfg_json, tpl = row + start_ts, end_ts, cfg_json, tpl, work_order_no, process_name, part_no, executor = row # build AppConfig from snapshot try: from tempfile import NamedTemporaryFile @@ -5013,14 +5089,22 @@ class MainWindow(QMainWindow): tf.write(cfg_json) snap_path = Path(tf.name) self.config = AppConfig.load(snap_path) + + # 配置快照中已包含工单信息,直接使用不覆盖 + self.logger.info(f"使用配置快照中的工单信息: 工单号={self.config.globalParameters.parameters.get('work_order_no')}, 零件号={self.config.globalParameters.parameters.get('part_no')}, 执行人={self.config.globalParameters.parameters.get('executor')}") except Exception as e: QMessageBox.warning(self, "报告", f"载入快照失败: {e}") return if tpl and Path(tpl).exists(): self.template_path = Path(tpl) - # generate to timestamped file + # generate to timestamped file in report/YYYYMMDD/ directory import datetime as _dt - out = Path.cwd() / f"report_exp_{exp_id}_{_dt.datetime.now().strftime('%Y%m%d_%H%M%S')}.docx" + now = _dt.datetime.now() + date_dir = now.strftime("%Y%m%d") + ts = now.strftime("%Y%m%d_%H%M%S") + report_dir = Path.cwd() / "report" / date_dir + report_dir.mkdir(parents=True, exist_ok=True) + out = report_dir / f"report_exp_{exp_id}_{ts}.docx" from report_generator import render_report normalized_start = self._normalize_dashboard_iso(start_ts) normalized_end = self._normalize_dashboard_iso(end_ts) if end_ts else "" @@ -5195,6 +5279,10 @@ class MainWindow(QMainWindow): # 启动实验状态监控器 self._start_experiment_monitor(work_order_no) + # 启动运行时间更新定时器 + self._waiting_update_timer.start() + self.logger.info("[等待状态] 运行时间更新定时器已启动") + # 先分闸,等待1秒,再合闸 self.logger.info("[等待状态] 开始分闸操作...") success_off = self._write_modbus_control_register(0x0000) # 0x0000 = 分闸 @@ -5219,6 +5307,37 @@ class MainWindow(QMainWindow): self.logger.info(f"[等待状态] 进入等待状态完成 - 工单号: {work_order_no}") + def _update_waiting_running_time(self) -> None: + """定时更新等待状态下的运行时间显示""" + if self._waiting_experiment_id is None: + return + + try: + # 从monitor获取running_time + running_time_str = "" + if self._experiment_monitor and hasattr(self._experiment_monitor, 'last_state'): + last_state = self._experiment_monitor.last_state + if isinstance(last_state, dict): + running_time = last_state.get('running_time') + if running_time: + try: + seconds = float(running_time) + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + secs = int(seconds % 60) + running_time_str = f" - 运行时间: {hours:02d}:{minutes:02d}:{secs:02d}" + except (ValueError, TypeError): + pass + + # 更新显示 + if running_time_str: + current_text = self.waiting_label.text() + if "工单号:" in current_text: + work_order_no = current_text.split("工单号: ")[-1].split(" - ")[0] + self.waiting_label.setText(f"⏳ 等待实验结束 - 工单号: {work_order_no}{running_time_str}") + except Exception as e: + self.logger.error(f"更新运行时间显示失败: {e}") + def _check_and_exit_waiting_state(self) -> None: """检查等待的实验是否已完成,如果完成则退出等待状态""" if self._waiting_experiment_id is None: @@ -5248,16 +5367,22 @@ class MainWindow(QMainWindow): work_order_no = self.waiting_label.text().split("工单号: ")[-1].split(" - ")[0] if "工单号:" in self.waiting_label.text() else "未知" # 从monitor获取running_time - running_time = None + running_time_str = "" if self._experiment_monitor and hasattr(self._experiment_monitor, 'last_state'): last_state = self._experiment_monitor.last_state if isinstance(last_state, dict): running_time = last_state.get('running_time') + if running_time: + try: + seconds = float(running_time) + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + secs = int(seconds % 60) + running_time_str = f" - 运行时间: {hours:02d}:{minutes:02d}:{secs:02d}" + except (ValueError, TypeError): + pass - if running_time: - self.waiting_label.setText(f"⏳ 等待实验结束 - 工单号: {work_order_no} - 运行时间: {running_time}") - else: - self.waiting_label.setText(f"⏳ 等待实验结束 - 工单号: {work_order_no}") + self.waiting_label.setText(f"⏳ 等待实验结束 - 工单号: {work_order_no}{running_time_str}") self.statusBar().showMessage("🔄 实验进行中,等待结束...", 3000) else: self.logger.info(f"[等待状态] ⏳ 实验 {self._waiting_experiment_id} 仍在等待开始") @@ -5269,6 +5394,10 @@ class MainWindow(QMainWindow): def _exit_waiting_state(self) -> None: """退出等待实验开始状态""" + # 停止运行时间更新定时器 + self._waiting_update_timer.stop() + self.logger.info("[等待状态] 运行时间更新定时器已停止") + # 停止监控器 if self._experiment_monitor is not None: try: