Farhan Beg commited on
Commit
2e4f994
·
1 Parent(s): 0f35c79

fix(permissions): chown /opt/hermes/.venv to hermes so auto-updates work

Browse files
Files changed (1) hide show
  1. Dockerfile +1 -190
Dockerfile CHANGED
@@ -1,190 +1 @@
1
- # HuggingMes + Hermes WebUI — merged deployment for Hugging Face Spaces
2
- # Base: NousResearch Hermes Agent (ships Hermes CLI, gateway, dashboard, Python venv)
3
-
4
- ARG HERMES_AGENT_VERSION=latest
5
- FROM nousresearch/hermes-agent:${HERMES_AGENT_VERSION}
6
-
7
- ARG WEBUI_REF=master
8
-
9
- USER root
10
-
11
- # System deps (mirrors HuggingMes) + git/nodejs for WebUI checkout + router
12
- RUN apt-get update && apt-get install -y --no-install-recommends \
13
- ca-certificates \
14
- curl \
15
- jq \
16
- git \
17
- python3 \
18
- nodejs \
19
- npm \
20
- chromium \
21
- libnss3 \
22
- libatk1.0-0 \
23
- libatk-bridge2.0-0 \
24
- libdrm2 \
25
- libgbm1 \
26
- libxcomposite1 \
27
- libxdamage1 \
28
- libxrandr2 \
29
- libxkbcommon0 \
30
- libx11-6 \
31
- libxext6 \
32
- libxfixes3 \
33
- libasound2 \
34
- fonts-dejavu-core \
35
- fonts-liberation \
36
- fonts-noto-color-emoji \
37
- && rm -rf /var/lib/apt/lists/* \
38
- && uv pip install --python /opt/hermes/.venv/bin/python --no-cache-dir \
39
- huggingface_hub hf_transfer pyyaml
40
-
41
- # Clone nesquena/hermes-webui (install deps into the agent venv so imports resolve)
42
- RUN git clone --depth 1 --branch ${WEBUI_REF} \
43
- https://github.com/nesquena/hermes-webui.git /opt/hermes-webui \
44
- && ( [ -f /opt/hermes-webui/requirements.txt ] \
45
- && /opt/hermes/.venv/bin/pip install --no-cache-dir -r /opt/hermes-webui/requirements.txt \
46
- || true ) \
47
- && chown -R hermes:hermes /opt/hermes-webui
48
-
49
- # HuggingMes-style integration scripts (vendored from somratpro/HuggingMes)
50
- COPY --chown=hermes:hermes start.sh /opt/huggingmes/start.sh
51
- COPY --chown=hermes:hermes health-server.js /opt/huggingmes/health-server.js
52
- COPY --chown=hermes:hermes hermes-sync.py /opt/huggingmes/hermes-sync.py
53
- COPY --chown=hermes:hermes cloudflare-proxy-setup.py /opt/huggingmes/cloudflare-proxy-setup.py
54
- COPY --chown=hermes:hermes cloudflare-keepalive-setup.py /opt/huggingmes/cloudflare-keepalive-setup.py
55
-
56
- RUN chmod +x \
57
- /opt/huggingmes/start.sh \
58
- /opt/huggingmes/hermes-sync.py \
59
- /opt/huggingmes/cloudflare-proxy-setup.py \
60
- /opt/huggingmes/cloudflare-keepalive-setup.py
61
-
62
- # Idempotent kanban migration patch (same workaround HuggingMes ships)
63
- RUN python3 - <<'PY'
64
- from pathlib import Path
65
- import sys
66
- p = Path("/opt/hermes/hermes_cli/kanban_db.py")
67
- if not p.exists():
68
- sys.exit(0)
69
- src = p.read_text(encoding="utf-8")
70
- sentinel = "# huggingmes-webui: idempotent-alter"
71
- if sentinel in src:
72
- sys.exit(0)
73
- old = (
74
- ' conn.execute(\n'
75
- ' "ALTER TABLE tasks ADD COLUMN consecutive_failures "\n'
76
- ' "INTEGER NOT NULL DEFAULT 0"\n'
77
- ' )'
78
- )
79
- new = (
80
- f' try: {sentinel}\n'
81
- ' conn.execute(\n'
82
- ' "ALTER TABLE tasks ADD COLUMN consecutive_failures "\n'
83
- ' "INTEGER NOT NULL DEFAULT 0"\n'
84
- ' )\n'
85
- ' except Exception:\n'
86
- ' pass'
87
- )
88
- if old in src:
89
- p.write_text(src.replace(old, new), encoding="utf-8")
90
- print("kanban patch: applied")
91
- PY
92
-
93
- # Quiet hermes-webui's per-request access log noise.
94
- # By default it prints `[webui] {"ts":...,"method":...}` for EVERY request,
95
- # which drowns the HF Logs tab once any browser tab is open polling
96
- # /api/dashboard/status, /api/health/agent, /api/sessions, /sw.js, etc.
97
- # Patch log_request() to drop 2xx responses for high-frequency poll paths.
98
- # Errors and chat/streaming paths still log normally.
99
- RUN python3 - <<'PY'
100
- from pathlib import Path
101
- import re
102
- import sys
103
-
104
- p = Path("/opt/hermes-webui/server.py")
105
- if not p.exists():
106
- sys.exit(0)
107
- src = p.read_text(encoding="utf-8")
108
- sentinel = "# huggingmes-webui: quiet-poll-paths"
109
- if sentinel in src:
110
- sys.exit(0)
111
-
112
- old = (
113
- " def log_request(self, code: str='-', size: str='-') -> None:\n"
114
- " \"\"\"Structured JSON logs for each request.\"\"\"\n"
115
- " import json as _json\n"
116
- " duration_ms = round((time.time() - getattr(self, '_req_t0', time.time())) * 1000, 1)\n"
117
- " record = _json.dumps({\n"
118
- " 'ts': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),\n"
119
- " 'method': self.command or '-',\n"
120
- " 'path': self.path or '-',\n"
121
- " 'status': int(code) if str(code).isdigit() else code,\n"
122
- " 'ms': duration_ms,\n"
123
- " })\n"
124
- " print(f'[webui] {record}', flush=True)"
125
- )
126
-
127
- new = (
128
- " _QUIET_POLL_PATHS = ( " + sentinel + "\n"
129
- " '/api/health/agent', '/api/dashboard/status',\n"
130
- " '/api/dashboard/config', '/api/sessions', '/api/profiles',\n"
131
- " '/api/profile/active', '/api/onboarding/status',\n"
132
- " '/api/insights', '/api/system/health',\n"
133
- " '/api/settings', '/api/projects', '/api/reasoning',\n"
134
- " '/api/models', '/api/chat/stream/status',\n"
135
- " '/api/git-info', '/sw.js', '/health',\n"
136
- " )\n"
137
- " _QUIET_PREFIXES = ('/static/', '/session/static/', '/assets/')\n"
138
- "\n"
139
- " def log_request(self, code: str='-', size: str='-') -> None:\n"
140
- " \"\"\"Structured JSON logs for each request, skipping noisy polls.\"\"\"\n"
141
- " # Always log non-2xx so 401/404/5xx remain visible.\n"
142
- " try:\n"
143
- " status_int = int(code) if str(code).isdigit() else 0\n"
144
- " except Exception:\n"
145
- " status_int = 0\n"
146
- " path = (self.path or '').split('?', 1)[0]\n"
147
- " if 200 <= status_int < 400:\n"
148
- " if path in self._QUIET_POLL_PATHS:\n"
149
- " return\n"
150
- " for pref in self._QUIET_PREFIXES:\n"
151
- " if path.startswith(pref):\n"
152
- " return\n"
153
- " import json as _json\n"
154
- " duration_ms = round((time.time() - getattr(self, '_req_t0', time.time())) * 1000, 1)\n"
155
- " record = _json.dumps({\n"
156
- " 'ts': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),\n"
157
- " 'method': self.command or '-',\n"
158
- " 'path': self.path or '-',\n"
159
- " 'status': int(code) if str(code).isdigit() else code,\n"
160
- " 'ms': duration_ms,\n"
161
- " })\n"
162
- " print(f'[webui] {record}', flush=True)"
163
- )
164
-
165
- if old in src:
166
- p.write_text(src.replace(old, new), encoding="utf-8")
167
- print("webui log-quiet patch: applied")
168
- else:
169
- print("webui log-quiet patch: pattern not found, skipping")
170
- PY
171
-
172
- # Keep hermes CLI on PATH for all shell types (login/interactive/non-interactive)
173
- RUN echo 'export PATH="/opt/hermes/.venv/bin:/opt/data/.local/bin:$PATH"' \
174
- > /etc/profile.d/hermes-venv.sh
175
-
176
- ENV HERMES_HOME=/opt/data \
177
- HUGGINGMES_APP_DIR=/opt/huggingmes \
178
- HERMES_WEBUI_REPO=/opt/hermes-webui \
179
- HERMES_AGENT_VERSION=${HERMES_AGENT_VERSION} \
180
- PYTHONUNBUFFERED=1 \
181
- HF_HUB_ENABLE_HF_TRANSFER=1 \
182
- PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
183
-
184
- EXPOSE 7861
185
-
186
- HEALTHCHECK --interval=30s --timeout=5s --start-period=120s \
187
- CMD curl -fsS http://localhost:7861/health || exit 1
188
-
189
- USER hermes
190
- ENTRYPOINT ["/opt/huggingmes/start.sh"]
 
1
+ IyBIdWdnaW5nTWVzICsgSGVybWVzIFdlYlVJIOKAlCBtZXJnZWQgZGVwbG95bWVudCBmb3IgSHVnZ2luZyBGYWNlIFNwYWNlcwojIEJhc2U6IE5vdXNSZXNlYXJjaCBIZXJtZXMgQWdlbnQgKHNoaXBzIEhlcm1lcyBDTEksIGdhdGV3YXksIGRhc2hib2FyZCwgUHl0aG9uIHZlbnYpCgpBUkcgSEVSTUVTX0FHRU5UX1ZFUlNJT049bGF0ZXN0CkZST00gbm91c3Jlc2VhcmNoL2hlcm1lcy1hZ2VudDoke0hFUk1FU19BR0VOVF9WRVJTSU9OfQoKUkcgV0VCVUlfUkVGPW1hc3RlcgoKVVNFUiByb290CgojIFN5c3RlbSBkZXBzIChtaXJyb3JzIEh1Z2dpbmdNZXMpICsgZ2l0L25vZGVqcyBmb3IgV2ViVUkgY2hlY2tvdXQgKyByb3V0ZXIKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSAtLW5vLWluc3RhbGwtcmVjb21tZW5kcwogICAgY2EtY2VydGlmaWNhdGVzIFwKICAgIGN1cmwgXAogICAganEgXAogICAgZ2l0IFwKICAgIHB5dGhvbjMgXAogICAgbm9kZWpzIFwKICAgIG5wbSBcCiAgICBjaHJvbWl1bSBcCiAgICBsaWJudHMzIFwKICAgIGxpYmF0azEuMC0wIFwKICAgIGxpYmF0ay1icmlkZ2UyLjAtMCBcCiAgICBsaWJkcm0yIFwKICAgIGxpYmdibTEgXAogICAgbGlieGNvbXBvc2l0ZTEgXAogICAgbGlieGRhbWFnZTEgXAogICAgbGlieHJhbmRyMiBcCiAgICBsaWJ4a2Jjb21tb24wIFwKICAgIGxpYngxMS02IFwKICAgIGxpYng2IFwKICAgIGxpYnhmaXhlczMgXAogICAgbGliYXNvdW5kMiBcCiAgICBmb250cy1kZWphdnUtY29yZSBcCiAgICBmb250cy1saWJlcmF0aW9uIFwKICAgIGZvbnRzLW5vdG8tY29sb3ItZW1vamkgXAogICAgJiYgcm0gLXJmIC92YXIvbGliL2FwdC9saXN0cy8qIFwKICAgICYmIHV2IHBpcCBpbnN0YWxsIC0tcHl0aG9uIC9vcHQvaGVybWVzLy52ZW52L2Jpbi9weXRob24gLS1uby1jYWNoZS1kaXIgXAogICAgICAgIGh1Z2dpbmdmYWNlX2h1YiBoZl90cmFuc2ZlciBweXlhbWwKCiMgQ2xvbmUgbmVzcXVlbmEvaGVybWVzLXdlYnVpIChpbnN0YWxsIGRlcHMgaW50byB0aGUgYWdlbnQgdmVudiBzbyBpbXBvcnRzIHJlc29sdmUpClJVTiBnaXQgY2xvbmUgLS1kZXB0aCAxIC0tYnJhbmNoICR7V0VCVUlfUkVGfQogICAgICAgIGh0dHBzOi8vZ2l0aHViLmNvbS9uZXNxdWVuYS9oZXJtZXMtd2VidWkuZ2l0IC9vcHQvaGVybWVzLXdlYnVpIFwKICYmICggWyAtZiAvb3B0L2hlcm1lcy13ZWJ1aS9yZXF1aXJlbWVudHMudHh0IF0KICAgICAgJiYgL29wdC9oZXJtZXMvLnZlbnYvYmluL3BpcCBpbnN0YWxsIC0tbm8tY2FjaGUtZGlyIC1yIC9vcHQvaGVybWVzLXdlYnVpL3JlcXVpcmVtZW50cy50eHQgXAogICAgICB8fCB0cnVlICkgXAogICYmIGNob3duIC1SIGhlcm1lczpoZXJtZXMgL29wdC9oZXJtZXMtd2VidWkKCiMgSHVnZ2luZ01lcy1zdHlsZSBpbnRlZ3JhdGlvbiBzY3JpcHRzICh2ZW5kb3JlZCBmcm9tIHNvbXJhdHByby9IdWdnaW5nTWVzKQpDT1BZIC0tY2hvd249aGVybWVzOmhlcm1lcyBzdGFydC5zaCAgICAgICAgICAgICAgICAgICAgICAgICAgIC9vcHQvaHVnZ2luZ21lcy9zdGFydC5zaApDT1BZIC0tY2hvd249aGVybWVzOmhlcm1lcyBoZWFsdGgtc2VydmVyLmpzICAgICAgICAgICAgICAgICAgICAvb3B0L2h1Z2dpbmdtZXMvaGVhbHRoLXNlcnZlci5qcwpDT1BZIC0tY2hvd249aGVybWVzOmhlcm1lcyBoZXJtZXMtc3luYy5weSAgICAgICAgICAgICAgICAgICAgIC9vcHQvaHVnZ2luZ21lcy9oZXJtZXMtc3luYy5weQpDT1BZIC0tY2hvd249aGVybWVzOmhlcm1lcyBjbG91ZGZsYXJlLXByb3h5LXNldHVwLnB5ICAgICAgL29wdC9odWdnaW5nbWVzL2Nsb3VkZmxhcmUtcHJveHktc2V0dXAucHkKQ09QWSAtLWNob3duPWhlcm1lczpoZXJtZXMgY2xvdWRmbGFyZS1rZWVwYWxpdmUtc2V0dXAucHkgIC9vcHQvaHVnZ2luZ21lcy9jbG91ZGZsYXJlLWtlZXBhbGl2ZS1zZXR1cC5weQoKUlVOIGNobW9kICt4IFwKICAgIC9vcHQvaHVnZ2luZ21lcy9zdGFydC5zaCBcCiAgICAvb3B0L2h1Z2dpbmdtZXMvaGVybWVzLXN5bmMucHkgXAogICAgL29wdC9odWdnaW5nbWVzL2Nsb3VkZmxhcmUtcHJveHktc2V0dXAucHkgXAogICAgL29wdC9odWdnaW5nbWVzL2Nsb3VkZmxhcmUta2VlcGFsaXZlLXNldHVwLnB5CgojIElkZW1wb3RlbnQga2FuYmFuIG1pZ3JhdGlvbiBwYXRjaCAoc2FtZSB3b3JrYXJvdW5kIEh1Z2dpbmdNZXMgc2hpcHMpClJVTiBweXRob24zIC0gPDwnUFknCmZyb20gcGF0aGxpYiBpbXBvcnQgUGF0aAppbXBvcnQgc3lzCnAgPSBQYXRoKCIvb3B0L2hlcm1lcy9oZXJtZXNfY2xpL2thbmJhbl9kYi5weSIpCmlmIG5vdCBwLmV4aXN0cygpOgogICAgc3lzLmV4aXQoMCkKc3JjID0gcC5yZWFkX3RleHQoZW5jb2Rpbmc9InV0Zi04IikKc2VudGluZWwgPSAiIyBodWdnaW5nbWVzLXdlYnVpOiBpZGVtcG90ZW50LWFsdGVyIgppZiBzZW50aW5lbCBpbiBzcmM6CiAgICBzeXMuZXhpdCgwKQpvbGQgPSAoCiAgICAnICAgIGNvbm4uZXhlY3V0ZShcbicKICAgICcgICAgICAgICJBTFRFUiBUQUJMRSB0YXNrcyBBREQgQ09MVU1OIGNvbnNlY3V0aXZlX2ZhaWx1cmVzICInCiAgICAnICAgICAgICAiSU5URUdFUiBOT1QgTlVMTCBERUZBVUxUIDAiJwogICAgJyAgICApJwopCm5ldyA9ICgKICAgIGYnICAgIHRyeTogIHtzZW50aW5lbH1cbicKICAgICcgICAgICAgIGNvbm4uZXhlY3V0ZShcbicKICAgICcgICAgICAgICAgICAiQUxURVIgVEFCTEUgdGFza3MgQUREIENPTFVNTiBjb25zZWN1dGl2ZV9mYWlsdXJlcyAiJwogICAgJyAgICAgICAgICAgICJJTlRFR0VSIE5PVCBOVUxMIERFRkFVTFQgMCInCiAgICAnICAgICAgICApXG4nCiAgICAnICAgIGV4Y2VwdCBFeGNlcHRpb246XG4nCiAgICAnICAgICAgICBwYXNzJwppZiBvbGQgaW4gc3JjOgogICAgcC53cml0ZV90ZXh0KHNyYy5yZXBsYWNlKG9sZCwgbmV3KSwgZW5jb2Rpbmc9InV0Zi04IikKICAgIHByaW50KCJrYW5iYW4gcGF0Y2g6IGFwcGxpZWQiKwpQWQoKIyBRdWlldCBoZXJtZXMtd2VidWkncyBwZXItcmVxdWVzdCBhY2Nlc3MgbG9nIG5vaXNlLgojIEJ5IGRlZmF1bHQgaXQgcHJpbnRzIGBbd2VidWldIHsidHMiOi4uLiJtZXRob2QiOi4uLn1gIGZvciBFVkVSWSByZXF1ZXN0LAojIHdoaWNoIGRyb3ducyB0aGUgSEYgTG9ncyB0YWIgb25jZSBhbnkgYnJvd3NlciB0YWIgaXMgb3BlbiBwb2xsaW5nCiMgL2FwaS9kYXNoYm9hcmQvc3RhdHVzLCAvYXBpL2hlYWx0aC9hZ2VudCwgL2FwaS9zZXNzaW9ucywgL3N3LmpzLCBldGMuCiMgUGF0Y2ggbG9nX3JlcXVlc3QoKSB0byBkcm9wIDJ4eCByZXNwb25zZXMgZm9yIGhpZ2gtZnJlcXVlbmN5IHBvbGwgcGF0aHMuCiMgRXJyb3JzIGFuZCBjaGF0L3N0cmVhbWluZyBwYXRocyBzdGlsbCBsb2cgbm9ybWFsbHkuClJVTiBweXRob24zIC0gPDwnUFknCmZyb20gcGF0aGxpYiBpbXBvcnQgUGF0aAppbXBvcnQgcmUKaW1wb3J0IHN5cwoKcCA9IFBhdGgoIi9vcHQvaGVybWVzLXdlYnVpL3NlcnZlci5weSIpCmlmIG5vdCBwLmV4aXN0cygpOgogICAgc3lzLmV4aXQoMCkKc3JjID0gcC5yZWFkX3RleHQoZW5jb2Rpbmc9InV0Zi04IikKc2VudGluZWwgPSAiIyBodWdnaW5nbWVzLXdlYnVpOiBxdWlldC1wb2xsLXBhdGhzIgppZiBzZW50aW5lbCBpbiBzcmM6CiAgICBzeXMuZXhpdCgwKQoKb2xkID0gKAogICAgIiAgICBkZWYgbG9nX3JlcXVlc3Qoc2VsZiwgY29kZTogc3RyPSctJywgc2l6ZTogc3RyPSctJykgLT4gTm9uZzpcbiIKICAgICIgICAgICAgIFwiXCJcIlN0cnVjdHVyZWQgSlNPTiBsb2dzIGZvciBlYWNoIHJlcXVlc3QuXCJcIlwiXG4iCiAgICAiICAgICAgICBpbXBvcnQganNvbiBhcyBfanNvblxuIgogICAgIiAgICAgICAgZHVyYXRpb25fbXMgPSByb3VuZCgodGltZS50aW1lKCkgLSBnZXRhdHRyKHNlbGYsICdfcmVxX3QwJywgdGltZS50aW1lKCkpKSAqIDEwMDAsIDEpXG4iCiAgICAiICAgICAgICByZWNvcmQgPSBfanNvbi5kdW1wcyh7XG4iCiAgICAiICAgICAgICAgICAgJ3RzJzogdGltZS5zdHJmdGltZSgnJVktJW0tJWRUJUglbTolJVN6JywgdGltZS5nbXRpbWUoKSksXG4iCiAgICAiICAgICAgICAgICAgJ21ldGhvZCc6IHNlbGYuY29tbWFuZCBvciAnLScsXG4iCiAgICAiICAgICAgICAgICAgJ3BhdGgnOiBzZWxmLnBhdGggb3IgJy0nLFxuIgogICAgIiAgICAgICAgICAgICdzdGF0dXMnOiBpbnQoY29kZSkgaWYgc3RyKGNvZGUpLmlzZGlnaXQoKSBlbHNlIGNvZGUsXG4iCiAgICAiICAgICAgICAgICAgJ21zJzogZHVyYXRpb25fbXMsXG4iCiAgICAiICAgICAgICB9KVxuIgogICAgIiAgICAgICAgcHJpbnQoZidbd2VidWldIHtyZWNvcmR9JywgZmx1c2g9VHJ1ZSkiCQoKbmV3ID0gKAogICAgIiAgICBfUVVJRVRfUE9MTF9QQVRIUyA9ICggICIgKyBzZW50aW5lbCArICJcbiIKICAgICIgICAgICAgICcvYXBpL2hlYWx0aC9hZ2VudCcsICcvYXBpL2Rhc2hib2FyZC9zdGF0dXMnLFxuIgogICAgIiAgICAgICAgJy9hcGkvZGFzaGJvYXJkL2NvbmZpZycsICcvYXBpL3Nlc3Npb25zJywgJy9hcGkvcHJvZmlsZXMnLFxuIgogICAgIiAgICAgICAgJy9hcGkvcHJvZmlsZS9hY3RpdmUnLCAnL2FwaS9vbmJvYXJkaW5nL3N0YXR1cycsXG4iCiAgICAiICAgICAgICAnL2FwaS9pbnNpZ2h0cycsICcvYXBpL3N5c3RlbS9oZWFsdGgnLFxuIgogICAgIiAgICAgICAgJy9hcGkvc2V0dGluZ3MnLCAnL2FwaS9wcm9qZWN0cycsICcvYXBpL3JlYXNvbmluZycsXG4iCiAgICAiICAgICAgICAnL2FwaS9tb2RlbHMnLCAnL2FwaS9jaGF0L3N0cmVhbS9zdGF0dXMnLFxuIgogICAgIiAgICAgICAgJy9hcGkvZ2l0LWluZm8nLCAnL3N3LmpzJywgJy9oZWFsdGgnLFxuIgogICAgIiAgICApXG4iCiAgICAiICAgIF9RVUlFVF9QUkVGSVhFUyA9ICgnL3N0YXRpYy8nLCAnL3Nlc3Npb24vc3RhdGljLycsICcvYXNzZXRzLycpXG4iCiAgICAiXG4iCiAgICAiICAgIGRlZiBsb2dfcmVxdWVzdChzZWxmLCBjb2RlOiBzdHI9Jy0nLCBzaXplOiBzdHI9Jy0nKSAtPiBOb25lOlxuIgogICAgIiAgICAgICAgXCJcIlwiU3RydWN0dXJlZCBKU09OIGxvZ3MgZm9yIGVhY2ggcmVxdWVzdCwgc2tpcHBpbmcgbm9pc3kgcG9sbHMuXCJcIlwiXG4iCiAgICAiICAgICAgICAjIEFsd2F5cyBsb2cgbm9uLTJ4eCBzbyA0MDEvNDA0LzV4eCByZW1haW4gdmlzaWJsZS5cbiIKICAgICIgICAgICAgIHRyeTpcbiIKICAgICIgICAgICAgICAgICBzdGF0dXNfaW50ID0gaW50KGNvZGUpIGlmIHN0cihjb2RlKS5pc2RpZ2l0KCkgZWxzZSAwXG4iCiAgICAiICAgICAgICBleGNlcHQgRXhjZXB0aW9uOlxuIgogICAgIiAgICAgICAgICAgIHN0YXR1c19pbnQgPSAwXG4iCiAgICAiICAgICAgICBwYXRoID0gKHNlbGYucGF0aCBvciAnJykuc3BsaXQoJz8nLCAxKVswXVxuIgogICAgIiAgICAgICAgaWYgMjAwIDw9IHN0YXR1c19pbnQgPCA0MDA6XG4iCiAgICAiICAgICAgICAgICAgaWYgcGF0aCBpbiBzZWxmLl9RVUlFVF9QT0xMX1BBVEhTOlxuIgogICAgIiAgICAgICAgICAgICAgICByZXR1cm5cbiIKICAgICIgICAgICAgICAgICBmb3IgcHJlZiBpbiBzZWxmLl9RVUlFVF9QUkVGSVhFUzpcbiIKICAgICIgICAgICAgICAgICAgICAgaWYgcGF0aC5zdGFydHN3aXRoKHByZWYpOlxuIgogICAgIiAgICAgICAgICAgICAgICAgICAgcmV0dXJuXG4iCiAgICAiICAgICAgICBpbXBvcnQganNvbiBhcyBfanNvblxuIgogICAgIiAgICAgICAgZHVyYXRpb25fbXMgPSByb3VuZCgodGltZS50aW1lKCkgLSBnZXRhdHRyKHNlbGYsICdfcmVxX3QwJywgdGltZS50aW1lKCkpKSAqIDEwMDAsIDEpXG4iCiAgICAiICAgICAgICByZWNvcmQgPSBfanNvbi5kdW1wcyh7XG4iCiAgICAiICAgICAgICAgICAgJ3RzJzogdGltZS5zdHJmdGltZSgnJVktJW0tJWRUJUglbTolJVN6JywgdGltZS5nbXRpbWUoKSksXG4iCiAgICAiICAgICAgICAgICAgJ21ldGhvZCc6IHNlbGYuY29tbWFuZCBvciAnLScsXG4iCiAgICAiICAgICAgICAgICAgJ3BhdGgnOiBzZWxmLnBhdGggb3IgJy0nLFxuIgogICAgIiAgICAgICAgICAgICdzdGF0dXMnOiBpbnQoY29kZSkgaWYgc3RyKGNvZGUpLmlzZGlnaXQoKSBlbHNlIGNvZGUsXG4iCiAgICAiICAgICAgICAgICAgJ21zJzogZHVyYXRpb25fbXMsXG4iCiAgICAiICAgICAgICB9KVxuIgogICAgIiAgICAgICAgcHJpbnQoZidbd2VidWldIHtyZWNvcmR9JywgZmx1c2g9VHJ1ZSkiCQoKaWYgb2xkIGluIHNyYzoKICAgIHAud3JpdGVfdGV4dChzcmMucmVwbGFjZShvbGQsIG5ldyksIGVuY29kaW5nPSJ1dGYtOCIpCiAgICBwcmludCgid2VidWkgbG9nLXF1aWV0IHBhdGNoOiBhcHBsaWVkIikKZWxzZToKICAgIHByaW50KCJ3ZWJ1aSBsb2ctcXVpZXQgcGF0Y2g6IHBhdHRlcm4gbm90IGZvdW5kLCBza2lwcGluZyIpClBZCgojIEZpeCBwZXJtaXNzaW9ucyBzbyBoZXJtZXMgdXNlciBjYW4gc2VsZi11cGRhdGUgcGFja2FnZXMKUlVOIGNob3duIC1SIGhlcm1lczpoZXJtZXMgL29wdC9oZXJtZXMvLnZlbnYKCiMgS2VlcCBoZXJtZXMgQ0xJIG9uIFBBVEggZm9yIGFsbCBzaGVsbCB0eXBlcyAobG9naW4vaW50ZXJhY3RpdmUvbm9uLWludGVyYWN0aXZlKQpSVU4gZWNobyAnZXhwb3J0IFBBVEg9Ii9vcHQvaGVybWVzLy52ZW52L2Jpbjovb3B0L2RhdGEvLmxvY2FsL2JpbjokUEFUSCInIFwKICAgID4gL2V0Yy9wcm9maWxlLmQvaGVybWVzLXZlbnYuc2gKCiMgQ3JlYXRlIGRpcmVjdG9yeSBmb3Igc2tpbiBzeW5jaW5nIApSVU4gbWtkaXIgLXAgJHtIRVJNRVNfSE9NRX0vc2tpbnMKClJVTiBjaG93biAtUiBoZXJtZXM6aGVybWVzICR7SEVSTUVTX0hPTUV9L3NraW5zCgpFTlYgSEVSTUVTX0hPTUU9L29wdC9kYXRhIFwKICAgIEhVR0dJTkdNRVNfQVBQX0RJUj0vb3B0L2h1Z2dpbmdtZXMgXAogICAgSEVSTUVTX1dFQlVJX1JFUE89L29wdC9oZXJtZXMtd2VidWkgXAogICAgSEVSTUVTX0FHRU5UX1ZFUlNJT049JHtIRVJNRVNfQUdFTlRfVkVSU0lPTn0gXAogICAgUFlUSE9OVU5CVUZGRVJFVD0xIFwKICAgIEhGX0hVQl9FTkFCTEVfSEZfVFJBTlNGRVI9MSBcCiAgICBQTEFZV1JJR0hUX0NIUk9NSVVNX0VYRUNVVEFCTEVfUEFUSD0vdXNyL2Jpbi9jaHJvbWl1bQpFWFBPU0UgNzg2MQpIRUFMVEhDSEVDSyAtLWludGVydmFsPTM0cyAtLXRpbWVvdXQ9NXMgLS1zdGFydC1wZXJpb2Q9MTI2cyBcClJVTiBjdXJsIC1mc1MgaHR0cDovL2xvY2FsaG9zdDo3ODYxL2hlYWx0aCB8fCBleGl0IDEKCiMgQ3JlYXRlIGRpcmVjdG9yeSBmb3Igc2tpbiBzeW5jaW5nIFUKUlVOIG1rZGlyIC1wICR7SEVSTUVTX0hPTUV9L3NraW5zIApSVU4gY2hvd24gLVIgaGVybWVzOmhlcm1lcyAke0hFUk1FU19IT01FfS9za2lucyA=