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 0000000..9f3e0a2 Binary files /dev/null and b/experiments.db1 differ diff --git a/report_generator.py b/report_generator.py index 401cd0e..50b44cb 100644 --- a/report_generator.py +++ b/report_generator.py @@ -1,1394 +1,384 @@ from __future__ import annotations - -import os -import tempfile +import os, json, subprocess, sys from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Tuple - +from typing import Any, Callable, Dict, List, Optional import pandas as pd -import pythoncom -import win32com.client as win32 - +from docx import Document +from docx.enum.text import WD_ALIGN_PARAGRAPH from config_model import AppConfig, PlaceholderConfig, DbConnectionConfig from influx_service import InfluxConnectionParams, InfluxService from logger import get_logger -import sys -from pathlib import Path logger = get_logger() +_PROGRESS_CB: Optional[Callable[[str, int, int], None]] = None -# 添加专门的报告生成日志文件 -def _setup_report_debug_logger(): - """设置报告生成专用的调试日志""" - import logging - - # 获取可执行文件路径 - if getattr(sys, 'frozen', False): - # 打包后的可执行文件 - exe_dir = Path(sys.executable).parent - else: - # 开发环境 - exe_dir = Path(__file__).parent - - # 创建报告生成专用日志文件 - report_log_file = exe_dir / "report_generation_debug.log" - - # 创建专用logger - report_logger = logging.getLogger('report_debug') - report_logger.setLevel(logging.DEBUG) - - # 清除现有处理器 - for handler in report_logger.handlers[:]: - report_logger.removeHandler(handler) - - # 文件处理器 - file_handler = logging.FileHandler(report_log_file, encoding='utf-8', mode='w') - file_handler.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s') - file_handler.setFormatter(formatter) - report_logger.addHandler(file_handler) - - # 控制台处理器 - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - console_handler.setFormatter(formatter) - report_logger.addHandler(console_handler) - - report_logger.info("=== 报告生成调试日志开始 ===") - report_logger.info("日志文件位置: %s", report_log_file) - report_logger.info("可执行文件目录: %s", exe_dir) - - return report_logger +def set_progress_callback(cb): + global _PROGRESS_CB; _PROGRESS_CB = cb +def _progress(msg, cur, total): + if _PROGRESS_CB: _PROGRESS_CB(msg, cur, total) -# 初始化报告调试日志 -report_debug_logger = _setup_report_debug_logger() +def _build_influx_service(cfg): + return InfluxService(InfluxConnectionParams(url=cfg.influx.url, org=cfg.influx.org, token=cfg.influx.token)) -# Progress callback (msg, current, total) -_PROGRESs_CB: Optional[Callable[[str, int, int], None]] = None - - -def set_progress_callback(cb: Optional[Callable[[str, int, int], None]]) -> 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: