From dde46f10e1392195656e9db731be0e7025744154 Mon Sep 17 00:00:00 2001 From: flfeders Date: Thu, 24 Sep 2026 17:06:52 +0200 Subject: [PATCH 1/5] fix(scan): generate valid systemd working directory --- agents/fedeo-device-agent/scripts/install-linux.sh | 4 +++- agents/fedeo-device-agent/tests/installer.test.mjs | 8 +++++++- frontend/assets/device-agent/bundle.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/agents/fedeo-device-agent/scripts/install-linux.sh b/agents/fedeo-device-agent/scripts/install-linux.sh index 17e1bce4..10f80445 100644 --- a/agents/fedeo-device-agent/scripts/install-linux.sh +++ b/agents/fedeo-device-agent/scripts/install-linux.sh @@ -81,7 +81,9 @@ def unit_quote(value): unit_path.write_text('\n'.join([ '[Unit]', 'Description=FEDEO Scan-Agent', 'After=network-online.target', '', '[Service]', 'Type=simple', - 'WorkingDirectory=' + unit_quote(agent_dir), + # WorkingDirectory takes a literal path, unlike Environment/ExecStart. + # Quotes would become part of the path; only systemd specifiers need escaping. + 'WorkingDirectory=' + str(agent_dir).replace('%', '%%'), 'Environment=' + unit_quote('FEDEO_AGENT_ENV=' + str(config_path)), 'ExecStart=' + unit_quote(node) + ' ' + unit_quote(agent_dir / 'dist/main.js'), 'Restart=on-failure', 'RestartSec=5', 'UMask=0077', '', diff --git a/agents/fedeo-device-agent/tests/installer.test.mjs b/agents/fedeo-device-agent/tests/installer.test.mjs index 2d8e0c46..425dd626 100644 --- a/agents/fedeo-device-agent/tests/installer.test.mjs +++ b/agents/fedeo-device-agent/tests/installer.test.mjs @@ -14,6 +14,8 @@ const agentId = 'b48c0e33-1abd-4b11-89fa-1bb7b39a3057' const token = 'fedeo_agent_' + 'a'.repeat(64) const options = { agentId, token, backendUrl: 'https://fedeo.example.com/backend', autostart: true } const execute = promisify(execFile) +const hasSystemdAnalyze = process.platform === 'linux' && await execute('systemd-analyze', ['--version']) + .then(() => true, error => { if (error.code === 'ENOENT') return false; throw error }) test('backend URLs preserve reverse-proxy paths and reject credentials or non-HTTP protocols', () => { assert.equal(agentBackendUrl('/backend/', 'https://fedeo.example.com'), options.backendUrl) @@ -70,9 +72,13 @@ for (const start of [false, true]) { for (const [file, encoded] of Object.entries(bundle.files)) { assert.equal((await readFile(path.join(agentDir, file))).toString('base64'), encoded) } - const unit = await readFile(path.join(env.XDG_CONFIG_HOME, 'systemd/user', 'fedeo-device-agent-' + agentId + '.service'), 'utf8') + const unitPath = path.join(env.XDG_CONFIG_HOME, 'systemd/user', 'fedeo-device-agent-' + agentId + '.service') + const unit = await readFile(unitPath, 'utf8') assert.ok(unit.includes('data with spaces %%')) assert.ok(!unit.includes(token)) + await t.test('systemd accepts the generated service, including a working directory with spaces and percent signs', { skip: !hasSystemdAnalyze }, async () => { + await execute('systemd-analyze', ['--user', 'verify', unitPath]) + }) const calls = await readFile(env.FEDEO_TEST_SYSTEMD_LOG, 'utf8') assert.equal(calls.includes('--user enable fedeo-device-agent-'), start) assert.equal(calls.includes('--user restart fedeo-device-agent-'), start) diff --git a/frontend/assets/device-agent/bundle.json b/frontend/assets/device-agent/bundle.json index 6a699e06..c3b71491 100644 --- a/frontend/assets/device-agent/bundle.json +++ b/frontend/assets/device-agent/bundle.json @@ -1,6 +1,6 @@ { "version": "0.1.0", - "installerTemplate": "#!/usr/bin/env bash\n# Generated by the FEDEO setup wizard. This file contains the agent's access token.\nset -euo pipefail\numask 077\n\nif [[ \"$(uname -s)\" != Linux ]]; then\n echo 'Diese Installation ist für Linux vorgesehen.' >&2\n exit 1\nfi\nif [[ \"$(id -u)\" == 0 ]]; then\n echo 'Bitte dieses Skript als normaler Benutzer ohne sudo starten.' >&2\n exit 1\nfi\nif [[ \"${1:-}\" != '' && \"${1:-}\" != --no-start ]]; then\n echo 'Aufruf: bash install.sh [--no-start]' >&2\n exit 1\nfi\n\nFEDEO_SETUP_ID='@@AGENT_ID@@'\nFEDEO_SETUP_AUTOSTART='@@AUTOSTART@@'\nFEDEO_SETUP_DIR=\"${XDG_DATA_HOME:-$HOME/.local/share}/fedeo-device-agent/$FEDEO_SETUP_ID\"\nFEDEO_SETUP_CONFIG=\"${XDG_CONFIG_HOME:-$HOME/.config}/fedeo-device-agent/$FEDEO_SETUP_ID.env\"\nFEDEO_SETUP_WORK=\"${XDG_STATE_HOME:-$HOME/.local/state}/fedeo-device-agent/$FEDEO_SETUP_ID\"\nFEDEO_SETUP_UNIT=\"fedeo-device-agent-$FEDEO_SETUP_ID.service\"\nFEDEO_SETUP_UNITS=\"${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user\"\n\necho 'FEDEO Scan-Agent wird für diesen Benutzer eingerichtet.'\nif ! command -v node >/dev/null || ! command -v scanimage >/dev/null || ! python3 -c 'import PIL' >/dev/null 2>&1; then\n if ! command -v apt-get >/dev/null; then\n echo 'Bitte Node.js >= 20, SANE (scanimage), Python 3 und Pillow installieren und das Skript erneut starten.' >&2\n exit 1\n fi\n echo 'Fehlende Systempakete werden über apt installiert (sudo benötigt).'\n sudo apt-get update\n sudo apt-get install -y nodejs sane-utils python3-pil\nfi\nnode -e 'if (Number(process.versions.node.split(\".\")[0]) < 20) { console.error(\"Node.js >= 20 wird benötigt. Bitte Node.js aktualisieren und die Installation erneut starten.\"); process.exit(1) }'\npython3 -c 'import PIL'\n\nFEDEO_SETUP_NODE=\"$(command -v node)\"\nFEDEO_SETUP_HAS_SYSTEMD=false\nif command -v systemctl >/dev/null && systemctl --user show-environment >/dev/null 2>&1; then\n FEDEO_SETUP_HAS_SYSTEMD=true\nfi\nif [[ \"$FEDEO_SETUP_AUTOSTART\" == true && \"$FEDEO_SETUP_HAS_SYSTEMD\" != true ]]; then\n echo 'Der Benutzerdienst von systemd ist nicht erreichbar. Bitte in der Desktop-Sitzung starten oder den Autostart im Assistenten ausschalten.' >&2\n exit 1\nfi\n\nmkdir -p \"$FEDEO_SETUP_DIR\" \"$(dirname \"$FEDEO_SETUP_CONFIG\")\" \"$FEDEO_SETUP_WORK\" \"$FEDEO_SETUP_UNITS\"\n# Stop only the matching installation before replacing its files.\nif [[ \"$FEDEO_SETUP_HAS_SYSTEMD\" == true && \"${1:-}\" != --no-start ]]; then\n systemctl --user stop \"$FEDEO_SETUP_UNIT\" 2>/dev/null || true\nfi\n\npython3 - \"$FEDEO_SETUP_DIR\" \"$FEDEO_SETUP_CONFIG\" \"$FEDEO_SETUP_WORK\" \"$FEDEO_SETUP_NODE\" \"$FEDEO_SETUP_UNITS/$FEDEO_SETUP_UNIT\" <<'FEDEO_PYTHON'\nimport base64\nimport json\nimport os\nfrom pathlib import Path\nimport sys\n\nagent_dir, config_path, work_dir, node, unit_path = map(Path, sys.argv[1:])\nfiles = json.loads(base64.b64decode('@@PAYLOAD_BASE64@@'))\nfor name, encoded in files.items():\n relative = Path(name)\n if relative.is_absolute() or '..' in relative.parts:\n raise ValueError('Ungültiger Dateiname im Installationspaket')\n target = agent_dir / relative\n target.parent.mkdir(parents=True, exist_ok=True)\n target.write_bytes(base64.b64decode(encoded, validate=True))\n\nconfig = base64.b64decode('@@CONFIG_BASE64@@').decode('utf8')\nconfig += 'FEDEO_WORK_DIR=' + str(work_dir) + '\\n'\nconfig_path.write_text(config, encoding='utf8')\nconfig_path.chmod(0o600)\n\ndef unit_quote(value):\n return '\"' + str(value).replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"').replace('%', '%%') + '\"'\n\nunit_path.write_text('\\n'.join([\n '[Unit]', 'Description=FEDEO Scan-Agent', 'After=network-online.target', '',\n '[Service]', 'Type=simple',\n 'WorkingDirectory=' + unit_quote(agent_dir),\n 'Environment=' + unit_quote('FEDEO_AGENT_ENV=' + str(config_path)),\n 'ExecStart=' + unit_quote(node) + ' ' + unit_quote(agent_dir / 'dist/main.js'),\n 'Restart=on-failure', 'RestartSec=5', 'UMask=0077', '',\n '[Install]', 'WantedBy=default.target', '',\n]), encoding='utf8')\nFEDEO_PYTHON\n\necho \"Agent installiert: $FEDEO_SETUP_DIR\"\necho \"Konfiguration: $FEDEO_SETUP_CONFIG\"\nif [[ \"${1:-}\" == --no-start ]]; then\n echo 'Installation abgeschlossen; Agent wurde nicht gestartet.'\nelif [[ \"$FEDEO_SETUP_AUTOSTART\" == true ]]; then\n systemctl --user daemon-reload\n systemctl --user enable \"$FEDEO_SETUP_UNIT\"\n systemctl --user restart \"$FEDEO_SETUP_UNIT\"\n echo 'Agent gestartet. Er startet künftig bei der Anmeldung.'\n printf 'Protokoll: journalctl --user -u %q -f\\n' \"$FEDEO_SETUP_UNIT\"\n echo 'Jetzt im FEDEO-Assistenten die Verbindung prüfen. Die heruntergeladene Installationsdatei kann anschließend gelöscht werden.'\nelse\n if [[ \"$FEDEO_SETUP_HAS_SYSTEMD\" == true ]]; then\n systemctl --user disable \"$FEDEO_SETUP_UNIT\" 2>/dev/null || true\n systemctl --user daemon-reload\n fi\n echo 'Agent startet im Terminal. Dieses Fenster geöffnet lassen; Beenden mit Strg+C.'\n cd \"$FEDEO_SETUP_DIR\"\n FEDEO_AGENT_ENV=\"$FEDEO_SETUP_CONFIG\" \"$FEDEO_SETUP_NODE\" dist/main.js\nfi\n", + "installerTemplate": "#!/usr/bin/env bash\n# Generated by the FEDEO setup wizard. This file contains the agent's access token.\nset -euo pipefail\numask 077\n\nif [[ \"$(uname -s)\" != Linux ]]; then\n echo 'Diese Installation ist für Linux vorgesehen.' >&2\n exit 1\nfi\nif [[ \"$(id -u)\" == 0 ]]; then\n echo 'Bitte dieses Skript als normaler Benutzer ohne sudo starten.' >&2\n exit 1\nfi\nif [[ \"${1:-}\" != '' && \"${1:-}\" != --no-start ]]; then\n echo 'Aufruf: bash install.sh [--no-start]' >&2\n exit 1\nfi\n\nFEDEO_SETUP_ID='@@AGENT_ID@@'\nFEDEO_SETUP_AUTOSTART='@@AUTOSTART@@'\nFEDEO_SETUP_DIR=\"${XDG_DATA_HOME:-$HOME/.local/share}/fedeo-device-agent/$FEDEO_SETUP_ID\"\nFEDEO_SETUP_CONFIG=\"${XDG_CONFIG_HOME:-$HOME/.config}/fedeo-device-agent/$FEDEO_SETUP_ID.env\"\nFEDEO_SETUP_WORK=\"${XDG_STATE_HOME:-$HOME/.local/state}/fedeo-device-agent/$FEDEO_SETUP_ID\"\nFEDEO_SETUP_UNIT=\"fedeo-device-agent-$FEDEO_SETUP_ID.service\"\nFEDEO_SETUP_UNITS=\"${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user\"\n\necho 'FEDEO Scan-Agent wird für diesen Benutzer eingerichtet.'\nif ! command -v node >/dev/null || ! command -v scanimage >/dev/null || ! python3 -c 'import PIL' >/dev/null 2>&1; then\n if ! command -v apt-get >/dev/null; then\n echo 'Bitte Node.js >= 20, SANE (scanimage), Python 3 und Pillow installieren und das Skript erneut starten.' >&2\n exit 1\n fi\n echo 'Fehlende Systempakete werden über apt installiert (sudo benötigt).'\n sudo apt-get update\n sudo apt-get install -y nodejs sane-utils python3-pil\nfi\nnode -e 'if (Number(process.versions.node.split(\".\")[0]) < 20) { console.error(\"Node.js >= 20 wird benötigt. Bitte Node.js aktualisieren und die Installation erneut starten.\"); process.exit(1) }'\npython3 -c 'import PIL'\n\nFEDEO_SETUP_NODE=\"$(command -v node)\"\nFEDEO_SETUP_HAS_SYSTEMD=false\nif command -v systemctl >/dev/null && systemctl --user show-environment >/dev/null 2>&1; then\n FEDEO_SETUP_HAS_SYSTEMD=true\nfi\nif [[ \"$FEDEO_SETUP_AUTOSTART\" == true && \"$FEDEO_SETUP_HAS_SYSTEMD\" != true ]]; then\n echo 'Der Benutzerdienst von systemd ist nicht erreichbar. Bitte in der Desktop-Sitzung starten oder den Autostart im Assistenten ausschalten.' >&2\n exit 1\nfi\n\nmkdir -p \"$FEDEO_SETUP_DIR\" \"$(dirname \"$FEDEO_SETUP_CONFIG\")\" \"$FEDEO_SETUP_WORK\" \"$FEDEO_SETUP_UNITS\"\n# Stop only the matching installation before replacing its files.\nif [[ \"$FEDEO_SETUP_HAS_SYSTEMD\" == true && \"${1:-}\" != --no-start ]]; then\n systemctl --user stop \"$FEDEO_SETUP_UNIT\" 2>/dev/null || true\nfi\n\npython3 - \"$FEDEO_SETUP_DIR\" \"$FEDEO_SETUP_CONFIG\" \"$FEDEO_SETUP_WORK\" \"$FEDEO_SETUP_NODE\" \"$FEDEO_SETUP_UNITS/$FEDEO_SETUP_UNIT\" <<'FEDEO_PYTHON'\nimport base64\nimport json\nimport os\nfrom pathlib import Path\nimport sys\n\nagent_dir, config_path, work_dir, node, unit_path = map(Path, sys.argv[1:])\nfiles = json.loads(base64.b64decode('@@PAYLOAD_BASE64@@'))\nfor name, encoded in files.items():\n relative = Path(name)\n if relative.is_absolute() or '..' in relative.parts:\n raise ValueError('Ungültiger Dateiname im Installationspaket')\n target = agent_dir / relative\n target.parent.mkdir(parents=True, exist_ok=True)\n target.write_bytes(base64.b64decode(encoded, validate=True))\n\nconfig = base64.b64decode('@@CONFIG_BASE64@@').decode('utf8')\nconfig += 'FEDEO_WORK_DIR=' + str(work_dir) + '\\n'\nconfig_path.write_text(config, encoding='utf8')\nconfig_path.chmod(0o600)\n\ndef unit_quote(value):\n return '\"' + str(value).replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"').replace('%', '%%') + '\"'\n\nunit_path.write_text('\\n'.join([\n '[Unit]', 'Description=FEDEO Scan-Agent', 'After=network-online.target', '',\n '[Service]', 'Type=simple',\n # WorkingDirectory takes a literal path, unlike Environment/ExecStart.\n # Quotes would become part of the path; only systemd specifiers need escaping.\n 'WorkingDirectory=' + str(agent_dir).replace('%', '%%'),\n 'Environment=' + unit_quote('FEDEO_AGENT_ENV=' + str(config_path)),\n 'ExecStart=' + unit_quote(node) + ' ' + unit_quote(agent_dir / 'dist/main.js'),\n 'Restart=on-failure', 'RestartSec=5', 'UMask=0077', '',\n '[Install]', 'WantedBy=default.target', '',\n]), encoding='utf8')\nFEDEO_PYTHON\n\necho \"Agent installiert: $FEDEO_SETUP_DIR\"\necho \"Konfiguration: $FEDEO_SETUP_CONFIG\"\nif [[ \"${1:-}\" == --no-start ]]; then\n echo 'Installation abgeschlossen; Agent wurde nicht gestartet.'\nelif [[ \"$FEDEO_SETUP_AUTOSTART\" == true ]]; then\n systemctl --user daemon-reload\n systemctl --user enable \"$FEDEO_SETUP_UNIT\"\n systemctl --user restart \"$FEDEO_SETUP_UNIT\"\n echo 'Agent gestartet. Er startet künftig bei der Anmeldung.'\n printf 'Protokoll: journalctl --user -u %q -f\\n' \"$FEDEO_SETUP_UNIT\"\n echo 'Jetzt im FEDEO-Assistenten die Verbindung prüfen. Die heruntergeladene Installationsdatei kann anschließend gelöscht werden.'\nelse\n if [[ \"$FEDEO_SETUP_HAS_SYSTEMD\" == true ]]; then\n systemctl --user disable \"$FEDEO_SETUP_UNIT\" 2>/dev/null || true\n systemctl --user daemon-reload\n fi\n echo 'Agent startet im Terminal. Dieses Fenster geöffnet lassen; Beenden mit Strg+C.'\n cd \"$FEDEO_SETUP_DIR\"\n FEDEO_AGENT_ENV=\"$FEDEO_SETUP_CONFIG\" \"$FEDEO_SETUP_NODE\" dist/main.js\nfi\n", "files": { "dist/api.js": "aW1wb3J0IHsgcmVhZEZpbGUsIHN0YXQgfSBmcm9tICJub2RlOmZzL3Byb21pc2VzIjsKaW1wb3J0IHsgYmFzZW5hbWUgfSBmcm9tICJub2RlOnBhdGgiOwpleHBvcnQgY2xhc3MgRmVkZW9BcGkgewogICAgY29uZmlnOwogICAgY29uc3RydWN0b3IoY29uZmlnKSB7CiAgICAgICAgdGhpcy5jb25maWcgPSBjb25maWc7CiAgICB9CiAgICB1cmwocGF0aCkgewogICAgICAgIHJldHVybiBgJHt0aGlzLmNvbmZpZy5mZWRlb1VybH0ke3BhdGh9YDsKICAgIH0KICAgIGhlYWRlcnMoZXh0cmEpIHsKICAgICAgICByZXR1cm4gewogICAgICAgICAgICAiWC1BZ2VudC1Ub2tlbiI6IHRoaXMuY29uZmlnLmFnZW50VG9rZW4sCiAgICAgICAgICAgIC4uLmV4dHJhLAogICAgICAgIH07CiAgICB9CiAgICBhc3luYyByZXF1ZXN0KHBhdGgsIGluaXQgPSB7fSwgdGltZW91dE1zID0gMzBfMDAwKSB7CiAgICAgICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmZXRjaCh0aGlzLnVybChwYXRoKSwgewogICAgICAgICAgICAuLi5pbml0LAogICAgICAgICAgICBoZWFkZXJzOiB0aGlzLmhlYWRlcnMoaW5pdC5oZWFkZXJzKSwKICAgICAgICAgICAgc2lnbmFsOiBBYm9ydFNpZ25hbC50aW1lb3V0KHRpbWVvdXRNcyksCiAgICAgICAgfSk7CiAgICAgICAgaWYgKCFyZXNwb25zZS5vaykgewogICAgICAgICAgICBjb25zdCBib2R5ID0gYXdhaXQgcmVzcG9uc2UudGV4dCgpLmNhdGNoKCgpID0+ICIiKTsKICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGAke2luaXQubWV0aG9kIHx8ICJHRVQifSAke3BhdGh9IGZlaGxnZXNjaGxhZ2VuOiAke3Jlc3BvbnNlLnN0YXR1c30gJHtib2R5fWApOwogICAgICAgIH0KICAgICAgICByZXR1cm4gYXdhaXQgcmVzcG9uc2UuanNvbigpOwogICAgfQogICAgaGVhcnRiZWF0KHBheWxvYWQpIHsKICAgICAgICByZXR1cm4gdGhpcy5yZXF1ZXN0KCIvaW5zdGFuY2UtYWdlbnQvaGVhcnRiZWF0IiwgewogICAgICAgICAgICBtZXRob2Q6ICJQT1NUIiwKICAgICAgICAgICAgaGVhZGVyczogeyAiQ29udGVudC1UeXBlIjogImFwcGxpY2F0aW9uL2pzb24iIH0sCiAgICAgICAgICAgIGJvZHk6IEpTT04uc3RyaW5naWZ5KHBheWxvYWQpLAogICAgICAgIH0pOwogICAgfQogICAgbmV4dFNjYW5Kb2IoKSB7CiAgICAgICAgcmV0dXJuIHRoaXMucmVxdWVzdCgiL2luc3RhbmNlLWFnZW50L3NjYW4tam9icy9uZXh0Iik7CiAgICB9CiAgICB1cGRhdGVTY2FuSm9iU3RhdHVzKGpvYklkLCBzdGF0dXMsIG1lc3NhZ2UpIHsKICAgICAgICByZXR1cm4gdGhpcy5yZXF1ZXN0KGAvaW5zdGFuY2UtYWdlbnQvc2Nhbi1qb2JzLyR7am9iSWR9L3N0YXR1c2AsIHsKICAgICAgICAgICAgbWV0aG9kOiAiUE9TVCIsCiAgICAgICAgICAgIGhlYWRlcnM6IHsgIkNvbnRlbnQtVHlwZSI6ICJhcHBsaWNhdGlvbi9qc29uIiB9LAogICAgICAgICAgICBib2R5OiBKU09OLnN0cmluZ2lmeSh7IHN0YXR1cywgbWVzc2FnZSB9KSwKICAgICAgICB9KTsKICAgIH0KICAgIGFzeW5jIHVwbG9hZFNjYW4oam9iSWQsIHJlc3VsdCkgewogICAgICAgIGlmICgoYXdhaXQgc3RhdChyZXN1bHQucGF0aCkpLnNpemUgPiAxMDAgKiAxMDI0ICogMTAyNCkgewogICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkRpZSBTY2FuLURhdGVpIMO8YmVyc2NocmVpdGV0IGRhcyBVcGxvYWQtTGltaXQgdm9uIDEwMCBNaUIuIEJpdHRlIGRlbiBTdGFwZWwgYXVmdGVpbGVuIG9kZXIgZGllIEF1ZmzDtnN1bmcgcmVkdXppZXJlbi4iKTsKICAgICAgICB9CiAgICAgICAgY29uc3QgZm9ybSA9IG5ldyBGb3JtRGF0YSgpOwogICAgICAgIGNvbnN0IGZpbGVCdWZmZXIgPSBhd2FpdCByZWFkRmlsZShyZXN1bHQucGF0aCk7CiAgICAgICAgY29uc3QgZmlsZSA9IG5ldyBGaWxlKFtmaWxlQnVmZmVyXSwgcmVzdWx0LmZpbGVuYW1lIHx8IGJhc2VuYW1lKHJlc3VsdC5wYXRoKSwgewogICAgICAgICAgICB0eXBlOiByZXN1bHQubWltZVR5cGUsCiAgICAgICAgfSk7CiAgICAgICAgZm9ybS5hcHBlbmQoImZpbGUiLCBmaWxlKTsKICAgICAgICByZXR1cm4gdGhpcy5yZXF1ZXN0KGAvaW5zdGFuY2UtYWdlbnQvc2Nhbi1qb2JzLyR7am9iSWR9L3VwbG9hZGAsIHsKICAgICAgICAgICAgbWV0aG9kOiAiUE9TVCIsCiAgICAgICAgICAgIGJvZHk6IGZvcm0sCiAgICAgICAgfSwgMTIwXzAwMCk7CiAgICB9Cn0K", "dist/commands.js": "aW1wb3J0IHsgc3Bhd24gfSBmcm9tICJub2RlOmNoaWxkX3Byb2Nlc3MiOwpleHBvcnQgY29uc3QgY29tbWFuZEV4aXN0cyA9IChjb21tYW5kKSA9PiBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4gewogICAgY29uc3QgY2hpbGQgPSBzcGF3bigic2giLCBbIi1jIiwgJ2NvbW1hbmQgLXYgIiQxIiA+L2Rldi9udWxsIDI+JjEnLCAiLS0iLCBjb21tYW5kXSk7CiAgICBjaGlsZC5vbigiZXJyb3IiLCAoKSA9PiByZXNvbHZlKGZhbHNlKSk7CiAgICBjaGlsZC5vbigiY2xvc2UiLCAoY29kZSkgPT4gcmVzb2x2ZShjb2RlID09PSAwKSk7Cn0pOwpleHBvcnQgY29uc3QgcnVuQ29tbWFuZCA9IChjb21tYW5kLCBhcmdzLCBvcHRpb25zID0ge30pID0+IG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHsKICAgIGNvbnN0IGNoaWxkID0gc3Bhd24oY29tbWFuZCwgYXJncywgewogICAgICAgIHN0ZGlvOiBbImlnbm9yZSIsICJwaXBlIiwgInBpcGUiXSwKICAgICAgICBjd2Q6IG9wdGlvbnMuY3dkLAogICAgfSk7CiAgICBjb25zdCBzdGRvdXQgPSBbXTsKICAgIGNvbnN0IHN0ZGVyciA9IFtdOwogICAgbGV0IHRpbWVkT3V0ID0gZmFsc2U7CiAgICBsZXQga2lsbFRpbWVvdXQ7CiAgICBjb25zdCB0aW1lb3V0ID0gb3B0aW9ucy50aW1lb3V0TXMKICAgICAgICA/IHNldFRpbWVvdXQoKCkgPT4gewogICAgICAgICAgICB0aW1lZE91dCA9IHRydWU7CiAgICAgICAgICAgIGNoaWxkLmtpbGwoIlNJR1RFUk0iKTsKICAgICAgICAgICAga2lsbFRpbWVvdXQgPSBzZXRUaW1lb3V0KCgpID0+IGNoaWxkLmtpbGwoIlNJR0tJTEwiKSwgMjAwMCk7CiAgICAgICAgfSwgb3B0aW9ucy50aW1lb3V0TXMpCiAgICAgICAgOiBudWxsOwogICAgY2hpbGQuc3Rkb3V0Lm9uKCJkYXRhIiwgKGNodW5rKSA9PiBzdGRvdXQucHVzaChCdWZmZXIuZnJvbShjaHVuaykpKTsKICAgIGNoaWxkLnN0ZGVyci5vbigiZGF0YSIsIChjaHVuaykgPT4gc3RkZXJyLnB1c2goQnVmZmVyLmZyb20oY2h1bmspKSk7CiAgICBjaGlsZC5vbigiZXJyb3IiLCAoZXJyb3IpID0+IHsKICAgICAgICBpZiAodGltZW91dCkKICAgICAgICAgICAgY2xlYXJUaW1lb3V0KHRpbWVvdXQpOwogICAgICAgIGlmIChraWxsVGltZW91dCkKICAgICAgICAgICAgY2xlYXJUaW1lb3V0KGtpbGxUaW1lb3V0KTsKICAgICAgICByZWplY3QoZXJyb3IpOwogICAgfSk7CiAgICBjaGlsZC5vbigiY2xvc2UiLCAoY29kZSwgc2lnbmFsKSA9PiB7CiAgICAgICAgaWYgKHRpbWVvdXQpCiAgICAgICAgICAgIGNsZWFyVGltZW91dCh0aW1lb3V0KTsKICAgICAgICBpZiAoa2lsbFRpbWVvdXQpCiAgICAgICAgICAgIGNsZWFyVGltZW91dChraWxsVGltZW91dCk7CiAgICAgICAgaWYgKHRpbWVkT3V0KSB7CiAgICAgICAgICAgIHJlamVjdChuZXcgRXJyb3IoYCR7Y29tbWFuZH0gd3VyZGUgbmFjaCAke29wdGlvbnMudGltZW91dE1zfSBtcyBiZWVuZGV0YCkpOwogICAgICAgICAgICByZXR1cm47CiAgICAgICAgfQogICAgICAgIGlmIChzaWduYWwpIHsKICAgICAgICAgICAgcmVqZWN0KG5ldyBFcnJvcihgJHtjb21tYW5kfSB3dXJkZSBkdXJjaCAke3NpZ25hbH0gYmVlbmRldGApKTsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgIH0KICAgICAgICByZXNvbHZlKHsKICAgICAgICAgICAgc3Rkb3V0OiBCdWZmZXIuY29uY2F0KHN0ZG91dCkudG9TdHJpbmcoInV0ZjgiKSwKICAgICAgICAgICAgc3RkZXJyOiBCdWZmZXIuY29uY2F0KHN0ZGVycikudG9TdHJpbmcoInV0ZjgiKSwKICAgICAgICAgICAgY29kZTogY29kZSA/PyAxLAogICAgICAgIH0pOwogICAgfSk7Cn0pOwo=", -- 2.49.1 From b9cfdbe29e5800d31591aaeec410b5e57055b78a Mon Sep 17 00:00:00 2001 From: flfeders Date: Thu, 24 Sep 2026 17:09:35 +0200 Subject: [PATCH 2/5] fix(scan): allow slower SANE network discovery --- agents/fedeo-device-agent/src/scan/sane.ts | 4 +++- agents/fedeo-device-agent/tests/scan.test.ts | 21 ++++++++++++++++++-- frontend/assets/device-agent/bundle.json | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/agents/fedeo-device-agent/src/scan/sane.ts b/agents/fedeo-device-agent/src/scan/sane.ts index c65f5f73..33b7d9f7 100644 --- a/agents/fedeo-device-agent/src/scan/sane.ts +++ b/agents/fedeo-device-agent/src/scan/sane.ts @@ -20,7 +20,9 @@ export const hasSane = () => commandExists("scanimage") export const listScanners = async () => { if (!await hasSane()) return [] - const result = await runCommand("scanimage", ["-L"], { timeoutMs: 10_000 }) + // SANE also probes network backends; these can take more than ten seconds + // even when the USB scanner is already available. + const result = await runCommand("scanimage", ["-L"], { timeoutMs: 30_000 }) if (result.code !== 0) return [] return result.stdout.split(/\r?\n/) .map((line) => line.trim().match(/^device `([^']+)'/)?.[1]) diff --git a/agents/fedeo-device-agent/tests/scan.test.ts b/agents/fedeo-device-agent/tests/scan.test.ts index d2fd8182..232cbcb1 100644 --- a/agents/fedeo-device-agent/tests/scan.test.ts +++ b/agents/fedeo-device-agent/tests/scan.test.ts @@ -3,16 +3,21 @@ import test, { TestContext } from "node:test" import os from "node:os" import path from "node:path" import { chmod, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises" -import { runScan } from "../src/scan/sane.js" +import { listScanners, runScan } from "../src/scan/sane.js" import { commandExists, runCommand } from "../src/commands.js" import { AgentConfig, ScanJob } from "../src/types.js" const fakeScanner = `#!/usr/bin/env python3 -import json, os, sys +import json, os, sys, time from pathlib import Path from PIL import Image args = sys.argv[1:] Path(os.environ['SCAN_TEST_CALL']).write_text(json.dumps(args)) +if args == ['-L']: + time.sleep(float(os.environ.get('SCAN_TEST_DISCOVERY_DELAY', '0'))) + print("device " + chr(96) + "fujitsu:ScanSnap iX1600:test' is a FUJITSU scanner") + print("device " + chr(96) + "airscan:test' is a network scanner") + sys.exit(int(os.environ.get('SCAN_TEST_EXIT', '0'))) batch = next((arg.split('=', 1)[1] for arg in args if arg.startswith('--batch=')), None) single = args[args.index('--output-file') + 1] if '--output-file' in args else None count = int(os.environ.get('SCAN_TEST_PAGES', '4')) if batch else 1 @@ -52,6 +57,18 @@ const setup = async (t: TestContext) => { return { config, job, dir, args: async () => JSON.parse(await readFile(process.env.SCAN_TEST_CALL!, "utf8")) as string[] } } +test("scanner discovery waits for network probes beyond the former ten-second limit", async (t) => { + await setup(t) + process.env.SCAN_TEST_DISCOVERY_DELAY = "10.2" + assert.deepEqual(await listScanners(), ["fujitsu:ScanSnap iX1600:test", "airscan:test"]) +}) + +test("failed scanner discovery does not advertise devices from an unsuccessful command", async (t) => { + await setup(t) + process.env.SCAN_TEST_EXIT = "1" + assert.deepEqual(await listScanners(), []) +}) + test("duplex batch saves all pages as PDF, accepts NO_DOCS and passes A4 geometry", async (t) => { const { config, job, args } = await setup(t) process.env.SCAN_TEST_EMPTY_FINAL = "1" diff --git a/frontend/assets/device-agent/bundle.json b/frontend/assets/device-agent/bundle.json index c3b71491..84a2b6ed 100644 --- a/frontend/assets/device-agent/bundle.json +++ b/frontend/assets/device-agent/bundle.json @@ -10,7 +10,7 @@ "dist/main.js": "IyEvdXNyL2Jpbi9lbnYgbm9kZQppbXBvcnQgb3MgZnJvbSAibm9kZTpvcyI7CmltcG9ydCBwYXRoIGZyb20gIm5vZGU6cGF0aCI7CmltcG9ydCB7IHJtIH0gZnJvbSAibm9kZTpmcy9wcm9taXNlcyI7CmltcG9ydCB7IEZlZGVvQXBpIH0gZnJvbSAiLi9hcGkuanMiOwppbXBvcnQgeyBsb2FkQ29uZmlnIH0gZnJvbSAiLi9jb25maWcuanMiOwppbXBvcnQgeyBsb2cgfSBmcm9tICIuL2xvZ2dlci5qcyI7CmltcG9ydCB7IGxpc3RQcmludGVycyB9IGZyb20gIi4vcHJpbnQvY3Vwcy5qcyI7CmltcG9ydCB7IGhhc1NhbmUsIGxpc3RTY2FubmVycywgcnVuU2NhbiB9IGZyb20gIi4vc2Nhbi9zYW5lLmpzIjsKY29uc3Qgc2xlZXAgPSAobXMpID0+IG5ldyBQcm9taXNlKChyZXNvbHZlKSA9PiBzZXRUaW1lb3V0KHJlc29sdmUsIG1zKSk7CmNvbnN0IHN0cmluZ2lmeUVycm9yID0gKGVycm9yKSA9PiB7CiAgICBpZiAoZXJyb3IgaW5zdGFuY2VvZiBFcnJvcikKICAgICAgICByZXR1cm4gZXJyb3IubWVzc2FnZTsKICAgIHJldHVybiBTdHJpbmcoZXJyb3IpOwp9Owpjb25zdCBtYWluID0gYXN5bmMgKCkgPT4gewogICAgY29uc3QgY29uZmlnID0gbG9hZENvbmZpZygpOwogICAgY29uc3QgYXBpID0gbmV3IEZlZGVvQXBpKGNvbmZpZyk7CiAgICBsb2cuaW5mbygiRkVERU8gR2Vyw6R0ZS1BZ2VudCBzdGFydGV0IiwgewogICAgICAgIHBsYXRmb3JtOiBwcm9jZXNzLnBsYXRmb3JtLAogICAgICAgIHdvcmtEaXI6IGNvbmZpZy53b3JrRGlyLAogICAgICAgIHBvbGxTZWNvbmRzOiBjb25maWcucG9sbFNlY29uZHMsCiAgICB9KTsKICAgIHdoaWxlICh0cnVlKSB7CiAgICAgICAgdHJ5IHsKICAgICAgICAgICAgY29uc3Qgc2Nhbm5lck5hbWVzID0gYXdhaXQgbGlzdFNjYW5uZXJzKCkuY2F0Y2goKGVycm9yKSA9PiB7CiAgICAgICAgICAgICAgICBsb2cud2FybigiU2Nhbm5lci1Fcmtlbm51bmcgZmVobGdlc2NobGFnZW4iLCBzdHJpbmdpZnlFcnJvcihlcnJvcikpOwogICAgICAgICAgICAgICAgcmV0dXJuIFtdOwogICAgICAgICAgICB9KTsKICAgICAgICAgICAgY29uc3QgcHJpbnRlck5hbWVzID0gYXdhaXQgbGlzdFByaW50ZXJzKCkuY2F0Y2goKCkgPT4gW10pOwogICAgICAgICAgICBjb25zdCBzY2FuQXZhaWxhYmxlID0gYXdhaXQgaGFzU2FuZSgpOwogICAgICAgICAgICBjb25zdCBoZWFydGJlYXRQYXlsb2FkID0gewogICAgICAgICAgICAgICAgY2FwYWJpbGl0aWVzOiB7CiAgICAgICAgICAgICAgICAgICAgc2Nhbjogc2NhbkF2YWlsYWJsZSwKICAgICAgICAgICAgICAgICAgICBwcmludDogcHJpbnRlck5hbWVzLmxlbmd0aCA+IDAsCiAgICAgICAgICAgICAgICAgICAgcGxhdGZvcm06IHByb2Nlc3MucGxhdGZvcm0sCiAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgc2Nhbm5lck5hbWVzLAogICAgICAgICAgICAgICAgcHJpbnRlck5hbWVzLAogICAgICAgICAgICAgICAgZGVidWdJbmZvOiB7CiAgICAgICAgICAgICAgICAgICAgaG9zdG5hbWU6IG9zLmhvc3RuYW1lKCksCiAgICAgICAgICAgICAgICAgICAgcmVsZWFzZTogb3MucmVsZWFzZSgpLAogICAgICAgICAgICAgICAgICAgIGFyY2g6IG9zLmFyY2goKSwKICAgICAgICAgICAgICAgICAgICBub2RlOiBwcm9jZXNzLnZlcnNpb24sCiAgICAgICAgICAgICAgICAgICAgdXB0aW1lU2Vjb25kczogTWF0aC5yb3VuZChvcy51cHRpbWUoKSksCiAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICB9OwogICAgICAgICAgICBjb25zdCBoZWFydGJlYXQgPSBhd2FpdCBhcGkuaGVhcnRiZWF0KGhlYXJ0YmVhdFBheWxvYWQpOwogICAgICAgICAgICBpZiAoaGVhcnRiZWF0LnBlbmRpbmdTY2FuSm9icyA+IDApIHsKICAgICAgICAgICAgICAgIGxvZy5pbmZvKGAke2hlYXJ0YmVhdC5wZW5kaW5nU2NhbkpvYnN9IFNjYW4tQXVmdHJhZy9BdWZ0csOkZ2Ugd2FydGVuYCk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgY29uc3QgbmV4dCA9IGF3YWl0IGFwaS5uZXh0U2NhbkpvYigpOwogICAgICAgICAgICBpZiAoIW5leHQuam9iKSB7CiAgICAgICAgICAgICAgICBhd2FpdCBzbGVlcChjb25maWcucG9sbFNlY29uZHMgKiAxMDAwKTsKICAgICAgICAgICAgICAgIGNvbnRpbnVlOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGxvZy5pbmZvKCJTY2FuLUF1ZnRyYWcgd2lyZCBhdXNnZWbDvGhydCIsIHsKICAgICAgICAgICAgICAgIGpvYklkOiBuZXh0LmpvYi5pZCwKICAgICAgICAgICAgICAgIHRlbmFudElkOiBuZXh0LmpvYi50ZW5hbnRJZCwKICAgICAgICAgICAgICAgIHNjYW5uZXJOYW1lOiBuZXh0LmpvYi5zY2FubmVyTmFtZSB8fCBjb25maWcuc2Nhbm5lck5hbWUgfHwgImRlZmF1bHQiLAogICAgICAgICAgICB9KTsKICAgICAgICAgICAgLy8gS2VlcCBvbmxpbmUgc3RhdHVzIGZyZXNoIGR1cmluZyBsb25nIHNjYW5zIHdpdGhvdXQgcHJvYmluZyBhbiBvY2N1cGllZCBVU0Igc2Nhbm5lci4KICAgICAgICAgICAgbGV0IGhlYXJ0YmVhdFJ1bm5pbmcgPSBmYWxzZTsKICAgICAgICAgICAgY29uc3QgaGVhcnRiZWF0VGltZXIgPSBzZXRJbnRlcnZhbChhc3luYyAoKSA9PiB7CiAgICAgICAgICAgICAgICBpZiAoaGVhcnRiZWF0UnVubmluZykKICAgICAgICAgICAgICAgICAgICByZXR1cm47CiAgICAgICAgICAgICAgICBoZWFydGJlYXRSdW5uaW5nID0gdHJ1ZTsKICAgICAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICAgICAgYXdhaXQgYXBpLmhlYXJ0YmVhdChoZWFydGJlYXRQYXlsb2FkKTsKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIGNhdGNoIChlcnJvcikgewogICAgICAgICAgICAgICAgICAgIGxvZy53YXJuKCJIZWFydGJlYXQgd8OkaHJlbmQgU2NhbiBmZWhsZ2VzY2hsYWdlbiIsIHN0cmluZ2lmeUVycm9yKGVycm9yKSk7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBmaW5hbGx5IHsKICAgICAgICAgICAgICAgICAgICBoZWFydGJlYXRSdW5uaW5nID0gZmFsc2U7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0sIDMwXzAwMCk7CiAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICBhd2FpdCBhcGkudXBkYXRlU2NhbkpvYlN0YXR1cyhuZXh0LmpvYi5pZCwgInJ1bm5pbmciKTsKICAgICAgICAgICAgICAgIGNvbnN0IGpvYklkID0gbmV4dC5qb2IuaWQ7CiAgICAgICAgICAgICAgICBjb25zdCByZXBvcnRQcm9ncmVzcyA9IGFzeW5jIChtZXNzYWdlKSA9PiB7CiAgICAgICAgICAgICAgICAgICAgYXdhaXQgYXBpLnVwZGF0ZVNjYW5Kb2JTdGF0dXMoam9iSWQsICJydW5uaW5nIiwgbWVzc2FnZSkuY2F0Y2goKGVycm9yKSA9PiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGxvZy53YXJuKCJTY2FuLVN0YXR1cyBrb25udGUgbmljaHQgw7xiZXJ0cmFnZW4gd2VyZGVuIiwgc3RyaW5naWZ5RXJyb3IoZXJyb3IpKTsKICAgICAgICAgICAgICAgICAgICB9KTsKICAgICAgICAgICAgICAgIH07CiAgICAgICAgICAgICAgICBjb25zdCBzY2FuUmVzdWx0ID0gYXdhaXQgcnVuU2Nhbihjb25maWcsIG5leHQuam9iLCByZXBvcnRQcm9ncmVzcyk7CiAgICAgICAgICAgICAgICBhd2FpdCByZXBvcnRQcm9ncmVzcyhgJHtzY2FuUmVzdWx0LnBhZ2VDb3VudH0gU2VpdGVuIGdlc2Nhbm50LiAke3NjYW5SZXN1bHQud2FybmluZ3Muam9pbigiICIpfWAudHJpbSgpKTsKICAgICAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICAgICAgYXdhaXQgYXBpLnVwbG9hZFNjYW4oam9iSWQsIHNjYW5SZXN1bHQpOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGAke3N0cmluZ2lmeUVycm9yKGVycm9yKX0gU2Nhbi1EYXRlaSBibGVpYnQgenVyIFdpZWRlcmhlcnN0ZWxsdW5nIHVudGVyICR7c2NhblJlc3VsdC5wYXRofS5gKTsKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIGF3YWl0IHJtKHBhdGguZGlybmFtZShzY2FuUmVzdWx0LnBhdGgpLCB7IHJlY3Vyc2l2ZTogdHJ1ZSwgZm9yY2U6IHRydWUgfSkuY2F0Y2goKGVycm9yKSA9PiB7CiAgICAgICAgICAgICAgICAgICAgbG9nLndhcm4oIlNjYW4gZ2VzcGVpY2hlcnQ7IHRlbXBvcsOkcmUgRGF0ZWllbiBrb25udGVuIG5pY2h0IGVudGZlcm50IHdlcmRlbiIsIHN0cmluZ2lmeUVycm9yKGVycm9yKSk7CiAgICAgICAgICAgICAgICB9KTsKICAgICAgICAgICAgICAgIGxvZy5pbmZvKCJTY2FuLUF1ZnRyYWcgYWJnZXNjaGxvc3NlbiIsIHsKICAgICAgICAgICAgICAgICAgICBqb2JJZDogbmV4dC5qb2IuaWQsCiAgICAgICAgICAgICAgICAgICAgZmlsZTogc2NhblJlc3VsdC5maWxlbmFtZSwKICAgICAgICAgICAgICAgICAgICBwYWdlczogc2NhblJlc3VsdC5wYWdlQ291bnQsCiAgICAgICAgICAgICAgICB9KTsKICAgICAgICAgICAgfQogICAgICAgICAgICBjYXRjaCAoZXJyb3IpIHsKICAgICAgICAgICAgICAgIGNvbnN0IG1lc3NhZ2UgPSBzdHJpbmdpZnlFcnJvcihlcnJvcik7CiAgICAgICAgICAgICAgICBsb2cuZXJyb3IoIlNjYW4tQXVmdHJhZyBmZWhsZ2VzY2hsYWdlbiIsIHsKICAgICAgICAgICAgICAgICAgICBqb2JJZDogbmV4dC5qb2IuaWQsCiAgICAgICAgICAgICAgICAgICAgbWVzc2FnZSwKICAgICAgICAgICAgICAgIH0pOwogICAgICAgICAgICAgICAgYXdhaXQgYXBpLnVwZGF0ZVNjYW5Kb2JTdGF0dXMobmV4dC5qb2IuaWQsICJmYWlsZWQiLCBtZXNzYWdlKTsKICAgICAgICAgICAgfQogICAgICAgICAgICBmaW5hbGx5IHsKICAgICAgICAgICAgICAgIGNsZWFySW50ZXJ2YWwoaGVhcnRiZWF0VGltZXIpOwogICAgICAgICAgICB9CiAgICAgICAgfQogICAgICAgIGNhdGNoIChlcnJvcikgewogICAgICAgICAgICBsb2cuZXJyb3IoIkFnZW50LVNjaGxlaWZlIGZlaGxnZXNjaGxhZ2VuIiwgc3RyaW5naWZ5RXJyb3IoZXJyb3IpKTsKICAgICAgICAgICAgYXdhaXQgc2xlZXAoY29uZmlnLnBvbGxTZWNvbmRzICogMTAwMCk7CiAgICAgICAgfQogICAgfQp9OwptYWluKCkuY2F0Y2goKGVycm9yKSA9PiB7CiAgICBsb2cuZXJyb3IoIkFnZW50IGtvbm50ZSBuaWNodCBnZXN0YXJ0ZXQgd2VyZGVuIiwgc3RyaW5naWZ5RXJyb3IoZXJyb3IpKTsKICAgIHByb2Nlc3MuZXhpdCgxKTsKfSk7Cg==", "dist/print/cups.js": "aW1wb3J0IHsgY29tbWFuZEV4aXN0cywgcnVuQ29tbWFuZCB9IGZyb20gIi4uL2NvbW1hbmRzLmpzIjsKZXhwb3J0IGNvbnN0IGhhc0N1cHMgPSAoKSA9PiBjb21tYW5kRXhpc3RzKCJscHN0YXQiKTsKZXhwb3J0IGNvbnN0IGxpc3RQcmludGVycyA9IGFzeW5jICgpID0+IHsKICAgIGlmICghYXdhaXQgaGFzQ3VwcygpKQogICAgICAgIHJldHVybiBbXTsKICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHJ1bkNvbW1hbmQoImxwc3RhdCIsIFsiLXAiXSwgeyB0aW1lb3V0TXM6IDEwXzAwMCB9KTsKICAgIGlmIChyZXN1bHQuY29kZSAhPT0gMCkKICAgICAgICByZXR1cm4gW107CiAgICByZXR1cm4gcmVzdWx0LnN0ZG91dAogICAgICAgIC5zcGxpdCgvXHI/XG4vKQogICAgICAgIC5tYXAoKGxpbmUpID0+IGxpbmUubWF0Y2goL15wcmludGVyXHMrKFxTKykvKT8uWzFdKQogICAgICAgIC5maWx0ZXIoKHByaW50ZXIpID0+IEJvb2xlYW4ocHJpbnRlcikpOwp9Owo=", "dist/scan/postprocess.js": "aW1wb3J0IHBhdGggZnJvbSAibm9kZTpwYXRoIjsKaW1wb3J0IHsgZmlsZVVSTFRvUGF0aCB9IGZyb20gIm5vZGU6dXJsIjsKaW1wb3J0IHsgcnVuQ29tbWFuZCB9IGZyb20gIi4uL2NvbW1hbmRzLmpzIjsKY29uc3QgYWdlbnRSb290ID0gcGF0aC5yZXNvbHZlKHBhdGguZGlybmFtZShmaWxlVVJMVG9QYXRoKGltcG9ydC5tZXRhLnVybCkpLCAiLi4vLi4iKTsKY29uc3QgZG9jdW1lbnRTY3JpcHQgPSBwYXRoLmpvaW4oYWdlbnRSb290LCAic2NyaXB0cy9zY2FuX2RvY3VtZW50LnB5Iik7CmV4cG9ydCBjb25zdCBjaGVja1NjYW5SdW50aW1lID0gYXN5bmMgKGNvbmZpZywgcG9zdHByb2Nlc3MpID0+IHsKICAgIGNvbnN0IGltcG9ydHMgPSBwb3N0cHJvY2VzcyAmJiBjb25maWcucG9zdHByb2Nlc3NTdHJpY3QgPyAiaW1wb3J0IFBJTCwgY3YyLCBudW1weSIgOiAiaW1wb3J0IFBJTCI7CiAgICB0cnkgewogICAgICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHJ1bkNvbW1hbmQoY29uZmlnLnBvc3Rwcm9jZXNzUHl0aG9uLCBbIi1jIiwgaW1wb3J0c10sIHsgdGltZW91dE1zOiAxMF8wMDAgfSk7CiAgICAgICAgaWYgKHJlc3VsdC5jb2RlID09PSAwKQogICAgICAgICAgICByZXR1cm47CiAgICB9CiAgICBjYXRjaCB7CiAgICAgICAgLy8gUmVwb3J0IG1pc3NpbmcgcnVudGltZXMgYmVmb3JlIHRoZSBzY2FubmVyIGNvbnN1bWVzIGFueSBwYXBlci4KICAgIH0KICAgIHRocm93IG5ldyBFcnJvcigiUHl0aG9uL1BpbGxvdyAoYmVpIHN0cmlrdGVyIE5hY2hiZWFyYmVpdHVuZyB6dXPDpHR6bGljaCBPcGVuQ1YpIGZlaGx0LiBCaXR0ZSBucG0gcnVuIHNldHVwOm9wZW5jdiBhdXNmw7xocmVuIG9kZXIgcHl0aG9uMy1waWwgaW5zdGFsbGllcmVuLiIpOwp9OwpleHBvcnQgY29uc3QgY3JlYXRlU2NhbkRvY3VtZW50ID0gYXN5bmMgKGNvbmZpZywgcGFnZXMsIG91dHB1dFBhdGgsIHJlc29sdXRpb24sIHBvc3Rwcm9jZXNzLCBwcm9maWxlKSA9PiB7CiAgICBjb25zdCBhcmdzID0gW2RvY3VtZW50U2NyaXB0LCAiLS1vdXRwdXQiLCBvdXRwdXRQYXRoLCAiLS1yZXNvbHV0aW9uIiwgU3RyaW5nKHJlc29sdXRpb24pLCAiLS1wcm9maWxlIiwgcHJvZmlsZV07CiAgICBpZiAocG9zdHByb2Nlc3MpCiAgICAgICAgYXJncy5wdXNoKCItLXBvc3Rwcm9jZXNzIik7CiAgICBpZiAoY29uZmlnLnBvc3Rwcm9jZXNzU3RyaWN0KQogICAgICAgIGFyZ3MucHVzaCgiLS1zdHJpY3QiKTsKICAgIGFyZ3MucHVzaCgiLS1pbnB1dHMiLCAuLi5wYWdlcyk7CiAgICBjb25zdCByZXN1bHQgPSBhd2FpdCBydW5Db21tYW5kKGNvbmZpZy5wb3N0cHJvY2Vzc1B5dGhvbiwgYXJncywgewogICAgICAgIHRpbWVvdXRNczogY29uZmlnLnNjYW5UaW1lb3V0U2Vjb25kcyAqIDEwMDAsCiAgICB9KTsKICAgIGlmIChyZXN1bHQuY29kZSAhPT0gMCkKICAgICAgICB0aHJvdyBuZXcgRXJyb3IocmVzdWx0LnN0ZGVyciB8fCAiU2Nhbi1EYXRlaSBrb25udGUgbmljaHQgZXJzdGVsbHQgd2VyZGVuIik7CiAgICBjb25zdCBkZXRhaWxzID0gSlNPTi5wYXJzZShyZXN1bHQuc3Rkb3V0KTsKICAgIHJldHVybiB7CiAgICAgICAgcGF0aDogb3V0cHV0UGF0aCwKICAgICAgICBmaWxlbmFtZTogcGF0aC5iYXNlbmFtZShvdXRwdXRQYXRoKSwKICAgICAgICBtaW1lVHlwZTogb3V0cHV0UGF0aC5lbmRzV2l0aCgiLnBkZiIpID8gImFwcGxpY2F0aW9uL3BkZiIgOiBvdXRwdXRQYXRoLmVuZHNXaXRoKCIucG5nIikgPyAiaW1hZ2UvcG5nIiA6ICJpbWFnZS90aWZmIiwKICAgICAgICAuLi5kZXRhaWxzLAogICAgfTsKfTsK", - "dist/scan/sane.js": "aW1wb3J0IHsgbWtkaXIsIG1rZHRlbXAsIHJlYWRkaXIsIHN0YXQgfSBmcm9tICJub2RlOmZzL3Byb21pc2VzIjsKaW1wb3J0IHBhdGggZnJvbSAibm9kZTpwYXRoIjsKaW1wb3J0IHsgY29tbWFuZEV4aXN0cywgcnVuQ29tbWFuZCB9IGZyb20gIi4uL2NvbW1hbmRzLmpzIjsKaW1wb3J0IHsgY2hlY2tTY2FuUnVudGltZSwgY3JlYXRlU2NhbkRvY3VtZW50IH0gZnJvbSAiLi9wb3N0cHJvY2Vzcy5qcyI7CmNvbnN0IHN0cmluZ1NldHRpbmcgPSAoc2V0dGluZ3MsIGtleSkgPT4gewogICAgY29uc3QgdmFsdWUgPSBzZXR0aW5nc1trZXldOwogICAgcmV0dXJuIHR5cGVvZiB2YWx1ZSA9PT0gInN0cmluZyIgJiYgdmFsdWUudHJpbSgpID8gdmFsdWUudHJpbSgpIDogdW5kZWZpbmVkOwp9Owpjb25zdCBib29sZWFuU2V0dGluZyA9IChzZXR0aW5ncywga2V5LCBmYWxsYmFjaykgPT4gewogICAgY29uc3QgdmFsdWUgPSBzZXR0aW5nc1trZXldOwogICAgaWYgKHR5cGVvZiB2YWx1ZSA9PT0gImJvb2xlYW4iKQogICAgICAgIHJldHVybiB2YWx1ZTsKICAgIGlmICh0eXBlb2YgdmFsdWUgPT09ICJzdHJpbmciKQogICAgICAgIHJldHVybiBbIjEiLCAidHJ1ZSIsICJ5ZXMiLCAiamEiLCAib24iXS5pbmNsdWRlcyh2YWx1ZS50cmltKCkudG9Mb3dlckNhc2UoKSk7CiAgICByZXR1cm4gZmFsbGJhY2s7Cn07CmV4cG9ydCBjb25zdCBoYXNTYW5lID0gKCkgPT4gY29tbWFuZEV4aXN0cygic2NhbmltYWdlIik7CmV4cG9ydCBjb25zdCBsaXN0U2Nhbm5lcnMgPSBhc3luYyAoKSA9PiB7CiAgICBpZiAoIWF3YWl0IGhhc1NhbmUoKSkKICAgICAgICByZXR1cm4gW107CiAgICBjb25zdCByZXN1bHQgPSBhd2FpdCBydW5Db21tYW5kKCJzY2FuaW1hZ2UiLCBbIi1MIl0sIHsgdGltZW91dE1zOiAxMF8wMDAgfSk7CiAgICBpZiAocmVzdWx0LmNvZGUgIT09IDApCiAgICAgICAgcmV0dXJuIFtdOwogICAgcmV0dXJuIHJlc3VsdC5zdGRvdXQuc3BsaXQoL1xyP1xuLykKICAgICAgICAubWFwKChsaW5lKSA9PiBsaW5lLnRyaW0oKS5tYXRjaCgvXmRldmljZSBgKFteJ10rKScvKT8uWzFdKQogICAgICAgIC5maWx0ZXIoKGRldmljZSkgPT4gQm9vbGVhbihkZXZpY2UpKTsKfTsKZXhwb3J0IGNvbnN0IHJ1blNjYW4gPSBhc3luYyAoY29uZmlnLCBqb2IsIHJlcG9ydFByb2dyZXNzID0gYXN5bmMgKCkgPT4geyB9KSA9PiB7CiAgICBpZiAoIWF3YWl0IGhhc1NhbmUoKSkKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoInNjYW5pbWFnZSBpc3QgbmljaHQgaW5zdGFsbGllcnQgb2RlciBuaWNodCBpbSBQQVRIIik7CiAgICBjb25zdCBzZXR0aW5ncyA9IGpvYi5zZXR0aW5ncyB8fCB7fTsKICAgIGNvbnN0IGZvcm1hdCA9IHN0cmluZ1NldHRpbmcoc2V0dGluZ3MsICJmb3JtYXQiKSB8fCBjb25maWcuc2NhbkZvcm1hdDsKICAgIGlmICghWyJwZGYiLCAicG5nIiwgInRpZmYiXS5pbmNsdWRlcyhmb3JtYXQpKQogICAgICAgIHRocm93IG5ldyBFcnJvcigiU2Nhbi1Gb3JtYXQgbXVzcyBQREYsIFBORyBvZGVyIFRJRkYgc2VpbiIpOwogICAgY29uc3QgcmVzb2x1dGlvbiA9IE51bWJlcihzZXR0aW5ncy5yZXNvbHV0aW9uID8/IGNvbmZpZy5zY2FuUmVzb2x1dGlvbik7CiAgICBpZiAoIU51bWJlci5pc0Zpbml0ZShyZXNvbHV0aW9uKSB8fCByZXNvbHV0aW9uIDwgNzUgfHwgcmVzb2x1dGlvbiA+IDEyMDApIHsKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkRpZSBTY2FuLUF1ZmzDtnN1bmcgbXVzcyB6d2lzY2hlbiA3NSB1bmQgMTIwMCBkcGkgbGllZ2VuIik7CiAgICB9CiAgICBjb25zdCBtb2RlID0gc3RyaW5nU2V0dGluZyhzZXR0aW5ncywgIm1vZGUiKSB8fCBjb25maWcuc2Nhbk1vZGU7CiAgICBjb25zdCBzb3VyY2UgPSBzdHJpbmdTZXR0aW5nKHNldHRpbmdzLCAic291cmNlIikgfHwgY29uZmlnLnNjYW5Tb3VyY2U7CiAgICBjb25zdCBzY2FubmVyTmFtZSA9IGpvYi5zY2FubmVyTmFtZSB8fCBjb25maWcuc2Nhbm5lck5hbWU7CiAgICBjb25zdCBwYXBlclNpemUgPSBzdHJpbmdTZXR0aW5nKHNldHRpbmdzLCAicGFwZXJTaXplIikgfHwgImF1dG8iOwogICAgaWYgKCFbImF1dG8iLCAiQTQiLCAiTGV0dGVyIl0uaW5jbHVkZXMocGFwZXJTaXplKSkKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIlVuYmVrYW5udGVzIFBhcGllcmZvcm1hdCIpOwogICAgY29uc3QgYmF0Y2ggPSBCb29sZWFuKHNvdXJjZSAmJiAvYWRmfGRvY3VtZW50IGZlZWRlci9pLnRlc3Qoc291cmNlKSk7CiAgICBpZiAoYmF0Y2ggJiYgZm9ybWF0ICE9PSAicGRmIikKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkbDvHIgRWluenVnLSB1bmQgRHVwbGV4LVNjYW5zIGJpdHRlIFBERiBhbHMgRm9ybWF0IHfDpGhsZW4iKTsKICAgIGNvbnN0IHBvc3Rwcm9jZXNzID0gYm9vbGVhblNldHRpbmcoc2V0dGluZ3MsICJwb3N0cHJvY2VzcyIsIGNvbmZpZy5zY2FuUG9zdHByb2Nlc3MpOwogICAgY29uc3QgcHJvZmlsZSA9IHN0cmluZ1NldHRpbmcoc2V0dGluZ3MsICJwb3N0cHJvY2Vzc1Byb2ZpbGUiKSB8fCBjb25maWcucG9zdHByb2Nlc3NQcm9maWxlOwogICAgaWYgKCFbImRvY3VtZW50IiwgInJlY2VpcHQiLCAicmF3Il0uaW5jbHVkZXMocHJvZmlsZSkpCiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCJVbmJla2FubnRlcyBOYWNoYmVhcmJlaXR1bmdzcHJvZmlsIik7CiAgICBhd2FpdCBjaGVja1NjYW5SdW50aW1lKGNvbmZpZywgcG9zdHByb2Nlc3MpOwogICAgLy8gRWFjaCBwaHlzaWNhbCBzY2FuIGhhcyBpdHMgb3duIGRpcmVjdG9yeS4gQSBmaWxlbmFtZSBjYW5ub3QgZXNjYXBlIGl0IG9yIG92ZXJ3cml0ZSBhbm90aGVyIGpvYi4KICAgIGF3YWl0IG1rZGlyKGNvbmZpZy53b3JrRGlyLCB7IHJlY3Vyc2l2ZTogdHJ1ZSwgbW9kZTogMG83MDAgfSk7CiAgICBjb25zdCBqb2JEaXIgPSBhd2FpdCBta2R0ZW1wKHBhdGguam9pbihwYXRoLnJlc29sdmUoY29uZmlnLndvcmtEaXIpLCAic2Nhbi0iKSk7CiAgICBjb25zdCByZXF1ZXN0ZWQgPSBwYXRoLmJhc2VuYW1lKChqb2IucmVxdWVzdGVkRmlsZW5hbWUgfHwgInNjYW4iKS5yZXBsYWNlKC9cXC9nLCAiLyIpKTsKICAgIGNvbnN0IHN0ZW0gPSByZXF1ZXN0ZWQucmVwbGFjZSgvXC5bXi5dKiQvLCAiIikucmVwbGFjZSgvW1x4MDAtXHgxZl0vZywgIiIpLnRyaW0oKSB8fCAic2NhbiI7CiAgICBjb25zdCBvdXRwdXRQYXRoID0gcGF0aC5qb2luKGpvYkRpciwgYCR7c3RlbX0uJHtmb3JtYXR9YCk7CiAgICB0cnkgewogICAgICAgIC8vIERldmljZS9zb3VyY2UgY29tZSBmaXJzdDogY2hhbmdpbmcgc291cmNlIGNhbiBjaGFuZ2UgdGhlIHJlbWFpbmluZyBTQU5FIG9wdGlvbnMuCiAgICAgICAgY29uc3QgYXJncyA9IFtdOwogICAgICAgIGlmIChzY2FubmVyTmFtZSkKICAgICAgICAgICAgYXJncy5wdXNoKCItLWRldmljZS1uYW1lIiwgc2Nhbm5lck5hbWUpOwogICAgICAgIGlmIChzb3VyY2UpCiAgICAgICAgICAgIGFyZ3MucHVzaCgiLS1zb3VyY2UiLCBzb3VyY2UpOwogICAgICAgIGFyZ3MucHVzaCgiLS1tb2RlIiwgbW9kZSwgIi0tcmVzb2x1dGlvbiIsIFN0cmluZyhyZXNvbHV0aW9uKSwgIi0tZm9ybWF0PXBuZyIpOwogICAgICAgIGlmIChwYXBlclNpemUgIT09ICJhdXRvIikgewogICAgICAgICAgICBjb25zdCBbd2lkdGgsIGhlaWdodF0gPSBwYXBlclNpemUgPT09ICJBNCIgPyBbMjEwLCAyOTddIDogWzIxNS45LCAyNzkuNF07CiAgICAgICAgICAgIGlmIChzY2FubmVyTmFtZT8uc3RhcnRzV2l0aCgiZnVqaXRzdToiKSkgewogICAgICAgICAgICAgICAgYXJncy5wdXNoKCItLXBhZ2Utd2lkdGgiLCBTdHJpbmcod2lkdGgpLCAiLS1wYWdlLWhlaWdodCIsIFN0cmluZyhoZWlnaHQpKTsKICAgICAgICAgICAgfQogICAgICAgICAgICBhcmdzLnB1c2goIi1sIiwgIjAiLCAiLXQiLCAiMCIsICIteCIsIFN0cmluZyh3aWR0aCksICIteSIsIFN0cmluZyhoZWlnaHQpKTsKICAgICAgICB9CiAgICAgICAgaWYgKGJhdGNoKSB7CiAgICAgICAgICAgIC8vIFJlbGF0aXZlIHBhdHRlcm4gYXZvaWRzIGludGVycHJldGluZyAnJScgaW4gYSB1c2VyLWNvbmZpZ3VyZWQgd29yayBkaXJlY3RvcnkuCiAgICAgICAgICAgIGFyZ3MucHVzaCgiLS1iYXRjaD1wYWdlLSUwNmQucG5nIiwgIi0tYmF0Y2gtc3RhcnQ9MSIsICItLWJhdGNoLWluY3JlbWVudD0xIik7CiAgICAgICAgfQogICAgICAgIGVsc2UgewogICAgICAgICAgICBhcmdzLnB1c2goIi0tb3V0cHV0LWZpbGUiLCAicGFnZS0wMDAwMDEucG5nIik7CiAgICAgICAgfQogICAgICAgIGF3YWl0IHJlcG9ydFByb2dyZXNzKGJhdGNoID8gIkRva3VtZW50ZSB3ZXJkZW4gZWluZ2V6b2dlbiB1bmQgZ2VzY2FubnQuLi4iIDogIlNlaXRlIHdpcmQgZ2VzY2FubnQuLi4iKTsKICAgICAgICBjb25zdCByZXN1bHQgPSBhd2FpdCBydW5Db21tYW5kKCJzY2FuaW1hZ2UiLCBhcmdzLCB7CiAgICAgICAgICAgIGN3ZDogam9iRGlyLAogICAgICAgICAgICB0aW1lb3V0TXM6IGNvbmZpZy5zY2FuVGltZW91dFNlY29uZHMgKiAxMDAwLAogICAgICAgIH0pOwogICAgICAgIC8vIFNBTkVfU1RBVFVTX05PX0RPQ1MgKDcpIGlzIHRoZSBleHBlY3RlZCBlbmQgb2YgYW4gdW5saW1pdGVkIEFERiBiYXRjaC4KICAgICAgICAvLyBOZXZlciB0dXJuIHBhcGVyIGphbXMsIEkvTyBlcnJvcnMgb3IgY2FuY2VsZWQgc2NhbnMgaW50byBhIHN1Y2Nlc3NmdWwgcGFydGlhbCB1cGxvYWQuCiAgICAgICAgaWYgKHJlc3VsdC5jb2RlICE9PSAwICYmICEoYmF0Y2ggJiYgcmVzdWx0LmNvZGUgPT09IDcpKSB7CiAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcihyZXN1bHQuc3RkZXJyLnRyaW0oKSB8fCBgc2NhbmltYWdlIHd1cmRlIG1pdCBDb2RlICR7cmVzdWx0LmNvZGV9IGJlZW5kZXRgKTsKICAgICAgICB9CiAgICAgICAgY29uc3QgbmFtZXMgPSAoYXdhaXQgcmVhZGRpcihqb2JEaXIpKQogICAgICAgICAgICAuZmlsdGVyKChuYW1lKSA9PiAvXnBhZ2UtXGQrXC5wbmckLy50ZXN0KG5hbWUpKQogICAgICAgICAgICAuc29ydCgoYSwgYikgPT4gTnVtYmVyKGEubWF0Y2goL1xkKy8pWzBdKSAtIE51bWJlcihiLm1hdGNoKC9cZCsvKVswXSkpOwogICAgICAgIGNvbnN0IHBhZ2VzID0gW107CiAgICAgICAgZm9yIChjb25zdCBbaW5kZXgsIG5hbWVdIG9mIG5hbWVzLmVudHJpZXMoKSkgewogICAgICAgICAgICBjb25zdCBwYWdlID0gcGF0aC5qb2luKGpvYkRpciwgbmFtZSk7CiAgICAgICAgICAgIC8vIFNvbWUgc2NhbmltYWdlIHZlcnNpb25zIGxlYXZlIGEgemVyby1ieXRlIGZpbGUgZm9yIHRoZSBmaW5hbCBOT19ET0NTIGF0dGVtcHQuCiAgICAgICAgICAgIGlmICgoYXdhaXQgc3RhdChwYWdlKSkuc2l6ZSA9PT0gMCkgewogICAgICAgICAgICAgICAgaWYgKGJhdGNoICYmIHJlc3VsdC5jb2RlID09PSA3ICYmIGluZGV4ID09PSBuYW1lcy5sZW5ndGggLSAxKQogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlOwogICAgICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBVbnZvbGxzdMOkbmRpZ2UgU2Nhbi1TZWl0ZTogJHtuYW1lfWApOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGlmIChOdW1iZXIobmFtZS5tYXRjaCgvXGQrLylbMF0pICE9PSBpbmRleCArIDEpCiAgICAgICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkltIFNjYW4gZmVobGVuIFNlaXRlbiIpOwogICAgICAgICAgICBwYWdlcy5wdXNoKHBhZ2UpOwogICAgICAgIH0KICAgICAgICBpZiAoIXBhZ2VzLmxlbmd0aCkKICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKCJLZWluZSBTZWl0ZW4gZ2VzY2FubnQuIEJpdHRlIFBhcGllciBpbiBkZW4gRWluenVnIGxlZ2VuLiIpOwogICAgICAgIGlmIChzb3VyY2U/LnRvTG93ZXJDYXNlKCkuaW5jbHVkZXMoImR1cGxleCIpICYmIHBhZ2VzLmxlbmd0aCAlIDIgIT09IDApIHsKICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKCJEZXIgRHVwbGV4LVNjYW4gaXN0IHVudm9sbHN0w6RuZGlnOiBadSBlaW5lciBWb3JkZXJzZWl0ZSBmZWhsdCBkaWUgUsO8Y2tzZWl0ZS4iKTsKICAgICAgICB9CiAgICAgICAgYXdhaXQgcmVwb3J0UHJvZ3Jlc3MoYCR7cGFnZXMubGVuZ3RofSBTZWl0ZW4gZ2VzY2FubnQuIERhdGVpIHdpcmQgZXJzdGVsbHQuLi5gKTsKICAgICAgICByZXR1cm4gYXdhaXQgY3JlYXRlU2NhbkRvY3VtZW50KGNvbmZpZywgcGFnZXMsIG91dHB1dFBhdGgsIHJlc29sdXRpb24sIHBvc3Rwcm9jZXNzLCBwcm9maWxlKTsKICAgIH0KICAgIGNhdGNoIChlcnJvcikgewogICAgICAgIGNvbnN0IG1lc3NhZ2UgPSBlcnJvciBpbnN0YW5jZW9mIEVycm9yID8gZXJyb3IubWVzc2FnZSA6IFN0cmluZyhlcnJvcik7CiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGAke21lc3NhZ2V9IFJvaHNjYW5zIGJsZWliZW4genVyIFdpZWRlcmhlcnN0ZWxsdW5nIGluICR7am9iRGlyfS5gKTsKICAgIH0KfTsK", + "dist/scan/sane.js": "aW1wb3J0IHsgbWtkaXIsIG1rZHRlbXAsIHJlYWRkaXIsIHN0YXQgfSBmcm9tICJub2RlOmZzL3Byb21pc2VzIjsKaW1wb3J0IHBhdGggZnJvbSAibm9kZTpwYXRoIjsKaW1wb3J0IHsgY29tbWFuZEV4aXN0cywgcnVuQ29tbWFuZCB9IGZyb20gIi4uL2NvbW1hbmRzLmpzIjsKaW1wb3J0IHsgY2hlY2tTY2FuUnVudGltZSwgY3JlYXRlU2NhbkRvY3VtZW50IH0gZnJvbSAiLi9wb3N0cHJvY2Vzcy5qcyI7CmNvbnN0IHN0cmluZ1NldHRpbmcgPSAoc2V0dGluZ3MsIGtleSkgPT4gewogICAgY29uc3QgdmFsdWUgPSBzZXR0aW5nc1trZXldOwogICAgcmV0dXJuIHR5cGVvZiB2YWx1ZSA9PT0gInN0cmluZyIgJiYgdmFsdWUudHJpbSgpID8gdmFsdWUudHJpbSgpIDogdW5kZWZpbmVkOwp9Owpjb25zdCBib29sZWFuU2V0dGluZyA9IChzZXR0aW5ncywga2V5LCBmYWxsYmFjaykgPT4gewogICAgY29uc3QgdmFsdWUgPSBzZXR0aW5nc1trZXldOwogICAgaWYgKHR5cGVvZiB2YWx1ZSA9PT0gImJvb2xlYW4iKQogICAgICAgIHJldHVybiB2YWx1ZTsKICAgIGlmICh0eXBlb2YgdmFsdWUgPT09ICJzdHJpbmciKQogICAgICAgIHJldHVybiBbIjEiLCAidHJ1ZSIsICJ5ZXMiLCAiamEiLCAib24iXS5pbmNsdWRlcyh2YWx1ZS50cmltKCkudG9Mb3dlckNhc2UoKSk7CiAgICByZXR1cm4gZmFsbGJhY2s7Cn07CmV4cG9ydCBjb25zdCBoYXNTYW5lID0gKCkgPT4gY29tbWFuZEV4aXN0cygic2NhbmltYWdlIik7CmV4cG9ydCBjb25zdCBsaXN0U2Nhbm5lcnMgPSBhc3luYyAoKSA9PiB7CiAgICBpZiAoIWF3YWl0IGhhc1NhbmUoKSkKICAgICAgICByZXR1cm4gW107CiAgICAvLyBTQU5FIGFsc28gcHJvYmVzIG5ldHdvcmsgYmFja2VuZHM7IHRoZXNlIGNhbiB0YWtlIG1vcmUgdGhhbiB0ZW4gc2Vjb25kcwogICAgLy8gZXZlbiB3aGVuIHRoZSBVU0Igc2Nhbm5lciBpcyBhbHJlYWR5IGF2YWlsYWJsZS4KICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHJ1bkNvbW1hbmQoInNjYW5pbWFnZSIsIFsiLUwiXSwgeyB0aW1lb3V0TXM6IDMwXzAwMCB9KTsKICAgIGlmIChyZXN1bHQuY29kZSAhPT0gMCkKICAgICAgICByZXR1cm4gW107CiAgICByZXR1cm4gcmVzdWx0LnN0ZG91dC5zcGxpdCgvXHI/XG4vKQogICAgICAgIC5tYXAoKGxpbmUpID0+IGxpbmUudHJpbSgpLm1hdGNoKC9eZGV2aWNlIGAoW14nXSspJy8pPy5bMV0pCiAgICAgICAgLmZpbHRlcigoZGV2aWNlKSA9PiBCb29sZWFuKGRldmljZSkpOwp9OwpleHBvcnQgY29uc3QgcnVuU2NhbiA9IGFzeW5jIChjb25maWcsIGpvYiwgcmVwb3J0UHJvZ3Jlc3MgPSBhc3luYyAoKSA9PiB7IH0pID0+IHsKICAgIGlmICghYXdhaXQgaGFzU2FuZSgpKQogICAgICAgIHRocm93IG5ldyBFcnJvcigic2NhbmltYWdlIGlzdCBuaWNodCBpbnN0YWxsaWVydCBvZGVyIG5pY2h0IGltIFBBVEgiKTsKICAgIGNvbnN0IHNldHRpbmdzID0gam9iLnNldHRpbmdzIHx8IHt9OwogICAgY29uc3QgZm9ybWF0ID0gc3RyaW5nU2V0dGluZyhzZXR0aW5ncywgImZvcm1hdCIpIHx8IGNvbmZpZy5zY2FuRm9ybWF0OwogICAgaWYgKCFbInBkZiIsICJwbmciLCAidGlmZiJdLmluY2x1ZGVzKGZvcm1hdCkpCiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCJTY2FuLUZvcm1hdCBtdXNzIFBERiwgUE5HIG9kZXIgVElGRiBzZWluIik7CiAgICBjb25zdCByZXNvbHV0aW9uID0gTnVtYmVyKHNldHRpbmdzLnJlc29sdXRpb24gPz8gY29uZmlnLnNjYW5SZXNvbHV0aW9uKTsKICAgIGlmICghTnVtYmVyLmlzRmluaXRlKHJlc29sdXRpb24pIHx8IHJlc29sdXRpb24gPCA3NSB8fCByZXNvbHV0aW9uID4gMTIwMCkgewogICAgICAgIHRocm93IG5ldyBFcnJvcigiRGllIFNjYW4tQXVmbMO2c3VuZyBtdXNzIHp3aXNjaGVuIDc1IHVuZCAxMjAwIGRwaSBsaWVnZW4iKTsKICAgIH0KICAgIGNvbnN0IG1vZGUgPSBzdHJpbmdTZXR0aW5nKHNldHRpbmdzLCAibW9kZSIpIHx8IGNvbmZpZy5zY2FuTW9kZTsKICAgIGNvbnN0IHNvdXJjZSA9IHN0cmluZ1NldHRpbmcoc2V0dGluZ3MsICJzb3VyY2UiKSB8fCBjb25maWcuc2NhblNvdXJjZTsKICAgIGNvbnN0IHNjYW5uZXJOYW1lID0gam9iLnNjYW5uZXJOYW1lIHx8IGNvbmZpZy5zY2FubmVyTmFtZTsKICAgIGNvbnN0IHBhcGVyU2l6ZSA9IHN0cmluZ1NldHRpbmcoc2V0dGluZ3MsICJwYXBlclNpemUiKSB8fCAiYXV0byI7CiAgICBpZiAoIVsiYXV0byIsICJBNCIsICJMZXR0ZXIiXS5pbmNsdWRlcyhwYXBlclNpemUpKQogICAgICAgIHRocm93IG5ldyBFcnJvcigiVW5iZWthbm50ZXMgUGFwaWVyZm9ybWF0Iik7CiAgICBjb25zdCBiYXRjaCA9IEJvb2xlYW4oc291cmNlICYmIC9hZGZ8ZG9jdW1lbnQgZmVlZGVyL2kudGVzdChzb3VyY2UpKTsKICAgIGlmIChiYXRjaCAmJiBmb3JtYXQgIT09ICJwZGYiKQogICAgICAgIHRocm93IG5ldyBFcnJvcigiRsO8ciBFaW56dWctIHVuZCBEdXBsZXgtU2NhbnMgYml0dGUgUERGIGFscyBGb3JtYXQgd8OkaGxlbiIpOwogICAgY29uc3QgcG9zdHByb2Nlc3MgPSBib29sZWFuU2V0dGluZyhzZXR0aW5ncywgInBvc3Rwcm9jZXNzIiwgY29uZmlnLnNjYW5Qb3N0cHJvY2Vzcyk7CiAgICBjb25zdCBwcm9maWxlID0gc3RyaW5nU2V0dGluZyhzZXR0aW5ncywgInBvc3Rwcm9jZXNzUHJvZmlsZSIpIHx8IGNvbmZpZy5wb3N0cHJvY2Vzc1Byb2ZpbGU7CiAgICBpZiAoIVsiZG9jdW1lbnQiLCAicmVjZWlwdCIsICJyYXciXS5pbmNsdWRlcyhwcm9maWxlKSkKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIlVuYmVrYW5udGVzIE5hY2hiZWFyYmVpdHVuZ3Nwcm9maWwiKTsKICAgIGF3YWl0IGNoZWNrU2NhblJ1bnRpbWUoY29uZmlnLCBwb3N0cHJvY2Vzcyk7CiAgICAvLyBFYWNoIHBoeXNpY2FsIHNjYW4gaGFzIGl0cyBvd24gZGlyZWN0b3J5LiBBIGZpbGVuYW1lIGNhbm5vdCBlc2NhcGUgaXQgb3Igb3ZlcndyaXRlIGFub3RoZXIgam9iLgogICAgYXdhaXQgbWtkaXIoY29uZmlnLndvcmtEaXIsIHsgcmVjdXJzaXZlOiB0cnVlLCBtb2RlOiAwbzcwMCB9KTsKICAgIGNvbnN0IGpvYkRpciA9IGF3YWl0IG1rZHRlbXAocGF0aC5qb2luKHBhdGgucmVzb2x2ZShjb25maWcud29ya0RpciksICJzY2FuLSIpKTsKICAgIGNvbnN0IHJlcXVlc3RlZCA9IHBhdGguYmFzZW5hbWUoKGpvYi5yZXF1ZXN0ZWRGaWxlbmFtZSB8fCAic2NhbiIpLnJlcGxhY2UoL1xcL2csICIvIikpOwogICAgY29uc3Qgc3RlbSA9IHJlcXVlc3RlZC5yZXBsYWNlKC9cLlteLl0qJC8sICIiKS5yZXBsYWNlKC9bXHgwMC1ceDFmXS9nLCAiIikudHJpbSgpIHx8ICJzY2FuIjsKICAgIGNvbnN0IG91dHB1dFBhdGggPSBwYXRoLmpvaW4oam9iRGlyLCBgJHtzdGVtfS4ke2Zvcm1hdH1gKTsKICAgIHRyeSB7CiAgICAgICAgLy8gRGV2aWNlL3NvdXJjZSBjb21lIGZpcnN0OiBjaGFuZ2luZyBzb3VyY2UgY2FuIGNoYW5nZSB0aGUgcmVtYWluaW5nIFNBTkUgb3B0aW9ucy4KICAgICAgICBjb25zdCBhcmdzID0gW107CiAgICAgICAgaWYgKHNjYW5uZXJOYW1lKQogICAgICAgICAgICBhcmdzLnB1c2goIi0tZGV2aWNlLW5hbWUiLCBzY2FubmVyTmFtZSk7CiAgICAgICAgaWYgKHNvdXJjZSkKICAgICAgICAgICAgYXJncy5wdXNoKCItLXNvdXJjZSIsIHNvdXJjZSk7CiAgICAgICAgYXJncy5wdXNoKCItLW1vZGUiLCBtb2RlLCAiLS1yZXNvbHV0aW9uIiwgU3RyaW5nKHJlc29sdXRpb24pLCAiLS1mb3JtYXQ9cG5nIik7CiAgICAgICAgaWYgKHBhcGVyU2l6ZSAhPT0gImF1dG8iKSB7CiAgICAgICAgICAgIGNvbnN0IFt3aWR0aCwgaGVpZ2h0XSA9IHBhcGVyU2l6ZSA9PT0gIkE0IiA/IFsyMTAsIDI5N10gOiBbMjE1LjksIDI3OS40XTsKICAgICAgICAgICAgaWYgKHNjYW5uZXJOYW1lPy5zdGFydHNXaXRoKCJmdWppdHN1OiIpKSB7CiAgICAgICAgICAgICAgICBhcmdzLnB1c2goIi0tcGFnZS13aWR0aCIsIFN0cmluZyh3aWR0aCksICItLXBhZ2UtaGVpZ2h0IiwgU3RyaW5nKGhlaWdodCkpOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGFyZ3MucHVzaCgiLWwiLCAiMCIsICItdCIsICIwIiwgIi14IiwgU3RyaW5nKHdpZHRoKSwgIi15IiwgU3RyaW5nKGhlaWdodCkpOwogICAgICAgIH0KICAgICAgICBpZiAoYmF0Y2gpIHsKICAgICAgICAgICAgLy8gUmVsYXRpdmUgcGF0dGVybiBhdm9pZHMgaW50ZXJwcmV0aW5nICclJyBpbiBhIHVzZXItY29uZmlndXJlZCB3b3JrIGRpcmVjdG9yeS4KICAgICAgICAgICAgYXJncy5wdXNoKCItLWJhdGNoPXBhZ2UtJTA2ZC5wbmciLCAiLS1iYXRjaC1zdGFydD0xIiwgIi0tYmF0Y2gtaW5jcmVtZW50PTEiKTsKICAgICAgICB9CiAgICAgICAgZWxzZSB7CiAgICAgICAgICAgIGFyZ3MucHVzaCgiLS1vdXRwdXQtZmlsZSIsICJwYWdlLTAwMDAwMS5wbmciKTsKICAgICAgICB9CiAgICAgICAgYXdhaXQgcmVwb3J0UHJvZ3Jlc3MoYmF0Y2ggPyAiRG9rdW1lbnRlIHdlcmRlbiBlaW5nZXpvZ2VuIHVuZCBnZXNjYW5udC4uLiIgOiAiU2VpdGUgd2lyZCBnZXNjYW5udC4uLiIpOwogICAgICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHJ1bkNvbW1hbmQoInNjYW5pbWFnZSIsIGFyZ3MsIHsKICAgICAgICAgICAgY3dkOiBqb2JEaXIsCiAgICAgICAgICAgIHRpbWVvdXRNczogY29uZmlnLnNjYW5UaW1lb3V0U2Vjb25kcyAqIDEwMDAsCiAgICAgICAgfSk7CiAgICAgICAgLy8gU0FORV9TVEFUVVNfTk9fRE9DUyAoNykgaXMgdGhlIGV4cGVjdGVkIGVuZCBvZiBhbiB1bmxpbWl0ZWQgQURGIGJhdGNoLgogICAgICAgIC8vIE5ldmVyIHR1cm4gcGFwZXIgamFtcywgSS9PIGVycm9ycyBvciBjYW5jZWxlZCBzY2FucyBpbnRvIGEgc3VjY2Vzc2Z1bCBwYXJ0aWFsIHVwbG9hZC4KICAgICAgICBpZiAocmVzdWx0LmNvZGUgIT09IDAgJiYgIShiYXRjaCAmJiByZXN1bHQuY29kZSA9PT0gNykpIHsKICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKHJlc3VsdC5zdGRlcnIudHJpbSgpIHx8IGBzY2FuaW1hZ2Ugd3VyZGUgbWl0IENvZGUgJHtyZXN1bHQuY29kZX0gYmVlbmRldGApOwogICAgICAgIH0KICAgICAgICBjb25zdCBuYW1lcyA9IChhd2FpdCByZWFkZGlyKGpvYkRpcikpCiAgICAgICAgICAgIC5maWx0ZXIoKG5hbWUpID0+IC9ecGFnZS1cZCtcLnBuZyQvLnRlc3QobmFtZSkpCiAgICAgICAgICAgIC5zb3J0KChhLCBiKSA9PiBOdW1iZXIoYS5tYXRjaCgvXGQrLylbMF0pIC0gTnVtYmVyKGIubWF0Y2goL1xkKy8pWzBdKSk7CiAgICAgICAgY29uc3QgcGFnZXMgPSBbXTsKICAgICAgICBmb3IgKGNvbnN0IFtpbmRleCwgbmFtZV0gb2YgbmFtZXMuZW50cmllcygpKSB7CiAgICAgICAgICAgIGNvbnN0IHBhZ2UgPSBwYXRoLmpvaW4oam9iRGlyLCBuYW1lKTsKICAgICAgICAgICAgLy8gU29tZSBzY2FuaW1hZ2UgdmVyc2lvbnMgbGVhdmUgYSB6ZXJvLWJ5dGUgZmlsZSBmb3IgdGhlIGZpbmFsIE5PX0RPQ1MgYXR0ZW1wdC4KICAgICAgICAgICAgaWYgKChhd2FpdCBzdGF0KHBhZ2UpKS5zaXplID09PSAwKSB7CiAgICAgICAgICAgICAgICBpZiAoYmF0Y2ggJiYgcmVzdWx0LmNvZGUgPT09IDcgJiYgaW5kZXggPT09IG5hbWVzLmxlbmd0aCAtIDEpCiAgICAgICAgICAgICAgICAgICAgY29udGludWU7CiAgICAgICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYFVudm9sbHN0w6RuZGlnZSBTY2FuLVNlaXRlOiAke25hbWV9YCk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgaWYgKE51bWJlcihuYW1lLm1hdGNoKC9cZCsvKVswXSkgIT09IGluZGV4ICsgMSkKICAgICAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcigiSW0gU2NhbiBmZWhsZW4gU2VpdGVuIik7CiAgICAgICAgICAgIHBhZ2VzLnB1c2gocGFnZSk7CiAgICAgICAgfQogICAgICAgIGlmICghcGFnZXMubGVuZ3RoKQogICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIktlaW5lIFNlaXRlbiBnZXNjYW5udC4gQml0dGUgUGFwaWVyIGluIGRlbiBFaW56dWcgbGVnZW4uIik7CiAgICAgICAgaWYgKHNvdXJjZT8udG9Mb3dlckNhc2UoKS5pbmNsdWRlcygiZHVwbGV4IikgJiYgcGFnZXMubGVuZ3RoICUgMiAhPT0gMCkgewogICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkRlciBEdXBsZXgtU2NhbiBpc3QgdW52b2xsc3TDpG5kaWc6IFp1IGVpbmVyIFZvcmRlcnNlaXRlIGZlaGx0IGRpZSBSw7xja3NlaXRlLiIpOwogICAgICAgIH0KICAgICAgICBhd2FpdCByZXBvcnRQcm9ncmVzcyhgJHtwYWdlcy5sZW5ndGh9IFNlaXRlbiBnZXNjYW5udC4gRGF0ZWkgd2lyZCBlcnN0ZWxsdC4uLmApOwogICAgICAgIHJldHVybiBhd2FpdCBjcmVhdGVTY2FuRG9jdW1lbnQoY29uZmlnLCBwYWdlcywgb3V0cHV0UGF0aCwgcmVzb2x1dGlvbiwgcG9zdHByb2Nlc3MsIHByb2ZpbGUpOwogICAgfQogICAgY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgY29uc3QgbWVzc2FnZSA9IGVycm9yIGluc3RhbmNlb2YgRXJyb3IgPyBlcnJvci5tZXNzYWdlIDogU3RyaW5nKGVycm9yKTsKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYCR7bWVzc2FnZX0gUm9oc2NhbnMgYmxlaWJlbiB6dXIgV2llZGVyaGVyc3RlbGx1bmcgaW4gJHtqb2JEaXJ9LmApOwogICAgfQp9Owo=", "dist/types.js": "ZXhwb3J0IHt9Owo=", "scripts/scan_document.py": "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoiIiJBc3NlbWJsZSBvcmRlcmVkIHNjYW5uZXIgcGFnZXM7IE9wZW5DViBpcyBvcHRpb25hbCwgUERGIGNyZWF0aW9uIGlzIG5vdC4iIiIKaW1wb3J0IGFyZ3BhcnNlCmltcG9ydCBqc29uCmZyb20gcGF0aGxpYiBpbXBvcnQgUGF0aAoKZnJvbSBQSUwgaW1wb3J0IEltYWdlCgoKZGVmIGNyZWF0ZV9kb2N1bWVudChpbnB1dHMsIG91dHB1dCwgcmVzb2x1dGlvbiwgdXNlX3Bvc3Rwcm9jZXNzPUZhbHNlLCBwcm9maWxlPSJkb2N1bWVudCIsIHN0cmljdD1GYWxzZSk6CiAgICBpZiBub3QgaW5wdXRzOgogICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoIktlaW5lIFNlaXRlbiB2b3JoYW5kZW4iKQogICAgb3V0cHV0ID0gUGF0aChvdXRwdXQpCiAgICBmaWxlX2Zvcm1hdCA9IHsiLnBkZiI6ICJQREYiLCAiLnBuZyI6ICJQTkciLCAiLnRpZmYiOiAiVElGRiJ9LmdldChvdXRwdXQuc3VmZml4Lmxvd2VyKCkpCiAgICBpZiBub3QgZmlsZV9mb3JtYXQ6CiAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiVW5iZWthbm50ZXMgQXVzZ2FiZWZvcm1hdCIpCiAgICBpZiBmaWxlX2Zvcm1hdCAhPSAiUERGIiBhbmQgbGVuKGlucHV0cykgIT0gMToKICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJNZWhyc2VpdGlnZSBTY2FucyBiZW7DtnRpZ2VuIFBERiBhbHMgQXVzZ2FiZWZvcm1hdCIpCgogICAgd2FybmluZ3MgPSBbXQogICAgcHJvY2Vzc29yID0gTm9uZQogICAgaWYgdXNlX3Bvc3Rwcm9jZXNzOgogICAgICAgIHRyeToKICAgICAgICAgICAgZnJvbSBvcGVuY3ZfcG9zdHByb2Nlc3MgaW1wb3J0IHBvc3Rwcm9jZXNzCiAgICAgICAgICAgIHByb2Nlc3NvciA9IHBvc3Rwcm9jZXNzCiAgICAgICAgZXhjZXB0IEltcG9ydEVycm9yOgogICAgICAgICAgICBpZiBzdHJpY3Q6CiAgICAgICAgICAgICAgICByYWlzZQogICAgICAgICAgICB3YXJuaW5ncy5hcHBlbmQoIk9wZW5DViBmZWhsdDsgYWxsZSBTZWl0ZW4gd3VyZGVuIG9obmUgQmlsZGtvcnJla3R1ciDDvGJlcm5vbW1lbi4iKQoKICAgIHBhcnRpYWwgPSBvdXRwdXQud2l0aF9uYW1lKG91dHB1dC5uYW1lICsgIi5wYXJ0aWFsIikKICAgIHRyeToKICAgICAgICBmb3IgaW5kZXgsIGlucHV0X3BhdGggaW4gZW51bWVyYXRlKGlucHV0cyk6CiAgICAgICAgICAgIGlucHV0X3BhdGggPSBQYXRoKGlucHV0X3BhdGgpCiAgICAgICAgICAgIGltYWdlX3BhdGggPSBpbnB1dF9wYXRoCiAgICAgICAgICAgIGNvcnJlY3RlZCA9IGlucHV0X3BhdGgud2l0aF9uYW1lKGlucHV0X3BhdGguc3RlbSArICIuY29ycmVjdGVkLnBuZyIpCiAgICAgICAgICAgIGlmIHByb2Nlc3NvcjoKICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICBwcm9jZXNzb3IoaW5wdXRfcGF0aCwgY29ycmVjdGVkLCBwcm9maWxlKQogICAgICAgICAgICAgICAgICAgIGltYWdlX3BhdGggPSBjb3JyZWN0ZWQKICAgICAgICAgICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgICAgICAgICAgaWYgc3RyaWN0OgogICAgICAgICAgICAgICAgICAgICAgICByYWlzZQogICAgICAgICAgICAgICAgICAgIHdhcm5pbmdzLmFwcGVuZChmIlNlaXRlIHtpbmRleCArIDF9OiBCaWxka29ycmVrdHVyIGZlaGxnZXNjaGxhZ2VuOyBSb2hzY2FuIMO8YmVybm9tbWVuLiIpCgogICAgICAgICAgICAjIE9ubHkgb25lIGRlY29kZWQgcGFnZSBpcyBrZXB0IGluIG1lbW9yeSwgZXZlbiBmb3IgbGFyZ2UgZHVwbGV4IHN0YWNrcy4KICAgICAgICAgICAgd2l0aCBJbWFnZS5vcGVuKGltYWdlX3BhdGgpIGFzIGltYWdlOgogICAgICAgICAgICAgICAgd2l0aCBpbWFnZS5jb252ZXJ0KCJSR0IiKSBhcyByZ2I6CiAgICAgICAgICAgICAgICAgICAgaWYgZmlsZV9mb3JtYXQgPT0gIlBERiI6CiAgICAgICAgICAgICAgICAgICAgICAgIHJnYi5zYXZlKHBhcnRpYWwsICJQREYiLCByZXNvbHV0aW9uPXJlc29sdXRpb24sIHF1YWxpdHk9OTAsIGFwcGVuZD1pbmRleCA+IDApCiAgICAgICAgICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgICAgICAgICAgcmdiLnNhdmUocGFydGlhbCwgZmlsZV9mb3JtYXQsIGRwaT0ocmVzb2x1dGlvbiwgcmVzb2x1dGlvbikpCiAgICAgICAgICAgIGNvcnJlY3RlZC51bmxpbmsobWlzc2luZ19vaz1UcnVlKQogICAgICAgIHBhcnRpYWwucmVwbGFjZShvdXRwdXQpCiAgICBmaW5hbGx5OgogICAgICAgIHBhcnRpYWwudW5saW5rKG1pc3Npbmdfb2s9VHJ1ZSkKCiAgICByZXR1cm4geyJwYWdlQ291bnQiOiBsZW4oaW5wdXRzKSwgIndhcm5pbmdzIjogd2FybmluZ3N9CgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPSJTY2FuLVNlaXRlbiB6dSBlaW5lciBEYXRlaSB6dXNhbW1lbmbDvGhyZW4iKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1pbnB1dHMiLCBuYXJncz0iKyIsIHJlcXVpcmVkPVRydWUpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLW91dHB1dCIsIHJlcXVpcmVkPVRydWUpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLXJlc29sdXRpb24iLCB0eXBlPWZsb2F0LCByZXF1aXJlZD1UcnVlKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1wcm9maWxlIiwgY2hvaWNlcz1bImRvY3VtZW50IiwgInJlY2VpcHQiLCAicmF3Il0sIGRlZmF1bHQ9ImRvY3VtZW50IikKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcG9zdHByb2Nlc3MiLCBhY3Rpb249InN0b3JlX3RydWUiKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1zdHJpY3QiLCBhY3Rpb249InN0b3JlX3RydWUiKQogICAgYXJncyA9IHBhcnNlci5wYXJzZV9hcmdzKCkKICAgIHByaW50KGpzb24uZHVtcHMoY3JlYXRlX2RvY3VtZW50KGFyZ3MuaW5wdXRzLCBhcmdzLm91dHB1dCwgYXJncy5yZXNvbHV0aW9uLCBhcmdzLnBvc3Rwcm9jZXNzLCBhcmdzLnByb2ZpbGUsIGFyZ3Muc3RyaWN0KSkpCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIG1haW4oKQo=", "scripts/opencv_postprocess.py": "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwppbXBvcnQgYXJncGFyc2UKaW1wb3J0IG1hdGgKZnJvbSBwYXRobGliIGltcG9ydCBQYXRoCgppbXBvcnQgY3YyCmltcG9ydCBudW1weSBhcyBucApmcm9tIFBJTCBpbXBvcnQgSW1hZ2UKCgpkZWYgb3JkZXJfcG9pbnRzKHBvaW50cyk6CiAgICByZWN0ID0gbnAuemVyb3MoKDQsIDIpLCBkdHlwZT0iZmxvYXQzMiIpCiAgICBwb2ludF9zdW0gPSBwb2ludHMuc3VtKGF4aXM9MSkKICAgIHBvaW50X2RpZmYgPSBucC5kaWZmKHBvaW50cywgYXhpcz0xKQoKICAgIHJlY3RbMF0gPSBwb2ludHNbbnAuYXJnbWluKHBvaW50X3N1bSldCiAgICByZWN0WzJdID0gcG9pbnRzW25wLmFyZ21heChwb2ludF9zdW0pXQogICAgcmVjdFsxXSA9IHBvaW50c1tucC5hcmdtaW4ocG9pbnRfZGlmZildCiAgICByZWN0WzNdID0gcG9pbnRzW25wLmFyZ21heChwb2ludF9kaWZmKV0KICAgIHJldHVybiByZWN0CgoKZGVmIGZvdXJfcG9pbnRfdHJhbnNmb3JtKGltYWdlLCBwb2ludHMpOgogICAgcmVjdCA9IG9yZGVyX3BvaW50cyhwb2ludHMpCiAgICB0b3BfbGVmdCwgdG9wX3JpZ2h0LCBib3R0b21fcmlnaHQsIGJvdHRvbV9sZWZ0ID0gcmVjdAoKICAgIHdpZHRoX2EgPSBucC5saW5hbGcubm9ybShib3R0b21fcmlnaHQgLSBib3R0b21fbGVmdCkKICAgIHdpZHRoX2IgPSBucC5saW5hbGcubm9ybSh0b3BfcmlnaHQgLSB0b3BfbGVmdCkKICAgIG1heF93aWR0aCA9IGludChtYXgod2lkdGhfYSwgd2lkdGhfYikpCgogICAgaGVpZ2h0X2EgPSBucC5saW5hbGcubm9ybSh0b3BfcmlnaHQgLSBib3R0b21fcmlnaHQpCiAgICBoZWlnaHRfYiA9IG5wLmxpbmFsZy5ub3JtKHRvcF9sZWZ0IC0gYm90dG9tX2xlZnQpCiAgICBtYXhfaGVpZ2h0ID0gaW50KG1heChoZWlnaHRfYSwgaGVpZ2h0X2IpKQoKICAgIGRlc3RpbmF0aW9uID0gbnAuYXJyYXkoWwogICAgICAgIFswLCAwXSwKICAgICAgICBbbWF4X3dpZHRoIC0gMSwgMF0sCiAgICAgICAgW21heF93aWR0aCAtIDEsIG1heF9oZWlnaHQgLSAxXSwKICAgICAgICBbMCwgbWF4X2hlaWdodCAtIDFdLAogICAgXSwgZHR5cGU9ImZsb2F0MzIiKQoKICAgIG1hdHJpeCA9IGN2Mi5nZXRQZXJzcGVjdGl2ZVRyYW5zZm9ybShyZWN0LCBkZXN0aW5hdGlvbikKICAgIHJldHVybiBjdjIud2FycFBlcnNwZWN0aXZlKGltYWdlLCBtYXRyaXgsIChtYXhfd2lkdGgsIG1heF9oZWlnaHQpLCBib3JkZXJWYWx1ZT0oMjU1LCAyNTUsIDI1NSkpCgoKZGVmIHJvdGF0ZV9ib3VuZChpbWFnZSwgYW5nbGUpOgogICAgaGVpZ2h0LCB3aWR0aCA9IGltYWdlLnNoYXBlWzoyXQogICAgY2VudGVyID0gKHdpZHRoIC8gMiwgaGVpZ2h0IC8gMikKICAgIG1hdHJpeCA9IGN2Mi5nZXRSb3RhdGlvbk1hdHJpeDJEKGNlbnRlciwgYW5nbGUsIDEuMCkKICAgIGNvcyA9IGFicyhtYXRyaXhbMCwgMF0pCiAgICBzaW4gPSBhYnMobWF0cml4WzAsIDFdKQoKICAgIG5ld193aWR0aCA9IGludCgoaGVpZ2h0ICogc2luKSArICh3aWR0aCAqIGNvcykpCiAgICBuZXdfaGVpZ2h0ID0gaW50KChoZWlnaHQgKiBjb3MpICsgKHdpZHRoICogc2luKSkKCiAgICBtYXRyaXhbMCwgMl0gKz0gKG5ld193aWR0aCAvIDIpIC0gY2VudGVyWzBdCiAgICBtYXRyaXhbMSwgMl0gKz0gKG5ld19oZWlnaHQgLyAyKSAtIGNlbnRlclsxXQoKICAgIHJldHVybiBjdjIud2FycEFmZmluZShpbWFnZSwgbWF0cml4LCAobmV3X3dpZHRoLCBuZXdfaGVpZ2h0KSwgYm9yZGVyVmFsdWU9KDI1NSwgMjU1LCAyNTUpKQoKCmRlZiBkZXNrZXdfYnlfdGV4dF9hbmdsZShpbWFnZSk6CiAgICBncmF5ID0gY3YyLmN2dENvbG9yKGltYWdlLCBjdjIuQ09MT1JfQkdSMkdSQVkpCiAgICBpbnZlcnRlZCA9IGN2Mi5iaXR3aXNlX25vdChncmF5KQogICAgdGhyZXNob2xkID0gY3YyLnRocmVzaG9sZChpbnZlcnRlZCwgMCwgMjU1LCBjdjIuVEhSRVNIX0JJTkFSWSB8IGN2Mi5USFJFU0hfT1RTVSlbMV0KICAgIGNvb3JkaW5hdGVzID0gbnAuY29sdW1uX3N0YWNrKG5wLndoZXJlKHRocmVzaG9sZCA+IDApKQoKICAgIGlmIGxlbihjb29yZGluYXRlcykgPCA1MDA6CiAgICAgICAgcmV0dXJuIGltYWdlCgogICAgYW5nbGUgPSBjdjIubWluQXJlYVJlY3QoY29vcmRpbmF0ZXMpWy0xXQogICAgaWYgYW5nbGUgPCAtNDU6CiAgICAgICAgYW5nbGUgPSAtKDkwICsgYW5nbGUpCiAgICBlbHNlOgogICAgICAgIGFuZ2xlID0gLWFuZ2xlCgogICAgaWYgYWJzKGFuZ2xlKSA8IDAuMiBvciBhYnMoYW5nbGUpID4gODoKICAgICAgICByZXR1cm4gaW1hZ2UKCiAgICByZXR1cm4gcm90YXRlX2JvdW5kKGltYWdlLCBhbmdsZSkKCgpkZWYgZmluZF9kb2N1bWVudF9jb250b3VyKGltYWdlLCBwcm9maWxlKToKICAgIHJhdGlvID0gaW1hZ2Uuc2hhcGVbMF0gLyA5MDAuMAogICAgcmVzaXplZCA9IGN2Mi5yZXNpemUoaW1hZ2UsIChpbnQoaW1hZ2Uuc2hhcGVbMV0gLyByYXRpbyksIDkwMCkpCiAgICBncmF5ID0gY3YyLmN2dENvbG9yKHJlc2l6ZWQsIGN2Mi5DT0xPUl9CR1IyR1JBWSkKICAgIGdyYXkgPSBjdjIuR2F1c3NpYW5CbHVyKGdyYXksICg1LCA1KSwgMCkKCiAgICBlZGdlcyA9IGN2Mi5DYW5ueShncmF5LCA0NSwgMTQwKQogICAga2VybmVsID0gY3YyLmdldFN0cnVjdHVyaW5nRWxlbWVudChjdjIuTU9SUEhfUkVDVCwgKDcsIDcpKQogICAgZWRnZXMgPSBjdjIubW9ycGhvbG9neUV4KGVkZ2VzLCBjdjIuTU9SUEhfQ0xPU0UsIGtlcm5lbCkKCiAgICBjb250b3VycywgXyA9IGN2Mi5maW5kQ29udG91cnMoZWRnZXMsIGN2Mi5SRVRSX0VYVEVSTkFMLCBjdjIuQ0hBSU5fQVBQUk9YX1NJTVBMRSkKICAgIGNvbnRvdXJzID0gc29ydGVkKGNvbnRvdXJzLCBrZXk9Y3YyLmNvbnRvdXJBcmVhLCByZXZlcnNlPVRydWUpWzo4XQoKICAgIG1pbl9hcmVhID0gcmVzaXplZC5zaGFwZVswXSAqIHJlc2l6ZWQuc2hhcGVbMV0gKiAoMC4wMyBpZiBwcm9maWxlID09ICJyZWNlaXB0IiBlbHNlIDAuMTIpCgogICAgZm9yIGNvbnRvdXIgaW4gY29udG91cnM6CiAgICAgICAgaWYgY3YyLmNvbnRvdXJBcmVhKGNvbnRvdXIpIDwgbWluX2FyZWE6CiAgICAgICAgICAgIGNvbnRpbnVlCgogICAgICAgIHBlcmltZXRlciA9IGN2Mi5hcmNMZW5ndGgoY29udG91ciwgVHJ1ZSkKICAgICAgICBhcHByb3ggPSBjdjIuYXBwcm94UG9seURQKGNvbnRvdXIsIDAuMDI1ICogcGVyaW1ldGVyLCBUcnVlKQogICAgICAgIGlmIGxlbihhcHByb3gpID09IDQ6CiAgICAgICAgICAgIHJldHVybiBhcHByb3gucmVzaGFwZSg0LCAyKSAqIHJhdGlvCgogICAgcmV0dXJuIE5vbmUKCgpkZWYgdHJpbV9saWdodF9ib3JkZXIoaW1hZ2UpOgogICAgZ3JheSA9IGN2Mi5jdnRDb2xvcihpbWFnZSwgY3YyLkNPTE9SX0JHUjJHUkFZKQogICAgbWFzayA9IGN2Mi50aHJlc2hvbGQoZ3JheSwgMjQ1LCAyNTUsIGN2Mi5USFJFU0hfQklOQVJZX0lOVilbMV0KICAgIGtlcm5lbCA9IGN2Mi5nZXRTdHJ1Y3R1cmluZ0VsZW1lbnQoY3YyLk1PUlBIX1JFQ1QsICg5LCA5KSkKICAgIG1hc2sgPSBjdjIubW9ycGhvbG9neUV4KG1hc2ssIGN2Mi5NT1JQSF9DTE9TRSwga2VybmVsKQoKICAgIGNvbnRvdXJzLCBfID0gY3YyLmZpbmRDb250b3VycyhtYXNrLCBjdjIuUkVUUl9FWFRFUk5BTCwgY3YyLkNIQUlOX0FQUFJPWF9TSU1QTEUpCiAgICBpZiBub3QgY29udG91cnM6CiAgICAgICAgcmV0dXJuIGltYWdlCgogICAgY29udG91ciA9IG1heChjb250b3Vycywga2V5PWN2Mi5jb250b3VyQXJlYSkKICAgIGlmIGN2Mi5jb250b3VyQXJlYShjb250b3VyKSA8IGltYWdlLnNoYXBlWzBdICogaW1hZ2Uuc2hhcGVbMV0gKiAwLjAyOgogICAgICAgIHJldHVybiBpbWFnZQoKICAgIHgsIHksIHdpZHRoLCBoZWlnaHQgPSBjdjIuYm91bmRpbmdSZWN0KGNvbnRvdXIpCiAgICBwYWRkaW5nID0gbWF4KDEyLCBpbnQobWluKHdpZHRoLCBoZWlnaHQpICogMC4wMjUpKQogICAgeCA9IG1heCgwLCB4IC0gcGFkZGluZykKICAgIHkgPSBtYXgoMCwgeSAtIHBhZGRpbmcpCiAgICB3aWR0aCA9IG1pbihpbWFnZS5zaGFwZVsxXSAtIHgsIHdpZHRoICsgcGFkZGluZyAqIDIpCiAgICBoZWlnaHQgPSBtaW4oaW1hZ2Uuc2hhcGVbMF0gLSB5LCBoZWlnaHQgKyBwYWRkaW5nICogMikKICAgIHJldHVybiBpbWFnZVt5OnkgKyBoZWlnaHQsIHg6eCArIHdpZHRoXQoKCmRlZiBlbmhhbmNlX3JlY2VpcHQoaW1hZ2UpOgogICAgZ3JheSA9IGN2Mi5jdnRDb2xvcihpbWFnZSwgY3YyLkNPTE9SX0JHUjJHUkFZKQogICAgY2xhaGUgPSBjdjIuY3JlYXRlQ0xBSEUoY2xpcExpbWl0PTIuMCwgdGlsZUdyaWRTaXplPSg4LCA4KSkKICAgIGdyYXkgPSBjbGFoZS5hcHBseShncmF5KQogICAgZ3JheSA9IGN2Mi5mYXN0TmxNZWFuc0Rlbm9pc2luZyhncmF5LCBOb25lLCA4LCA3LCAyMSkKICAgIGdyYXkgPSBjdjIubm9ybWFsaXplKGdyYXksIE5vbmUsIDAsIDI1NSwgY3YyLk5PUk1fTUlOTUFYKQogICAgcmV0dXJuIGN2Mi5jdnRDb2xvcihncmF5LCBjdjIuQ09MT1JfR1JBWTJCR1IpCgoKZGVmIGVuaGFuY2VfZG9jdW1lbnQoaW1hZ2UpOgogICAgbGFiID0gY3YyLmN2dENvbG9yKGltYWdlLCBjdjIuQ09MT1JfQkdSMkxBQikKICAgIGxfY2hhbm5lbCwgYV9jaGFubmVsLCBiX2NoYW5uZWwgPSBjdjIuc3BsaXQobGFiKQogICAgY2xhaGUgPSBjdjIuY3JlYXRlQ0xBSEUoY2xpcExpbWl0PTEuNiwgdGlsZUdyaWRTaXplPSg4LCA4KSkKICAgIGxfY2hhbm5lbCA9IGNsYWhlLmFwcGx5KGxfY2hhbm5lbCkKICAgIHJldHVybiBjdjIuY3Z0Q29sb3IoY3YyLm1lcmdlKChsX2NoYW5uZWwsIGFfY2hhbm5lbCwgYl9jaGFubmVsKSksIGN2Mi5DT0xPUl9MQUIyQkdSKQoKCmRlZiBhdXRvX3JvdGF0ZV9wcm9maWxlKGltYWdlLCBwcm9maWxlKToKICAgIGhlaWdodCwgd2lkdGggPSBpbWFnZS5zaGFwZVs6Ml0KCiAgICBpZiBwcm9maWxlID09ICJyZWNlaXB0IiBhbmQgd2lkdGggPiBoZWlnaHQ6CiAgICAgICAgcmV0dXJuIGN2Mi5yb3RhdGUoaW1hZ2UsIGN2Mi5ST1RBVEVfOTBfQ0xPQ0tXSVNFKQoKICAgIHJldHVybiBpbWFnZQoKCmRlZiBwb3N0cHJvY2VzcyhpbnB1dF9wYXRoLCBvdXRwdXRfcGF0aCwgcHJvZmlsZSk6CiAgICBpbWFnZSA9IGN2Mi5pbXJlYWQoc3RyKGlucHV0X3BhdGgpLCBjdjIuSU1SRUFEX0NPTE9SKQogICAgaWYgaW1hZ2UgaXMgTm9uZToKICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoZiJPcGVuQ1Yga29ubnRlIHtpbnB1dF9wYXRofSBuaWNodCBsZXNlbiIpCgogICAgY29udG91ciA9IGZpbmRfZG9jdW1lbnRfY29udG91cihpbWFnZSwgcHJvZmlsZSkKICAgIGlmIGNvbnRvdXIgaXMgbm90IE5vbmU6CiAgICAgICAgcHJvY2Vzc2VkID0gZm91cl9wb2ludF90cmFuc2Zvcm0oaW1hZ2UsIGNvbnRvdXIuYXN0eXBlKCJmbG9hdDMyIikpCiAgICBlbHNlOgogICAgICAgIHByb2Nlc3NlZCA9IHRyaW1fbGlnaHRfYm9yZGVyKGltYWdlKQoKICAgIHByb2Nlc3NlZCA9IGRlc2tld19ieV90ZXh0X2FuZ2xlKHByb2Nlc3NlZCkKICAgIHByb2Nlc3NlZCA9IHRyaW1fbGlnaHRfYm9yZGVyKHByb2Nlc3NlZCkKICAgIHByb2Nlc3NlZCA9IGF1dG9fcm90YXRlX3Byb2ZpbGUocHJvY2Vzc2VkLCBwcm9maWxlKQoKICAgIGlmIHByb2ZpbGUgPT0gInJlY2VpcHQiOgogICAgICAgIHByb2Nlc3NlZCA9IGVuaGFuY2VfcmVjZWlwdChwcm9jZXNzZWQpCiAgICBlbGlmIHByb2ZpbGUgIT0gInJhdyI6CiAgICAgICAgcHJvY2Vzc2VkID0gZW5oYW5jZV9kb2N1bWVudChwcm9jZXNzZWQpCgogICAgc2F2ZV9vdXRwdXQocHJvY2Vzc2VkLCBvdXRwdXRfcGF0aCkKCgpkZWYgc2F2ZV9vdXRwdXQoaW1hZ2UsIG91dHB1dF9wYXRoKToKICAgIHN1ZmZpeCA9IG91dHB1dF9wYXRoLnN1ZmZpeC5sb3dlcigpCgogICAgaWYgc3VmZml4ID09ICIucGRmIjoKICAgICAgICByZ2IgPSBjdjIuY3Z0Q29sb3IoaW1hZ2UsIGN2Mi5DT0xPUl9CR1IyUkdCKQogICAgICAgIHBpbF9pbWFnZSA9IEltYWdlLmZyb21hcnJheShyZ2IpCiAgICAgICAgaWYgcGlsX2ltYWdlLm1vZGUgIT0gIlJHQiI6CiAgICAgICAgICAgIHBpbF9pbWFnZSA9IHBpbF9pbWFnZS5jb252ZXJ0KCJSR0IiKQogICAgICAgIHBpbF9pbWFnZS5zYXZlKG91dHB1dF9wYXRoLCAiUERGIiwgcmVzb2x1dGlvbj0zMDAuMCkKICAgICAgICByZXR1cm4KCiAgICBpZiBzdWZmaXggaW4geyIuanBnIiwgIi5qcGVnIn06CiAgICAgICAgY3YyLmltd3JpdGUoc3RyKG91dHB1dF9wYXRoKSwgaW1hZ2UsIFtjdjIuSU1XUklURV9KUEVHX1FVQUxJVFksIDkyXSkKICAgICAgICByZXR1cm4KCiAgICBpZiBzdWZmaXggPT0gIi5wbmciOgogICAgICAgIGN2Mi5pbXdyaXRlKHN0cihvdXRwdXRfcGF0aCksIGltYWdlLCBbY3YyLklNV1JJVEVfUE5HX0NPTVBSRVNTSU9OLCAzXSkKICAgICAgICByZXR1cm4KCiAgICBpZiBzdWZmaXggaW4geyIudGlmIiwgIi50aWZmIn06CiAgICAgICAgY3YyLmltd3JpdGUoc3RyKG91dHB1dF9wYXRoKSwgaW1hZ2UpCiAgICAgICAgcmV0dXJuCgogICAgcmFpc2UgUnVudGltZUVycm9yKGYiTmljaHQgdW50ZXJzdMO8dHp0ZXMgQXVzZ2FiZWZvcm1hdDoge3N1ZmZpeH0iKQoKCmRlZiBtYWluKCk6CiAgICBwYXJzZXIgPSBhcmdwYXJzZS5Bcmd1bWVudFBhcnNlcihkZXNjcmlwdGlvbj0iRkVERU8gU2Nhbi1OYWNoYmVhcmJlaXR1bmcgbWl0IE9wZW5DViIpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLWlucHV0IiwgcmVxdWlyZWQ9VHJ1ZSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tb3V0cHV0IiwgcmVxdWlyZWQ9VHJ1ZSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcHJvZmlsZSIsIGRlZmF1bHQ9ImRvY3VtZW50IiwgY2hvaWNlcz1bImRvY3VtZW50IiwgInJlY2VpcHQiLCAicmF3Il0pCiAgICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3MoKQoKICAgIHBvc3Rwcm9jZXNzKFBhdGgoYXJncy5pbnB1dCksIFBhdGgoYXJncy5vdXRwdXQpLCBhcmdzLnByb2ZpbGUpCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIG1haW4oKQo=", -- 2.49.1 From a65bd72d740553e5554217dafaa37fbbd973ddfb Mon Sep 17 00:00:00 2001 From: flfeders Date: Thu, 24 Sep 2026 17:41:30 +0200 Subject: [PATCH 3/5] feat(scan): whiten receipt backgrounds and embed OCR in PDFs --- agents/fedeo-device-agent/.env.example | 3 + agents/fedeo-device-agent/Dockerfile | 6 + agents/fedeo-device-agent/README.md | 34 ++++- .../scripts/build-installer.mjs | 2 +- .../scripts/install-linux.sh | 13 +- .../scripts/scan_background.py | 74 +++++++++ .../scripts/scan_document.py | 142 ++++++++++++++---- agents/fedeo-device-agent/src/config.ts | 3 + agents/fedeo-device-agent/src/main.ts | 3 + .../src/scan/postprocess.ts | 34 ++++- agents/fedeo-device-agent/src/scan/sane.ts | 11 +- agents/fedeo-device-agent/src/types.ts | 5 + .../tests/installer.test.mjs | 4 + agents/fedeo-device-agent/tests/scan.test.ts | 13 ++ .../tests/scan_background_test.py | 64 ++++++++ .../fedeo-device-agent/tests/scan_ocr_test.py | 71 +++++++++ backend/src/routes/instanceAgents.ts | 14 +- backend/tests/instanceAgentSetup.test.ts | 27 ++++ frontend/assets/device-agent/bundle.json | 13 +- frontend/components/AgentSetupWizard.vue | 20 ++- frontend/components/FileScanModal.vue | 32 +++- frontend/composables/useAdmin.ts | 3 + frontend/pages/administration/scanners.vue | 27 +++- frontend/tests/agent-setup.test.cjs | 14 +- frontend/tests/file-scan.test.cjs | 14 +- frontend/utils/agentInstaller.mjs | 2 + 26 files changed, 572 insertions(+), 76 deletions(-) create mode 100644 agents/fedeo-device-agent/scripts/scan_background.py create mode 100644 agents/fedeo-device-agent/tests/scan_background_test.py create mode 100644 agents/fedeo-device-agent/tests/scan_ocr_test.py diff --git a/agents/fedeo-device-agent/.env.example b/agents/fedeo-device-agent/.env.example index cb905369..0d9f363d 100644 --- a/agents/fedeo-device-agent/.env.example +++ b/agents/fedeo-device-agent/.env.example @@ -10,6 +10,9 @@ FEDEO_SCAN_MODE=Color FEDEO_SCAN_SOURCE= FEDEO_SCAN_TIMEOUT_SECONDS=900 FEDEO_SCAN_POSTPROCESS=false +FEDEO_SCAN_BACKGROUND_CLEANUP=true +FEDEO_SCAN_OCR=true +FEDEO_SCAN_OCR_LANGUAGE=deu+eng FEDEO_SCAN_POSTPROCESS_PROFILE=document FEDEO_SCAN_POSTPROCESS_PYTHON= FEDEO_SCAN_POSTPROCESS_STRICT=false diff --git a/agents/fedeo-device-agent/Dockerfile b/agents/fedeo-device-agent/Dockerfile index d86e8402..fca344c7 100644 --- a/agents/fedeo-device-agent/Dockerfile +++ b/agents/fedeo-device-agent/Dockerfile @@ -13,6 +13,9 @@ FROM node:20-bookworm-slim AS runtime ENV NODE_ENV=production ENV FEDEO_WORK_DIR=/work ENV FEDEO_SCAN_POSTPROCESS=false +ENV FEDEO_SCAN_BACKGROUND_CLEANUP=true +ENV FEDEO_SCAN_OCR=true +ENV FEDEO_SCAN_OCR_LANGUAGE=deu+eng ENV FEDEO_SCAN_POSTPROCESS_PROFILE=document ENV FEDEO_SCAN_POSTPROCESS_PYTHON=/opt/fedeo-device-agent/.venv-opencv/bin/python ENV FEDEO_SCAN_POSTPROCESS_STRICT=false @@ -28,6 +31,9 @@ RUN apt-get update \ python3-pip \ python3-venv \ sane-utils \ + tesseract-ocr \ + tesseract-ocr-deu \ + tesseract-ocr-eng \ && rm -rf /var/lib/apt/lists/* COPY requirements-opencv.txt ./ diff --git a/agents/fedeo-device-agent/README.md b/agents/fedeo-device-agent/README.md index 94a03673..3af82718 100644 --- a/agents/fedeo-device-agent/README.md +++ b/agents/fedeo-device-agent/README.md @@ -44,11 +44,11 @@ FEDEO_TEST_POSTGRES_IMAGE=postgres:16-alpine node --import tsx tests/instanceAge ### macOS ```bash -brew install node sane-backends +brew install node sane-backends tesseract tesseract-lang scanimage -L ``` -Für die Dateierstellung wird Python mit Pillow benötigt, beispielsweise über `npm run setup:opencv` (siehe unten). +Für die Dateierstellung und Hintergrundbereinigung wird Python mit Pillow und OpenCV benötigt, beispielsweise über `npm run setup:opencv` (siehe unten). `tesseract --list-langs` muss für die Standardkonfiguration `deu` und `eng` enthalten. Drucken nutzt später das macOS-Drucksystem/CUPS: @@ -60,7 +60,7 @@ lpstat -p ```bash sudo apt update -sudo apt install -y nodejs npm sane-utils python3-pil +sudo apt install -y nodejs npm sane-utils python3-pil python3-opencv tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng scanimage -L ``` @@ -90,6 +90,9 @@ Der iX1600 wird vom SANE-Backend `fujitsu` per USB unterstützt. Die USB-ID ist FEDEO_SCAN_RESOLUTION=300 FEDEO_SCAN_MODE=Color FEDEO_SCAN_POSTPROCESS=false + FEDEO_SCAN_BACKGROUND_CLEANUP=true + FEDEO_SCAN_OCR=true + FEDEO_SCAN_OCR_LANGUAGE=deu+eng FEDEO_SCAN_TIMEOUT_SECONDS=900 ``` @@ -139,7 +142,24 @@ npm run build:installer `npm test` prüft mit `check:installer`, dass das Paket dem aktuellen Quellcode entspricht. Das Paket enthält keine Zugangsdaten; diese ergänzt erst der Browser beim Download. Die Installer-Tests verwenden temporäre Verzeichnisse und simulierte Systembefehle. -## OpenCV-Nachbearbeitung +## Scannerhintergrund und OCR + +Hintergrundbereinigung und OCR sind standardmäßig aktiv und lassen sich in **Administration → Scanner** als Vorgabe oder im einzelnen Scan-Dialog einstellen: + +- **Scannerhintergrund weiß machen:** Bei einem kleinen Bon oder Dokument werden sicher erkannte Flächen außerhalb des Papiers weiß. Seitengröße, Position und Inhalt des Papiers bleiben erhalten. Unsichere Papiergrenzen werden nicht verändert. Diese Verarbeitung ist unabhängig vom zusätzlichen Zuschnitt. +- **Durchsuchbares PDF (OCR):** Der Agent erkennt nach der Bildverarbeitung Text mit Tesseract und bettet ihn direkt in das PDF ein, bevor er es hochlädt. Das gilt auch für normale Dateien ohne Eingangsbeleg. Deutsch und Englisch sind vorausgewählt; PNG und TIFF erhalten keine OCR-Textschicht. Die Darstellung bleibt das gescannte Bild, ergänzt um auswählbaren und durchsuchbaren Text ([Tesseract-Dokumentation](https://tesseract-ocr.github.io/tessdoc/Command-Line-Usage.html)). + +```env +FEDEO_SCAN_BACKGROUND_CLEANUP=true +FEDEO_SCAN_OCR=true +FEDEO_SCAN_OCR_LANGUAGE=deu+eng +``` + +Nach einem Update von FEDEO bestehende Agenten über **Einrichtung öffnen** mit dem neuen Linux-Installer aktualisieren. Dieser installiert auch OpenCV, Tesseract und die Sprachpakete. Bei manueller Installation die oben genannten Pakete und die aktuelle Agent-Software installieren und den Dienst neu starten. Der Agent meldet die verfügbare Verarbeitung beim Start an FEDEO; fehlende Fähigkeiten werden vor dem Auftrag gemeldet. Zusätzlich prüft er die benötigten Pakete und die gewählte OCR-Sprache vor jedem Papiereinzug. Ein OCR-Fehler erzeugt keinen erfolgreichen Scan ohne Textschicht; die Rohseiten bleiben zur Wiederherstellung erhalten. + +Die OCR läuft innerhalb des Scan-Auftrags nach dem Einzug. Bei langen Stapeln kann dieser Schritt länger dauern; das Zeitlimit für die Dateierstellung wird ebenfalls über `FEDEO_SCAN_TIMEOUT_SECONDS` gesetzt. Leere Rückseiten und Seitenreihenfolge bleiben erhalten. + +## Zusätzliche OpenCV-Nachbearbeitung Für automatischen Zuschnitt, leichte Entzerrung, Rotation und Kontrastkorrektur kann die OpenCV-Pipeline aktiviert werden. @@ -156,7 +176,7 @@ FEDEO_SCAN_POSTPROCESS_PYTHON=/pfad/zum/agent/.venv-opencv/bin/python FEDEO_SCAN_POSTPROCESS_STRICT=false ``` -Wenn `FEDEO_SCAN_POSTPROCESS_PYTHON` leer bleibt, verwendet der Agent automatisch `.venv-opencv/bin/python`, sofern diese Umgebung existiert, sonst `python3`. Pillow ist auch ohne Bildkorrektur für die Dateierstellung erforderlich. Falls OpenCV nicht installiert ist und `FEDEO_SCAN_POSTPROCESS_STRICT=false` gesetzt ist, werden die Rohseiten im gewünschten Ausgabeformat zusammengeführt. Eine fehlgeschlagene Korrektur verliert keine Seiten. Die Warnung erscheint im Scan-Ergebnis. Beim iX1600-Profil ist die Bildkorrektur zunächst ausgeschaltet, damit der vollständige A4-Scan erhalten bleibt. +Wenn `FEDEO_SCAN_POSTPROCESS_PYTHON` leer bleibt, verwendet der Agent automatisch `.venv-opencv/bin/python`, sofern diese Umgebung existiert, sonst `python3`. Pillow ist auch ohne Bildkorrektur für die Dateierstellung erforderlich. Falls die zusätzliche Korrektur fehlschlägt und `FEDEO_SCAN_POSTPROCESS_STRICT=false` gesetzt ist, werden die betroffenen Rohseiten weiterverarbeitet; die Warnung erscheint im Scan-Ergebnis. Fehlendes OpenCV kann nur bei ausgeschalteter Hintergrundbereinigung toleriert werden. Beim iX1600-Profil ist der zusätzliche Zuschnitt ausgeschaltet, damit der vollständige A4-Scan erhalten bleibt; Hintergrundbereinigung und OCR sind eingeschaltet. Profile: @@ -166,7 +186,7 @@ Profile: ## Container-Betrieb -Auf Linux und Raspberry Pi OS kann der Agent komplett im Container laufen. Dadurch bleiben Node.js, Python, OpenCV und SANE im Image. Auf dem Host werden dann nur Docker und Zugriff auf den USB-Scanner benötigt. +Auf Linux und Raspberry Pi OS kann der Agent komplett im Container laufen. Dadurch bleiben Node.js, Python, OpenCV, Tesseract einschließlich Deutsch/Englisch und SANE im Image. Auf dem Host werden dann nur Docker und Zugriff auf den USB-Scanner benötigt. ```bash cp .env.example .env @@ -202,7 +222,7 @@ npm test npm run test:python ``` -Die Tests benötigen Python 3 mit Pillow. Sie simulieren `scanimage`, erstellen echte PDFs und prüfen Duplex-/Simplex-Stapel, Seitenreihenfolge, Auflösung, leeren Einzug, Papierstau, beschädigte Seiten, Dateinamen und OpenCV-Ausfälle. Ein Test mit angeschlossenem iX1600 bleibt zusätzlich erforderlich. +Die Tests benötigen Python 3 mit Pillow, NumPy und OpenCV. Für den OCR-Integrationstest sind Tesseract mit `deu`/`eng` und `pdftotext` aus Poppler erforderlich; ohne diese Programme wird er übersprungen. Bei Verwendung der virtuellen Umgebung zuerst `. .venv-opencv/bin/activate` ausführen. Die Tests simulieren `scanimage`, erstellen echte PDFs und prüfen Duplex-/Simplex-Stapel, Seitenreihenfolge, Auflösung, leeren Einzug, Papierstau, beschädigte Seiten, Dateinamen und OpenCV-Ausfälle. Bildtests prüfen die Hintergrundbereinigung bei geraden und schrägen Bons sowie den Erhalt von Text, farbigen Logos und vollständigen Seiten. Der OCR-Test liest die tatsächliche PDF-Textschicht einschließlich leerer Rückseiten aus. Ein Test mit angeschlossenem iX1600 bleibt zusätzlich erforderlich. Hardwareprüfung am 17.09.2026: ScanSnap iX1600 per USB unter Linux Mint 22.3 mit SANE 1.2.1. Zwei A4-Blätter wurden mit 300 dpi in Farbe beidseitig gescannt; das erzeugte PDF enthält vier A4-Seiten in der Reihenfolge Vorderseite/Rückseite. Alle vier Seiten wurden visuell geprüft. Der Test umfasste den lokalen Agenten bis zur PDF-Erstellung; der FEDEO-Upload wird separat mit einem simulierten Dateispeicher getestet. diff --git a/agents/fedeo-device-agent/scripts/build-installer.mjs b/agents/fedeo-device-agent/scripts/build-installer.mjs index 26b888c8..92ebc12b 100644 --- a/agents/fedeo-device-agent/scripts/build-installer.mjs +++ b/agents/fedeo-device-agent/scripts/build-installer.mjs @@ -14,7 +14,7 @@ async function filesUnder(directory) { const packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8')) const paths = [ ...(await filesUnder('dist')).filter(file => file.endsWith('.js')), - 'scripts/scan_document.py', 'scripts/opencv_postprocess.py', 'scripts/setup-opencv.sh', + 'scripts/scan_document.py', 'scripts/scan_background.py', 'scripts/opencv_postprocess.py', 'scripts/setup-opencv.sh', 'requirements-opencv.txt', ] const files = Object.fromEntries(await Promise.all(paths.map(async file => [file, (await readFile(path.join(root, file))).toString('base64')]))) diff --git a/agents/fedeo-device-agent/scripts/install-linux.sh b/agents/fedeo-device-agent/scripts/install-linux.sh index 10f80445..fec53ad2 100644 --- a/agents/fedeo-device-agent/scripts/install-linux.sh +++ b/agents/fedeo-device-agent/scripts/install-linux.sh @@ -25,17 +25,22 @@ FEDEO_SETUP_UNIT="fedeo-device-agent-$FEDEO_SETUP_ID.service" FEDEO_SETUP_UNITS="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" echo 'FEDEO Scan-Agent wird für diesen Benutzer eingerichtet.' -if ! command -v node >/dev/null || ! command -v scanimage >/dev/null || ! python3 -c 'import PIL' >/dev/null 2>&1; then +fedeo_processing_ready() { + python3 -c 'import PIL, cv2, numpy' >/dev/null 2>&1 && + tesseract --list-langs 2>/dev/null | grep -qx deu && + tesseract --list-langs 2>/dev/null | grep -qx eng +} +if ! command -v node >/dev/null || ! command -v scanimage >/dev/null || ! fedeo_processing_ready; then if ! command -v apt-get >/dev/null; then - echo 'Bitte Node.js >= 20, SANE (scanimage), Python 3 und Pillow installieren und das Skript erneut starten.' >&2 + echo 'Bitte Node.js >= 20, SANE, Python/Pillow/OpenCV sowie Tesseract mit Deutsch/Englisch installieren und das Skript erneut starten.' >&2 exit 1 fi echo 'Fehlende Systempakete werden über apt installiert (sudo benötigt).' sudo apt-get update - sudo apt-get install -y nodejs sane-utils python3-pil + sudo apt-get install -y nodejs sane-utils python3-pil python3-opencv tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng fi node -e 'if (Number(process.versions.node.split(".")[0]) < 20) { console.error("Node.js >= 20 wird benötigt. Bitte Node.js aktualisieren und die Installation erneut starten."); process.exit(1) }' -python3 -c 'import PIL' +fedeo_processing_ready || { echo 'Die benötigte Bildverarbeitung oder die OCR-Sprachpakete fehlen weiterhin.' >&2; exit 1; } FEDEO_SETUP_NODE="$(command -v node)" FEDEO_SETUP_HAS_SYSTEMD=false diff --git a/agents/fedeo-device-agent/scripts/scan_background.py b/agents/fedeo-device-agent/scripts/scan_background.py new file mode 100644 index 00000000..2477ec51 --- /dev/null +++ b/agents/fedeo-device-agent/scripts/scan_background.py @@ -0,0 +1,74 @@ +"""Conservative paper masking. Keep the canvas and every pixel inside the paper.""" +from pathlib import Path + +import cv2 +import numpy as np + + +def whiten_background(image): + height, width = image.shape[:2] + scale = min(1.0, 1000.0 / max(height, width)) + small = cv2.resize(image, None, fx=scale, fy=scale, interpolation=cv2.INTER_AREA) + gray = cv2.cvtColor(small, cv2.COLOR_BGR2GRAY) + border = np.concatenate((gray[0], gray[-1], gray[:, 0], gray[:, -1])) + background = float(np.median(border)) + # White surroundings already need no correction. Never search inside a + # white document for an isolated text box or logo to keep instead. + if background >= 235 or np.percentile(border, 75) - np.percentile(border, 25) > 55: + return image + + threshold, _ = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + mask = np.uint8(gray > max(threshold, background + 20)) * 255 + mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8)) + contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + candidates = [] + area = gray.size + for contour in contours: + contour_area = cv2.contourArea(contour) + if not 0.015 * area < contour_area < 0.985 * area: + continue + perimeter = cv2.arcLength(contour, True) + polygon = cv2.approxPolyDP(contour, 0.018 * perimeter, True) + if len(polygon) != 4 or not cv2.isContourConvex(polygon): + continue + # Use the quadrilateral only to recognize a sheet, then keep its full + # contour hull so rounded or slightly uneven edges cannot lose content. + polygon = cv2.convexHull(contour) + paper = np.zeros_like(gray) + cv2.fillPoly(paper, [polygon], 255) + inside = cv2.erode(paper, np.ones((7, 7), np.uint8)) > 0 + ring = (cv2.dilate(paper, np.ones((9, 9), np.uint8)) > 0) & (paper == 0) + if inside.sum() < 100 or ring.sum() < 50: + continue + paper_light = float(np.median(gray[inside])) + # A paper edge must separate a mostly light sheet from darker scanner + # backing all around it. Ambiguous photos/pages are left untouched. + if paper_light < background + 30 or np.mean(gray[ring] < paper_light - 20) < 0.85: + continue + candidates.append(polygon) + + if not candidates: + return image + keep_small = np.zeros_like(gray) + cv2.fillPoly(keep_small, candidates, 255) + # Do not erase another significant object that we could not classify. + outside = (keep_small == 0) + if np.mean(gray[outside] > background + 30) > 0.04: + return image + keep = np.zeros((height, width), dtype=np.uint8) + polygons = [np.rint(polygon / scale).astype(np.int32) for polygon in candidates] + cv2.fillPoly(keep, polygons, 255) + # Rounding/downsampling uncertainty grows with the original scan DPI. + padding = max(2, int(np.ceil(2 / scale))) + keep = cv2.dilate(keep, np.ones((2 * padding + 1, 2 * padding + 1), np.uint8)) + result = image.copy() + result[keep == 0] = 255 + return result + + +def clean_scan_background(input_path: Path, output_path: Path): + image = cv2.imread(str(input_path), cv2.IMREAD_COLOR) + if image is None: + raise RuntimeError("Scan-Bild konnte nicht gelesen werden") + if not cv2.imwrite(str(output_path), whiten_background(image)): + raise RuntimeError("Bereinigtes Scan-Bild konnte nicht gespeichert werden") diff --git a/agents/fedeo-device-agent/scripts/scan_document.py b/agents/fedeo-device-agent/scripts/scan_document.py index a6759984..a5215f2b 100644 --- a/agents/fedeo-device-agent/scripts/scan_document.py +++ b/agents/fedeo-device-agent/scripts/scan_document.py @@ -1,21 +1,56 @@ #!/usr/bin/env python3 -"""Assemble ordered scanner pages; OpenCV is optional, PDF creation is not.""" +"""Prepare ordered scanner pages and publish an optionally searchable PDF atomically.""" import argparse import json +import math from pathlib import Path +import re +import signal +import subprocess +import tempfile -from PIL import Image +from PIL import Image, PdfParser -def create_document(inputs, output, resolution, use_postprocess=False, profile="document", strict=False): +def run_ocr_command(arguments): + process = subprocess.Popen(arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + try: + stdout, stderr = process.communicate() + except BaseException: + process.kill() + process.wait() + raise + if process.returncode != 0: + raise RuntimeError("OCR fehlgeschlagen: " + stderr.strip()) + return stdout + + +def check_ocr(language): + if not re.fullmatch(r"[a-zA-Z0-9_]+(?:\+[a-zA-Z0-9_]+)*", language): + raise ValueError("Ungültige OCR-Sprache") + try: + languages = run_ocr_command(["tesseract", "--list-langs"]).splitlines() + except FileNotFoundError as error: + raise RuntimeError("Tesseract fehlt. Bitte tesseract-ocr und die Sprachpakete tesseract-ocr-deu/tesseract-ocr-eng installieren.") from error + missing = set(language.split("+")) - {line.strip() for line in languages} + if missing: + raise RuntimeError("OCR-Sprachpakete fehlen: " + ", ".join(sorted(missing))) + + +def create_document(inputs, output, resolution, use_postprocess=False, profile="document", strict=False, + ocr=False, ocr_language="deu+eng", background_cleanup=False): if not inputs: raise ValueError("Keine Seiten vorhanden") + if not math.isfinite(resolution) or resolution <= 0: + raise ValueError("Ungültige Scan-Auflösung") output = Path(output) file_format = {".pdf": "PDF", ".png": "PNG", ".tiff": "TIFF"}.get(output.suffix.lower()) if not file_format: raise ValueError("Unbekanntes Ausgabeformat") - if file_format != "PDF" and len(inputs) != 1: - raise ValueError("Mehrseitige Scans benötigen PDF als Ausgabeformat") + if (len(inputs) != 1 or ocr) and file_format != "PDF": + raise ValueError("Mehrseitige Scans und OCR benötigen PDF als Ausgabeformat") + if ocr: + check_ocr(ocr_language) warnings = [] processor = None @@ -27,47 +62,92 @@ def create_document(inputs, output, resolution, use_postprocess=False, profile=" if strict: raise warnings.append("OpenCV fehlt; alle Seiten wurden ohne Bildkorrektur übernommen.") + if background_cleanup: + from scan_background import clean_scan_background partial = output.with_name(output.name + ".partial") try: - for index, input_path in enumerate(inputs): - input_path = Path(input_path) - image_path = input_path - corrected = input_path.with_name(input_path.stem + ".corrected.png") - if processor: - try: - processor(input_path, corrected, profile) - image_path = corrected - except Exception: - if strict: - raise - warnings.append(f"Seite {index + 1}: Bildkorrektur fehlgeschlagen; Rohscan übernommen.") + with tempfile.TemporaryDirectory(prefix="prepared-", dir=output.parent) as directory: + prepared = [] + for index, input_path in enumerate(inputs): + image_path = Path(input_path) + corrected = Path(directory) / f"corrected-{index:06}.png" + if processor: + try: + processor(image_path, corrected, profile) + image_path = corrected + except Exception: + if strict: + raise + warnings.append(f"Seite {index + 1}: Bildkorrektur fehlgeschlagen; Rohscan übernommen.") + if background_cleanup: + cleaned = Path(directory) / f"cleaned-{index:06}.png" + clean_scan_background(image_path, cleaned) + image_path = cleaned - # Only one decoded page is kept in memory, even for large duplex stacks. - with Image.open(image_path) as image: - with image.convert("RGB") as rgb: - if file_format == "PDF": - rgb.save(partial, "PDF", resolution=resolution, quality=90, append=index > 0) - else: - rgb.save(partial, file_format, dpi=(resolution, resolution)) - corrected.unlink(missing_ok=True) - partial.replace(output) + # Only one decoded page is kept in memory, even for duplex stacks. + with Image.open(image_path) as image: + with image.convert("RGB") as rgb: + if ocr: + page = Path(directory).resolve() / f"page-{index:06}.png" + rgb.save(page, "PNG", dpi=(resolution, resolution)) + prepared.append(page) + elif file_format == "PDF": + rgb.save(partial, "PDF", resolution=resolution, quality=90, append=index > 0) + else: + rgb.save(partial, file_format, dpi=(resolution, resolution)) + corrected.unlink(missing_ok=True) + if background_cleanup: + cleaned.unlink(missing_ok=True) + if ocr: + page_list = Path(directory) / "pages.txt" + if any("\n" in str(page) or "\r" in str(page) for page in prepared): + raise ValueError("Das Scan-Arbeitsverzeichnis darf keinen Zeilenumbruch enthalten") + page_list.write_text("\n".join(map(str, prepared)) + "\n", encoding="utf8") + prefix = Path(directory) / "searchable" + run_ocr_command(["tesseract", str(page_list), str(prefix), "-l", ocr_language, + "--dpi", str(round(resolution)), "pdf"]) + searchable = prefix.with_suffix(".pdf") + with PdfParser.PdfParser(filename=str(searchable)) as pdf: + if len(pdf.pages) != len(inputs): + raise RuntimeError("OCR-PDF ist unvollständig; Rohscans bleiben erhalten") + searchable.replace(partial) + partial.replace(output) finally: partial.unlink(missing_ok=True) - return {"pageCount": len(inputs), "warnings": warnings} +def interrupted(*_): + raise SystemExit("Scan-Verarbeitung abgebrochen") + + def main(): + # Let finally blocks stop Tesseract and remove incomplete PDFs on agent timeout. + signal.signal(signal.SIGTERM, interrupted) parser = argparse.ArgumentParser(description="Scan-Seiten zu einer Datei zusammenführen") - parser.add_argument("--inputs", nargs="+", required=True) - parser.add_argument("--output", required=True) - parser.add_argument("--resolution", type=float, required=True) + parser.add_argument("--inputs", nargs="+") + parser.add_argument("--output") + parser.add_argument("--resolution", type=float, default=300) parser.add_argument("--profile", choices=["document", "receipt", "raw"], default="document") parser.add_argument("--postprocess", action="store_true") parser.add_argument("--strict", action="store_true") + parser.add_argument("--background-cleanup", action="store_true") + parser.add_argument("--ocr", action="store_true") + parser.add_argument("--ocr-language", default="deu+eng") + parser.add_argument("--check-runtime", action="store_true") args = parser.parse_args() - print(json.dumps(create_document(args.inputs, args.output, args.resolution, args.postprocess, args.profile, args.strict))) + if args.check_runtime: + if args.background_cleanup or (args.postprocess and args.strict): + import cv2, numpy + if args.ocr: + check_ocr(args.ocr_language) + print(json.dumps({"ready": True})) + return + if not args.inputs or not args.output: + parser.error("--inputs und --output sind erforderlich") + print(json.dumps(create_document(args.inputs, args.output, args.resolution, args.postprocess, args.profile, + args.strict, args.ocr, args.ocr_language, args.background_cleanup))) if __name__ == "__main__": diff --git a/agents/fedeo-device-agent/src/config.ts b/agents/fedeo-device-agent/src/config.ts index dd8ad075..0a4c895f 100644 --- a/agents/fedeo-device-agent/src/config.ts +++ b/agents/fedeo-device-agent/src/config.ts @@ -62,6 +62,9 @@ export const loadConfig = (): AgentConfig => { scanSource: optional(process.env.FEDEO_SCAN_SOURCE), scanTimeoutSeconds: numberFromEnv(process.env.FEDEO_SCAN_TIMEOUT_SECONDS, 900), scanPostprocess: booleanFromEnv(process.env.FEDEO_SCAN_POSTPROCESS, false), + scanBackgroundCleanup: booleanFromEnv(process.env.FEDEO_SCAN_BACKGROUND_CLEANUP, true), + scanOcr: booleanFromEnv(process.env.FEDEO_SCAN_OCR, true), + scanOcrLanguage: optional(process.env.FEDEO_SCAN_OCR_LANGUAGE) || "deu+eng", postprocessProfile: postprocessProfileFromEnv(process.env.FEDEO_SCAN_POSTPROCESS_PROFILE), postprocessPython: optional(process.env.FEDEO_SCAN_POSTPROCESS_PYTHON) || defaultPostprocessPython(), postprocessStrict: booleanFromEnv(process.env.FEDEO_SCAN_POSTPROCESS_STRICT, false), diff --git a/agents/fedeo-device-agent/src/main.ts b/agents/fedeo-device-agent/src/main.ts index 0d0b7eeb..86af388a 100644 --- a/agents/fedeo-device-agent/src/main.ts +++ b/agents/fedeo-device-agent/src/main.ts @@ -7,6 +7,7 @@ import { loadConfig } from "./config.js" import { log } from "./logger.js" import { listPrinters } from "./print/cups.js" import { hasSane, listScanners, runScan } from "./scan/sane.js" +import { processingCapabilities } from "./scan/postprocess.js" import { AgentHeartbeat } from "./types.js" const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) @@ -19,6 +20,7 @@ const stringifyError = (error: unknown) => { const main = async () => { const config = loadConfig() const api = new FedeoApi(config) + const processing = await processingCapabilities(config) log.info("FEDEO Geräte-Agent startet", { platform: process.platform, @@ -38,6 +40,7 @@ const main = async () => { const heartbeatPayload: AgentHeartbeat = { capabilities: { scan: scanAvailable, + ...processing, print: printerNames.length > 0, platform: process.platform, }, diff --git a/agents/fedeo-device-agent/src/scan/postprocess.ts b/agents/fedeo-device-agent/src/scan/postprocess.ts index b70d2214..4e6eae71 100644 --- a/agents/fedeo-device-agent/src/scan/postprocess.ts +++ b/agents/fedeo-device-agent/src/scan/postprocess.ts @@ -6,15 +6,34 @@ import { runCommand } from "../commands.js" const agentRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..") const documentScript = path.join(agentRoot, "scripts/scan_document.py") -export const checkScanRuntime = async (config: AgentConfig, postprocess: boolean) => { - const imports = postprocess && config.postprocessStrict ? "import PIL, cv2, numpy" : "import PIL" +type ProcessingOptions = { ocr: boolean; ocrLanguage: string; backgroundCleanup: boolean } + +export const checkScanRuntime = async (config: AgentConfig, postprocess: boolean, options: ProcessingOptions) => { + const args = [documentScript, "--check-runtime", "--ocr-language", options.ocrLanguage] + if (postprocess) args.push("--postprocess") + if (config.postprocessStrict) args.push("--strict") + if (options.backgroundCleanup) args.push("--background-cleanup") + if (options.ocr) args.push("--ocr") try { - const result = await runCommand(config.postprocessPython, ["-c", imports], { timeoutMs: 10_000 }) + const result = await runCommand(config.postprocessPython, args, { timeoutMs: 10_000 }) if (result.code === 0) return - } catch { + throw new Error(result.stderr.trim()) + } catch (error) { // Report missing runtimes before the scanner consumes any paper. + const detail = error instanceof Error ? error.message : String(error) + throw new Error("Scan-Verarbeitung nicht verfügbar. Bitte den Agenten mit Python/Pillow, OpenCV und Tesseract samt OCR-Sprachpaketen installieren. " + detail) } - throw new Error("Python/Pillow (bei strikter Nachbearbeitung zusätzlich OpenCV) fehlt. Bitte npm run setup:opencv ausführen oder python3-pil installieren.") +} + +export const processingCapabilities = async (config: AgentConfig) => { + const available = async (options: ProcessingOptions) => { + try { await checkScanRuntime(config, false, options); return true } catch { return false } + } + const [ocr, backgroundCleanup] = await Promise.all([ + available({ ocr: true, ocrLanguage: config.scanOcrLanguage, backgroundCleanup: false }), + available({ ocr: false, ocrLanguage: config.scanOcrLanguage, backgroundCleanup: true }), + ]) + return { ocr, backgroundCleanup } } export const createScanDocument = async ( @@ -23,11 +42,14 @@ export const createScanDocument = async ( outputPath: string, resolution: number, postprocess: boolean, - profile: AgentConfig["postprocessProfile"] + profile: AgentConfig["postprocessProfile"], + options: ProcessingOptions ): Promise => { const args = [documentScript, "--output", outputPath, "--resolution", String(resolution), "--profile", profile] if (postprocess) args.push("--postprocess") if (config.postprocessStrict) args.push("--strict") + if (options.backgroundCleanup) args.push("--background-cleanup") + if (options.ocr) args.push("--ocr", "--ocr-language", options.ocrLanguage) args.push("--inputs", ...pages) const result = await runCommand(config.postprocessPython, args, { diff --git a/agents/fedeo-device-agent/src/scan/sane.ts b/agents/fedeo-device-agent/src/scan/sane.ts index 33b7d9f7..3e283594 100644 --- a/agents/fedeo-device-agent/src/scan/sane.ts +++ b/agents/fedeo-device-agent/src/scan/sane.ts @@ -53,7 +53,12 @@ export const runScan = async ( const postprocess = booleanSetting(settings, "postprocess", config.scanPostprocess) const profile = stringSetting(settings, "postprocessProfile") || config.postprocessProfile if (!["document", "receipt", "raw"].includes(profile)) throw new Error("Unbekanntes Nachbearbeitungsprofil") - await checkScanRuntime(config, postprocess) + const processing = { + backgroundCleanup: booleanSetting(settings, "backgroundCleanup", config.scanBackgroundCleanup), + ocr: format === "pdf" && booleanSetting(settings, "ocr", config.scanOcr), + ocrLanguage: stringSetting(settings, "ocrLanguage") || config.scanOcrLanguage, + } + await checkScanRuntime(config, postprocess, processing) // Each physical scan has its own directory. A filename cannot escape it or overwrite another job. await mkdir(config.workDir, { recursive: true, mode: 0o700 }) @@ -111,8 +116,8 @@ export const runScan = async ( throw new Error("Der Duplex-Scan ist unvollständig: Zu einer Vorderseite fehlt die Rückseite.") } - await reportProgress(`${pages.length} Seiten gescannt. Datei wird erstellt...`) - return await createScanDocument(config, pages, outputPath, resolution, postprocess, profile as AgentConfig["postprocessProfile"]) + await reportProgress(`${pages.length} Seiten gescannt. ${processing.ocr ? "Durchsuchbares PDF mit OCR wird erstellt..." : "Datei wird erstellt..."}`) + return await createScanDocument(config, pages, outputPath, resolution, postprocess, profile as AgentConfig["postprocessProfile"], processing) } catch (error) { const message = error instanceof Error ? error.message : String(error) throw new Error(`${message} Rohscans bleiben zur Wiederherstellung in ${jobDir}.`) diff --git a/agents/fedeo-device-agent/src/types.ts b/agents/fedeo-device-agent/src/types.ts index a30dd125..8a7efedb 100644 --- a/agents/fedeo-device-agent/src/types.ts +++ b/agents/fedeo-device-agent/src/types.ts @@ -11,6 +11,9 @@ export type AgentConfig = { scanSource?: string scanTimeoutSeconds: number scanPostprocess: boolean + scanBackgroundCleanup: boolean + scanOcr: boolean + scanOcrLanguage: string postprocessProfile: "document" | "receipt" | "raw" postprocessPython: string postprocessStrict: boolean @@ -20,6 +23,8 @@ export type AgentHeartbeat = { capabilities: { scan: boolean print: boolean + ocr: boolean + backgroundCleanup: boolean platform: NodeJS.Platform } scannerNames: string[] diff --git a/agents/fedeo-device-agent/tests/installer.test.mjs b/agents/fedeo-device-agent/tests/installer.test.mjs index 425dd626..b8927d18 100644 --- a/agents/fedeo-device-agent/tests/installer.test.mjs +++ b/agents/fedeo-device-agent/tests/installer.test.mjs @@ -52,11 +52,15 @@ for (const start of [false, true]) { await writeFile(path.join(bin, 'scanimage'), '#!/bin/sh\nexit 0\n', { mode: 0o755 }) await writeFile(path.join(bin, 'id'), '#!/bin/sh\nprintf "1000\\n"\n', { mode: 0o755 }) await writeFile(path.join(bin, 'systemctl'), '#!/bin/sh\nprintf "%s\\n" "$*" >> "$FEDEO_TEST_SYSTEMD_LOG"\n', { mode: 0o755 }) + const realPython = (await execute('sh', ['-c', 'command -v python3'])).stdout.trim() + await writeFile(path.join(bin, 'python3'), '#!/bin/sh\nif [ "$1" = "-c" ] && [ "$2" = "import PIL, cv2, numpy" ]; then exit 0; fi\nexec "$FEDEO_TEST_PYTHON" "$@"\n', { mode: 0o755 }) + await writeFile(path.join(bin, 'tesseract'), '#!/bin/sh\nprintf "deu\\neng\\n"\n', { mode: 0o755 }) const env = { ...process.env, PATH: bin + ':' + process.env.PATH, XDG_DATA_HOME: path.join(temp, 'data with spaces %'), XDG_CONFIG_HOME: path.join(temp, 'config'), XDG_STATE_HOME: path.join(temp, 'state'), FEDEO_TEST_SYSTEMD_LOG: path.join(temp, 'systemd.log'), + FEDEO_TEST_PYTHON: realPython, } const scriptPath = path.join(temp, 'install.sh') await writeFile(scriptPath, renderAgentInstaller(bundle, options)) diff --git a/agents/fedeo-device-agent/tests/scan.test.ts b/agents/fedeo-device-agent/tests/scan.test.ts index 232cbcb1..4239bc67 100644 --- a/agents/fedeo-device-agent/tests/scan.test.ts +++ b/agents/fedeo-device-agent/tests/scan.test.ts @@ -51,6 +51,7 @@ const setup = async (t: TestContext) => { workDir: path.join(dir, "scans with 100% space"), scannerName: "fujitsu:ScanSnap iX1600:test", scanFormat: "pdf", scanResolution: 300, scanMode: "Color", scanSource: "ADF Duplex", scanTimeoutSeconds: 10, scanPostprocess: false, postprocessProfile: "document", + scanBackgroundCleanup: false, scanOcr: false, scanOcrLanguage: "deu+eng", postprocessPython: "python3", postprocessStrict: false, } const job: ScanJob = { id: "test-job", tenantId: 1, agentId: "test-agent", status: "running", settings: { paperSize: "A4" } } @@ -143,6 +144,18 @@ test("missing image runtime is detected before feeding paper", async (t) => { await assert.rejects(args(), /ENOENT/) }) +test("missing requested OCR language fails before the scanner consumes paper", async (t) => { + const { config, job, dir, args } = await setup(t) + const tesseract = path.join(dir, "tesseract") + await writeFile(tesseract, "#!/bin/sh\nprintf 'List of available languages (1):\\neng\\n'\n") + await chmod(tesseract, 0o755) + job.settings!.ocr = true + job.settings!.ocrLanguage = "deu+eng" + await assert.rejects(runScan(config, job), /OCR-Sprachpakete fehlen: deu/) + await assert.rejects(args(), /ENOENT/) + await assert.rejects(readdir(config.workDir), /ENOENT/) +}) + test("single-page scans work without batch and requested paths cannot escape the job directory", async (t) => { const { config, job, args } = await setup(t) job.settings = { source: "Flatbed", format: "png" } diff --git a/agents/fedeo-device-agent/tests/scan_background_test.py b/agents/fedeo-device-agent/tests/scan_background_test.py new file mode 100644 index 00000000..673e4f1e --- /dev/null +++ b/agents/fedeo-device-agent/tests/scan_background_test.py @@ -0,0 +1,64 @@ +import sys +import unittest +from pathlib import Path + +import cv2 +import numpy as np + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) +from scan_background import whiten_background + + +class ScanBackgroundTest(unittest.TestCase): + def receipt(self, background=45, paper=245): + image = np.full((1600, 1200, 3), background, dtype=np.uint8) + image[120:1440, 360:820] = paper + for y in range(220, 1350, 70): + cv2.putText(image, "BON 12,34 EUR", (375, y), cv2.FONT_HERSHEY_SIMPLEX, .7, (20, 20, 20), 2) + cv2.rectangle(image, (400, 140), (780, 180), (40, 80, 210), -1) + return image + + def test_receipt_on_dark_or_gray_backing_keeps_canvas_text_and_colored_logo(self): + for background in (35, 140): + image = self.receipt(background) + result = whiten_background(image) + self.assertEqual(result.shape, image.shape) + self.assertTrue(np.all(result[:80] == 255)) + self.assertTrue(np.all(result[:, :250] == 255)) + self.assertTrue(np.array_equal(result[120:1440, 360:820], image[120:1440, 360:820])) + + def test_slanted_receipt_is_whitened_without_rotating_or_cropping(self): + image = np.full((1600, 1200, 3), 55, dtype=np.uint8) + polygon = np.array([[450, 100], [890, 155], [720, 1450], [280, 1390]]) + cv2.fillPoly(image, [polygon], (245, 245, 245)) + cv2.putText(image, "FEDEO", (470, 700), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 0, 0), 3) + result = whiten_background(image) + self.assertEqual(result.shape, image.shape) + inside = np.zeros(image.shape[:2], dtype=np.uint8) + cv2.fillPoly(inside, [polygon], 255) + self.assertTrue(np.array_equal(result[inside > 0], image[inside > 0])) + self.assertTrue(np.all(result[:50] == 255)) + + def test_white_full_page_is_not_replaced_by_its_largest_text_box(self): + image = np.full((1600, 1200, 3), 255, dtype=np.uint8) + cv2.rectangle(image, (300, 400), (800, 1200), (30, 30, 30), 4) + cv2.putText(image, "Footer", (20, 1550), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2) + self.assertTrue(np.array_equal(whiten_background(image), image)) + + def test_slanted_receipt_retains_every_paper_edge_pixel_at_300_dpi(self): + image = np.full((3508, 2480, 3), 45, dtype=np.uint8) + polygon = np.array([[820, 200], [1700, 340], [1250, 3300], [380, 3160]]) + cv2.fillPoly(image, [polygon], (245, 245, 245)) + inside = np.zeros(image.shape[:2], dtype=np.uint8) + cv2.fillPoly(inside, [polygon], 255) + result = whiten_background(image) + self.assertTrue(np.array_equal(result[inside > 0], image[inside > 0])) + self.assertTrue(np.all(result[:50] == 255)) + + def test_blank_and_ambiguous_low_contrast_pages_remain_unchanged(self): + for image in [np.full((300, 200, 3), 245, dtype=np.uint8), self.receipt(215, 230)]: + self.assertTrue(np.array_equal(whiten_background(image), image)) + + +if __name__ == "__main__": + unittest.main() diff --git a/agents/fedeo-device-agent/tests/scan_ocr_test.py b/agents/fedeo-device-agent/tests/scan_ocr_test.py new file mode 100644 index 00000000..4645190e --- /dev/null +++ b/agents/fedeo-device-agent/tests/scan_ocr_test.py @@ -0,0 +1,71 @@ +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from PIL import Image, ImageDraw, ImageFont, PdfParser + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) +from scan_document import create_document, check_ocr + + +class ScanOcrTest(unittest.TestCase): + def test_missing_language_is_reported_before_processing(self): + with patch("scan_document.run_ocr_command", return_value="List of available languages (1):\neng\n"): + with self.assertRaisesRegex(RuntimeError, "deu"): + check_ocr("deu+eng") + with self.assertRaises(ValueError): + check_ocr("--bad-language") + + @unittest.skipUnless(shutil.which("tesseract") and shutil.which("pdftotext"), "Tesseract and Poppler required") + def test_ocr_keeps_receipt_canvas_blank_back_and_page_order_with_real_extractable_text(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + pages = [] + for index, text in enumerate(["FEDEO Bon 12345", "", "Zweite Seite 24680"]): + page = root / f"page-{index}.png" + image = Image.new("RGB", (1240, 1754), (45, 45, 45) if index == 0 else "white") + draw = ImageDraw.Draw(image) + if index == 0: + draw.rectangle((260, 150, 1000, 1550), fill="white") + if text: + draw.text((300, 300), text, fill="black", font=ImageFont.load_default(size=44)) + draw.text((300, 390), "Summe EUR 12,34", fill="black", font=ImageFont.load_default(size=36)) + image.save(page) + pages.append(page) + output = root / "searchable.pdf" + result = create_document(pages, output, 150, ocr=True, background_cleanup=True) + self.assertEqual(result["pageCount"], 3) + with PdfParser.PdfParser(filename=str(output)) as pdf: + self.assertEqual(len(pdf.pages), 3) + for reference in pdf.pages: + box = pdf.read_indirect(reference)[b"MediaBox"] + self.assertAlmostEqual(box[2], 1240 * 72 / 150, delta=.1) + self.assertAlmostEqual(box[3], 1754 * 72 / 150, delta=.1) + text = subprocess.check_output(["pdftotext", str(output), "-"], text=True) + extracted = text.split("\f") + self.assertIn("12345", extracted[0]) + self.assertEqual(extracted[1].strip(), "") + self.assertIn("24680", extracted[2]) + self.assertTrue(all(page.exists() for page in pages)) + + def test_ocr_failure_never_publishes_a_partial_document_or_removes_raw_pages(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + page = root / "raw.png" + Image.new("RGB", (400, 600), "white").save(page) + output = root / "failed.pdf" + with patch("scan_document.check_ocr"), patch("scan_document.run_ocr_command", side_effect=RuntimeError("OCR failed")): + with self.assertRaisesRegex(RuntimeError, "OCR failed"): + create_document([page], output, 300, ocr=True) + self.assertFalse(output.exists()) + self.assertFalse(output.with_name("failed.pdf.partial").exists()) + self.assertTrue(page.exists()) + self.assertEqual(list(root.glob("prepared-*")), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/src/routes/instanceAgents.ts b/backend/src/routes/instanceAgents.ts index 9f1d1e35..3f82ce4b 100644 --- a/backend/src/routes/instanceAgents.ts +++ b/backend/src/routes/instanceAgents.ts @@ -152,6 +152,7 @@ export default async function instanceAgentRoutes(server: FastifyInstance) { active: instanceAgents.active, preferredScannerName: instanceAgents.preferredScannerName, scanDefaults: instanceAgents.scanDefaults, + capabilities: instanceAgents.capabilities, }) .from(instanceAgents) .where(eq(instanceAgents.id, body.agentId)) @@ -161,6 +162,14 @@ export default async function instanceAgentRoutes(server: FastifyInstance) { return reply.code(404).send({ error: "Active agent not found" }) } + const settings = { ...((agent.scanDefaults || {}) as Record), ...(body.settings || {}) } + const enabled = (value: unknown) => value === true || (typeof value === "string" && ["1", "true", "yes", "ja", "on"].includes(value.toLowerCase().trim())) + const capabilities = agent.capabilities as Record + if (((settings.format || "pdf") === "pdf" && enabled(settings.ocr) && !capabilities?.ocr) || + (enabled(settings.backgroundCleanup) && !capabilities?.backgroundCleanup)) { + return reply.code(409).send({ error: "Bitte den Scanner-Agenten mit Tesseract und OpenCV aktualisieren. Die angeforderte Scan-Verarbeitung ist dort noch nicht verfügbar." }) + } + const [job] = await server.db .insert(instanceAgentScanJobs) .values({ @@ -169,10 +178,7 @@ export default async function instanceAgentRoutes(server: FastifyInstance) { requestedBy: req.user?.user_id, scannerName: body.scannerName || agent.preferredScannerName, requestedFilename: body.requestedFilename, - settings: { - ...((agent.scanDefaults || {}) as Record), - ...(body.settings || {}), - }, + settings, target: body.target || {}, }) .returning() diff --git a/backend/tests/instanceAgentSetup.test.ts b/backend/tests/instanceAgentSetup.test.ts index ab220e32..0330bdff 100644 --- a/backend/tests/instanceAgentSetup.test.ts +++ b/backend/tests/instanceAgentSetup.test.ts @@ -55,3 +55,30 @@ test("unknown agents cannot receive setup credentials", async (t) => { assert.equal(response.statusCode, 404) assert.equal(f.updates.length, 0) }) + +test("OCR and background cleanup require an agent that supports the requested processing", async t => { + const app = Fastify() + t.after(() => app.close()) + const jobs: any[] = [] + let capabilities: Record = { scan: true } + app.addHook("onRequest", async request => { request.user = { is_admin: true, tenant_id: 1 } as any }) + app.decorate("db", { + select: () => ({ from: () => ({ where: () => ({ limit: async () => [{ id, active: true, scanDefaults: {}, capabilities }] }) }) }), + insert: () => ({ values: (data: any) => ({ returning: async () => { jobs.push(data); return [{ id: "job", ...data }] } }) }), + }) + await app.register(routes) + for (const settings of [{ ocr: true }, { ocr: "true" }, { backgroundCleanup: true }]) { + const response = await app.inject({ method: "POST", url: "/scan-jobs", payload: { agentId: id, settings } }) + assert.equal(response.statusCode, 409) + assert.equal(jobs.length, 0) + } + capabilities = { scan: true, ocr: true, backgroundCleanup: true } + const response = await app.inject({ + method: "POST", url: "/scan-jobs", + payload: { agentId: id, settings: { ocr: true, ocrLanguage: "deu+eng", backgroundCleanup: true }, target: { folder: null, type: null } }, + }) + assert.equal(response.statusCode, 200) + assert.equal(jobs.length, 1) + assert.equal(jobs[0].settings.ocr, true) + assert.equal(jobs[0].target.type, null) +}) diff --git a/frontend/assets/device-agent/bundle.json b/frontend/assets/device-agent/bundle.json index 84a2b6ed..e52b307f 100644 --- a/frontend/assets/device-agent/bundle.json +++ b/frontend/assets/device-agent/bundle.json @@ -1,18 +1,19 @@ { "version": "0.1.0", - "installerTemplate": "#!/usr/bin/env bash\n# Generated by the FEDEO setup wizard. This file contains the agent's access token.\nset -euo pipefail\numask 077\n\nif [[ \"$(uname -s)\" != Linux ]]; then\n echo 'Diese Installation ist für Linux vorgesehen.' >&2\n exit 1\nfi\nif [[ \"$(id -u)\" == 0 ]]; then\n echo 'Bitte dieses Skript als normaler Benutzer ohne sudo starten.' >&2\n exit 1\nfi\nif [[ \"${1:-}\" != '' && \"${1:-}\" != --no-start ]]; then\n echo 'Aufruf: bash install.sh [--no-start]' >&2\n exit 1\nfi\n\nFEDEO_SETUP_ID='@@AGENT_ID@@'\nFEDEO_SETUP_AUTOSTART='@@AUTOSTART@@'\nFEDEO_SETUP_DIR=\"${XDG_DATA_HOME:-$HOME/.local/share}/fedeo-device-agent/$FEDEO_SETUP_ID\"\nFEDEO_SETUP_CONFIG=\"${XDG_CONFIG_HOME:-$HOME/.config}/fedeo-device-agent/$FEDEO_SETUP_ID.env\"\nFEDEO_SETUP_WORK=\"${XDG_STATE_HOME:-$HOME/.local/state}/fedeo-device-agent/$FEDEO_SETUP_ID\"\nFEDEO_SETUP_UNIT=\"fedeo-device-agent-$FEDEO_SETUP_ID.service\"\nFEDEO_SETUP_UNITS=\"${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user\"\n\necho 'FEDEO Scan-Agent wird für diesen Benutzer eingerichtet.'\nif ! command -v node >/dev/null || ! command -v scanimage >/dev/null || ! python3 -c 'import PIL' >/dev/null 2>&1; then\n if ! command -v apt-get >/dev/null; then\n echo 'Bitte Node.js >= 20, SANE (scanimage), Python 3 und Pillow installieren und das Skript erneut starten.' >&2\n exit 1\n fi\n echo 'Fehlende Systempakete werden über apt installiert (sudo benötigt).'\n sudo apt-get update\n sudo apt-get install -y nodejs sane-utils python3-pil\nfi\nnode -e 'if (Number(process.versions.node.split(\".\")[0]) < 20) { console.error(\"Node.js >= 20 wird benötigt. Bitte Node.js aktualisieren und die Installation erneut starten.\"); process.exit(1) }'\npython3 -c 'import PIL'\n\nFEDEO_SETUP_NODE=\"$(command -v node)\"\nFEDEO_SETUP_HAS_SYSTEMD=false\nif command -v systemctl >/dev/null && systemctl --user show-environment >/dev/null 2>&1; then\n FEDEO_SETUP_HAS_SYSTEMD=true\nfi\nif [[ \"$FEDEO_SETUP_AUTOSTART\" == true && \"$FEDEO_SETUP_HAS_SYSTEMD\" != true ]]; then\n echo 'Der Benutzerdienst von systemd ist nicht erreichbar. Bitte in der Desktop-Sitzung starten oder den Autostart im Assistenten ausschalten.' >&2\n exit 1\nfi\n\nmkdir -p \"$FEDEO_SETUP_DIR\" \"$(dirname \"$FEDEO_SETUP_CONFIG\")\" \"$FEDEO_SETUP_WORK\" \"$FEDEO_SETUP_UNITS\"\n# Stop only the matching installation before replacing its files.\nif [[ \"$FEDEO_SETUP_HAS_SYSTEMD\" == true && \"${1:-}\" != --no-start ]]; then\n systemctl --user stop \"$FEDEO_SETUP_UNIT\" 2>/dev/null || true\nfi\n\npython3 - \"$FEDEO_SETUP_DIR\" \"$FEDEO_SETUP_CONFIG\" \"$FEDEO_SETUP_WORK\" \"$FEDEO_SETUP_NODE\" \"$FEDEO_SETUP_UNITS/$FEDEO_SETUP_UNIT\" <<'FEDEO_PYTHON'\nimport base64\nimport json\nimport os\nfrom pathlib import Path\nimport sys\n\nagent_dir, config_path, work_dir, node, unit_path = map(Path, sys.argv[1:])\nfiles = json.loads(base64.b64decode('@@PAYLOAD_BASE64@@'))\nfor name, encoded in files.items():\n relative = Path(name)\n if relative.is_absolute() or '..' in relative.parts:\n raise ValueError('Ungültiger Dateiname im Installationspaket')\n target = agent_dir / relative\n target.parent.mkdir(parents=True, exist_ok=True)\n target.write_bytes(base64.b64decode(encoded, validate=True))\n\nconfig = base64.b64decode('@@CONFIG_BASE64@@').decode('utf8')\nconfig += 'FEDEO_WORK_DIR=' + str(work_dir) + '\\n'\nconfig_path.write_text(config, encoding='utf8')\nconfig_path.chmod(0o600)\n\ndef unit_quote(value):\n return '\"' + str(value).replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"').replace('%', '%%') + '\"'\n\nunit_path.write_text('\\n'.join([\n '[Unit]', 'Description=FEDEO Scan-Agent', 'After=network-online.target', '',\n '[Service]', 'Type=simple',\n # WorkingDirectory takes a literal path, unlike Environment/ExecStart.\n # Quotes would become part of the path; only systemd specifiers need escaping.\n 'WorkingDirectory=' + str(agent_dir).replace('%', '%%'),\n 'Environment=' + unit_quote('FEDEO_AGENT_ENV=' + str(config_path)),\n 'ExecStart=' + unit_quote(node) + ' ' + unit_quote(agent_dir / 'dist/main.js'),\n 'Restart=on-failure', 'RestartSec=5', 'UMask=0077', '',\n '[Install]', 'WantedBy=default.target', '',\n]), encoding='utf8')\nFEDEO_PYTHON\n\necho \"Agent installiert: $FEDEO_SETUP_DIR\"\necho \"Konfiguration: $FEDEO_SETUP_CONFIG\"\nif [[ \"${1:-}\" == --no-start ]]; then\n echo 'Installation abgeschlossen; Agent wurde nicht gestartet.'\nelif [[ \"$FEDEO_SETUP_AUTOSTART\" == true ]]; then\n systemctl --user daemon-reload\n systemctl --user enable \"$FEDEO_SETUP_UNIT\"\n systemctl --user restart \"$FEDEO_SETUP_UNIT\"\n echo 'Agent gestartet. Er startet künftig bei der Anmeldung.'\n printf 'Protokoll: journalctl --user -u %q -f\\n' \"$FEDEO_SETUP_UNIT\"\n echo 'Jetzt im FEDEO-Assistenten die Verbindung prüfen. Die heruntergeladene Installationsdatei kann anschließend gelöscht werden.'\nelse\n if [[ \"$FEDEO_SETUP_HAS_SYSTEMD\" == true ]]; then\n systemctl --user disable \"$FEDEO_SETUP_UNIT\" 2>/dev/null || true\n systemctl --user daemon-reload\n fi\n echo 'Agent startet im Terminal. Dieses Fenster geöffnet lassen; Beenden mit Strg+C.'\n cd \"$FEDEO_SETUP_DIR\"\n FEDEO_AGENT_ENV=\"$FEDEO_SETUP_CONFIG\" \"$FEDEO_SETUP_NODE\" dist/main.js\nfi\n", + "installerTemplate": "#!/usr/bin/env bash\n# Generated by the FEDEO setup wizard. This file contains the agent's access token.\nset -euo pipefail\numask 077\n\nif [[ \"$(uname -s)\" != Linux ]]; then\n echo 'Diese Installation ist für Linux vorgesehen.' >&2\n exit 1\nfi\nif [[ \"$(id -u)\" == 0 ]]; then\n echo 'Bitte dieses Skript als normaler Benutzer ohne sudo starten.' >&2\n exit 1\nfi\nif [[ \"${1:-}\" != '' && \"${1:-}\" != --no-start ]]; then\n echo 'Aufruf: bash install.sh [--no-start]' >&2\n exit 1\nfi\n\nFEDEO_SETUP_ID='@@AGENT_ID@@'\nFEDEO_SETUP_AUTOSTART='@@AUTOSTART@@'\nFEDEO_SETUP_DIR=\"${XDG_DATA_HOME:-$HOME/.local/share}/fedeo-device-agent/$FEDEO_SETUP_ID\"\nFEDEO_SETUP_CONFIG=\"${XDG_CONFIG_HOME:-$HOME/.config}/fedeo-device-agent/$FEDEO_SETUP_ID.env\"\nFEDEO_SETUP_WORK=\"${XDG_STATE_HOME:-$HOME/.local/state}/fedeo-device-agent/$FEDEO_SETUP_ID\"\nFEDEO_SETUP_UNIT=\"fedeo-device-agent-$FEDEO_SETUP_ID.service\"\nFEDEO_SETUP_UNITS=\"${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user\"\n\necho 'FEDEO Scan-Agent wird für diesen Benutzer eingerichtet.'\nfedeo_processing_ready() {\n python3 -c 'import PIL, cv2, numpy' >/dev/null 2>&1 &&\n tesseract --list-langs 2>/dev/null | grep -qx deu &&\n tesseract --list-langs 2>/dev/null | grep -qx eng\n}\nif ! command -v node >/dev/null || ! command -v scanimage >/dev/null || ! fedeo_processing_ready; then\n if ! command -v apt-get >/dev/null; then\n echo 'Bitte Node.js >= 20, SANE, Python/Pillow/OpenCV sowie Tesseract mit Deutsch/Englisch installieren und das Skript erneut starten.' >&2\n exit 1\n fi\n echo 'Fehlende Systempakete werden über apt installiert (sudo benötigt).'\n sudo apt-get update\n sudo apt-get install -y nodejs sane-utils python3-pil python3-opencv tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng\nfi\nnode -e 'if (Number(process.versions.node.split(\".\")[0]) < 20) { console.error(\"Node.js >= 20 wird benötigt. Bitte Node.js aktualisieren und die Installation erneut starten.\"); process.exit(1) }'\nfedeo_processing_ready || { echo 'Die benötigte Bildverarbeitung oder die OCR-Sprachpakete fehlen weiterhin.' >&2; exit 1; }\n\nFEDEO_SETUP_NODE=\"$(command -v node)\"\nFEDEO_SETUP_HAS_SYSTEMD=false\nif command -v systemctl >/dev/null && systemctl --user show-environment >/dev/null 2>&1; then\n FEDEO_SETUP_HAS_SYSTEMD=true\nfi\nif [[ \"$FEDEO_SETUP_AUTOSTART\" == true && \"$FEDEO_SETUP_HAS_SYSTEMD\" != true ]]; then\n echo 'Der Benutzerdienst von systemd ist nicht erreichbar. Bitte in der Desktop-Sitzung starten oder den Autostart im Assistenten ausschalten.' >&2\n exit 1\nfi\n\nmkdir -p \"$FEDEO_SETUP_DIR\" \"$(dirname \"$FEDEO_SETUP_CONFIG\")\" \"$FEDEO_SETUP_WORK\" \"$FEDEO_SETUP_UNITS\"\n# Stop only the matching installation before replacing its files.\nif [[ \"$FEDEO_SETUP_HAS_SYSTEMD\" == true && \"${1:-}\" != --no-start ]]; then\n systemctl --user stop \"$FEDEO_SETUP_UNIT\" 2>/dev/null || true\nfi\n\npython3 - \"$FEDEO_SETUP_DIR\" \"$FEDEO_SETUP_CONFIG\" \"$FEDEO_SETUP_WORK\" \"$FEDEO_SETUP_NODE\" \"$FEDEO_SETUP_UNITS/$FEDEO_SETUP_UNIT\" <<'FEDEO_PYTHON'\nimport base64\nimport json\nimport os\nfrom pathlib import Path\nimport sys\n\nagent_dir, config_path, work_dir, node, unit_path = map(Path, sys.argv[1:])\nfiles = json.loads(base64.b64decode('@@PAYLOAD_BASE64@@'))\nfor name, encoded in files.items():\n relative = Path(name)\n if relative.is_absolute() or '..' in relative.parts:\n raise ValueError('Ungültiger Dateiname im Installationspaket')\n target = agent_dir / relative\n target.parent.mkdir(parents=True, exist_ok=True)\n target.write_bytes(base64.b64decode(encoded, validate=True))\n\nconfig = base64.b64decode('@@CONFIG_BASE64@@').decode('utf8')\nconfig += 'FEDEO_WORK_DIR=' + str(work_dir) + '\\n'\nconfig_path.write_text(config, encoding='utf8')\nconfig_path.chmod(0o600)\n\ndef unit_quote(value):\n return '\"' + str(value).replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"').replace('%', '%%') + '\"'\n\nunit_path.write_text('\\n'.join([\n '[Unit]', 'Description=FEDEO Scan-Agent', 'After=network-online.target', '',\n '[Service]', 'Type=simple',\n # WorkingDirectory takes a literal path, unlike Environment/ExecStart.\n # Quotes would become part of the path; only systemd specifiers need escaping.\n 'WorkingDirectory=' + str(agent_dir).replace('%', '%%'),\n 'Environment=' + unit_quote('FEDEO_AGENT_ENV=' + str(config_path)),\n 'ExecStart=' + unit_quote(node) + ' ' + unit_quote(agent_dir / 'dist/main.js'),\n 'Restart=on-failure', 'RestartSec=5', 'UMask=0077', '',\n '[Install]', 'WantedBy=default.target', '',\n]), encoding='utf8')\nFEDEO_PYTHON\n\necho \"Agent installiert: $FEDEO_SETUP_DIR\"\necho \"Konfiguration: $FEDEO_SETUP_CONFIG\"\nif [[ \"${1:-}\" == --no-start ]]; then\n echo 'Installation abgeschlossen; Agent wurde nicht gestartet.'\nelif [[ \"$FEDEO_SETUP_AUTOSTART\" == true ]]; then\n systemctl --user daemon-reload\n systemctl --user enable \"$FEDEO_SETUP_UNIT\"\n systemctl --user restart \"$FEDEO_SETUP_UNIT\"\n echo 'Agent gestartet. Er startet künftig bei der Anmeldung.'\n printf 'Protokoll: journalctl --user -u %q -f\\n' \"$FEDEO_SETUP_UNIT\"\n echo 'Jetzt im FEDEO-Assistenten die Verbindung prüfen. Die heruntergeladene Installationsdatei kann anschließend gelöscht werden.'\nelse\n if [[ \"$FEDEO_SETUP_HAS_SYSTEMD\" == true ]]; then\n systemctl --user disable \"$FEDEO_SETUP_UNIT\" 2>/dev/null || true\n systemctl --user daemon-reload\n fi\n echo 'Agent startet im Terminal. Dieses Fenster geöffnet lassen; Beenden mit Strg+C.'\n cd \"$FEDEO_SETUP_DIR\"\n FEDEO_AGENT_ENV=\"$FEDEO_SETUP_CONFIG\" \"$FEDEO_SETUP_NODE\" dist/main.js\nfi\n", "files": { "dist/api.js": "aW1wb3J0IHsgcmVhZEZpbGUsIHN0YXQgfSBmcm9tICJub2RlOmZzL3Byb21pc2VzIjsKaW1wb3J0IHsgYmFzZW5hbWUgfSBmcm9tICJub2RlOnBhdGgiOwpleHBvcnQgY2xhc3MgRmVkZW9BcGkgewogICAgY29uZmlnOwogICAgY29uc3RydWN0b3IoY29uZmlnKSB7CiAgICAgICAgdGhpcy5jb25maWcgPSBjb25maWc7CiAgICB9CiAgICB1cmwocGF0aCkgewogICAgICAgIHJldHVybiBgJHt0aGlzLmNvbmZpZy5mZWRlb1VybH0ke3BhdGh9YDsKICAgIH0KICAgIGhlYWRlcnMoZXh0cmEpIHsKICAgICAgICByZXR1cm4gewogICAgICAgICAgICAiWC1BZ2VudC1Ub2tlbiI6IHRoaXMuY29uZmlnLmFnZW50VG9rZW4sCiAgICAgICAgICAgIC4uLmV4dHJhLAogICAgICAgIH07CiAgICB9CiAgICBhc3luYyByZXF1ZXN0KHBhdGgsIGluaXQgPSB7fSwgdGltZW91dE1zID0gMzBfMDAwKSB7CiAgICAgICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmZXRjaCh0aGlzLnVybChwYXRoKSwgewogICAgICAgICAgICAuLi5pbml0LAogICAgICAgICAgICBoZWFkZXJzOiB0aGlzLmhlYWRlcnMoaW5pdC5oZWFkZXJzKSwKICAgICAgICAgICAgc2lnbmFsOiBBYm9ydFNpZ25hbC50aW1lb3V0KHRpbWVvdXRNcyksCiAgICAgICAgfSk7CiAgICAgICAgaWYgKCFyZXNwb25zZS5vaykgewogICAgICAgICAgICBjb25zdCBib2R5ID0gYXdhaXQgcmVzcG9uc2UudGV4dCgpLmNhdGNoKCgpID0+ICIiKTsKICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGAke2luaXQubWV0aG9kIHx8ICJHRVQifSAke3BhdGh9IGZlaGxnZXNjaGxhZ2VuOiAke3Jlc3BvbnNlLnN0YXR1c30gJHtib2R5fWApOwogICAgICAgIH0KICAgICAgICByZXR1cm4gYXdhaXQgcmVzcG9uc2UuanNvbigpOwogICAgfQogICAgaGVhcnRiZWF0KHBheWxvYWQpIHsKICAgICAgICByZXR1cm4gdGhpcy5yZXF1ZXN0KCIvaW5zdGFuY2UtYWdlbnQvaGVhcnRiZWF0IiwgewogICAgICAgICAgICBtZXRob2Q6ICJQT1NUIiwKICAgICAgICAgICAgaGVhZGVyczogeyAiQ29udGVudC1UeXBlIjogImFwcGxpY2F0aW9uL2pzb24iIH0sCiAgICAgICAgICAgIGJvZHk6IEpTT04uc3RyaW5naWZ5KHBheWxvYWQpLAogICAgICAgIH0pOwogICAgfQogICAgbmV4dFNjYW5Kb2IoKSB7CiAgICAgICAgcmV0dXJuIHRoaXMucmVxdWVzdCgiL2luc3RhbmNlLWFnZW50L3NjYW4tam9icy9uZXh0Iik7CiAgICB9CiAgICB1cGRhdGVTY2FuSm9iU3RhdHVzKGpvYklkLCBzdGF0dXMsIG1lc3NhZ2UpIHsKICAgICAgICByZXR1cm4gdGhpcy5yZXF1ZXN0KGAvaW5zdGFuY2UtYWdlbnQvc2Nhbi1qb2JzLyR7am9iSWR9L3N0YXR1c2AsIHsKICAgICAgICAgICAgbWV0aG9kOiAiUE9TVCIsCiAgICAgICAgICAgIGhlYWRlcnM6IHsgIkNvbnRlbnQtVHlwZSI6ICJhcHBsaWNhdGlvbi9qc29uIiB9LAogICAgICAgICAgICBib2R5OiBKU09OLnN0cmluZ2lmeSh7IHN0YXR1cywgbWVzc2FnZSB9KSwKICAgICAgICB9KTsKICAgIH0KICAgIGFzeW5jIHVwbG9hZFNjYW4oam9iSWQsIHJlc3VsdCkgewogICAgICAgIGlmICgoYXdhaXQgc3RhdChyZXN1bHQucGF0aCkpLnNpemUgPiAxMDAgKiAxMDI0ICogMTAyNCkgewogICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkRpZSBTY2FuLURhdGVpIMO8YmVyc2NocmVpdGV0IGRhcyBVcGxvYWQtTGltaXQgdm9uIDEwMCBNaUIuIEJpdHRlIGRlbiBTdGFwZWwgYXVmdGVpbGVuIG9kZXIgZGllIEF1ZmzDtnN1bmcgcmVkdXppZXJlbi4iKTsKICAgICAgICB9CiAgICAgICAgY29uc3QgZm9ybSA9IG5ldyBGb3JtRGF0YSgpOwogICAgICAgIGNvbnN0IGZpbGVCdWZmZXIgPSBhd2FpdCByZWFkRmlsZShyZXN1bHQucGF0aCk7CiAgICAgICAgY29uc3QgZmlsZSA9IG5ldyBGaWxlKFtmaWxlQnVmZmVyXSwgcmVzdWx0LmZpbGVuYW1lIHx8IGJhc2VuYW1lKHJlc3VsdC5wYXRoKSwgewogICAgICAgICAgICB0eXBlOiByZXN1bHQubWltZVR5cGUsCiAgICAgICAgfSk7CiAgICAgICAgZm9ybS5hcHBlbmQoImZpbGUiLCBmaWxlKTsKICAgICAgICByZXR1cm4gdGhpcy5yZXF1ZXN0KGAvaW5zdGFuY2UtYWdlbnQvc2Nhbi1qb2JzLyR7am9iSWR9L3VwbG9hZGAsIHsKICAgICAgICAgICAgbWV0aG9kOiAiUE9TVCIsCiAgICAgICAgICAgIGJvZHk6IGZvcm0sCiAgICAgICAgfSwgMTIwXzAwMCk7CiAgICB9Cn0K", "dist/commands.js": "aW1wb3J0IHsgc3Bhd24gfSBmcm9tICJub2RlOmNoaWxkX3Byb2Nlc3MiOwpleHBvcnQgY29uc3QgY29tbWFuZEV4aXN0cyA9IChjb21tYW5kKSA9PiBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4gewogICAgY29uc3QgY2hpbGQgPSBzcGF3bigic2giLCBbIi1jIiwgJ2NvbW1hbmQgLXYgIiQxIiA+L2Rldi9udWxsIDI+JjEnLCAiLS0iLCBjb21tYW5kXSk7CiAgICBjaGlsZC5vbigiZXJyb3IiLCAoKSA9PiByZXNvbHZlKGZhbHNlKSk7CiAgICBjaGlsZC5vbigiY2xvc2UiLCAoY29kZSkgPT4gcmVzb2x2ZShjb2RlID09PSAwKSk7Cn0pOwpleHBvcnQgY29uc3QgcnVuQ29tbWFuZCA9IChjb21tYW5kLCBhcmdzLCBvcHRpb25zID0ge30pID0+IG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHsKICAgIGNvbnN0IGNoaWxkID0gc3Bhd24oY29tbWFuZCwgYXJncywgewogICAgICAgIHN0ZGlvOiBbImlnbm9yZSIsICJwaXBlIiwgInBpcGUiXSwKICAgICAgICBjd2Q6IG9wdGlvbnMuY3dkLAogICAgfSk7CiAgICBjb25zdCBzdGRvdXQgPSBbXTsKICAgIGNvbnN0IHN0ZGVyciA9IFtdOwogICAgbGV0IHRpbWVkT3V0ID0gZmFsc2U7CiAgICBsZXQga2lsbFRpbWVvdXQ7CiAgICBjb25zdCB0aW1lb3V0ID0gb3B0aW9ucy50aW1lb3V0TXMKICAgICAgICA/IHNldFRpbWVvdXQoKCkgPT4gewogICAgICAgICAgICB0aW1lZE91dCA9IHRydWU7CiAgICAgICAgICAgIGNoaWxkLmtpbGwoIlNJR1RFUk0iKTsKICAgICAgICAgICAga2lsbFRpbWVvdXQgPSBzZXRUaW1lb3V0KCgpID0+IGNoaWxkLmtpbGwoIlNJR0tJTEwiKSwgMjAwMCk7CiAgICAgICAgfSwgb3B0aW9ucy50aW1lb3V0TXMpCiAgICAgICAgOiBudWxsOwogICAgY2hpbGQuc3Rkb3V0Lm9uKCJkYXRhIiwgKGNodW5rKSA9PiBzdGRvdXQucHVzaChCdWZmZXIuZnJvbShjaHVuaykpKTsKICAgIGNoaWxkLnN0ZGVyci5vbigiZGF0YSIsIChjaHVuaykgPT4gc3RkZXJyLnB1c2goQnVmZmVyLmZyb20oY2h1bmspKSk7CiAgICBjaGlsZC5vbigiZXJyb3IiLCAoZXJyb3IpID0+IHsKICAgICAgICBpZiAodGltZW91dCkKICAgICAgICAgICAgY2xlYXJUaW1lb3V0KHRpbWVvdXQpOwogICAgICAgIGlmIChraWxsVGltZW91dCkKICAgICAgICAgICAgY2xlYXJUaW1lb3V0KGtpbGxUaW1lb3V0KTsKICAgICAgICByZWplY3QoZXJyb3IpOwogICAgfSk7CiAgICBjaGlsZC5vbigiY2xvc2UiLCAoY29kZSwgc2lnbmFsKSA9PiB7CiAgICAgICAgaWYgKHRpbWVvdXQpCiAgICAgICAgICAgIGNsZWFyVGltZW91dCh0aW1lb3V0KTsKICAgICAgICBpZiAoa2lsbFRpbWVvdXQpCiAgICAgICAgICAgIGNsZWFyVGltZW91dChraWxsVGltZW91dCk7CiAgICAgICAgaWYgKHRpbWVkT3V0KSB7CiAgICAgICAgICAgIHJlamVjdChuZXcgRXJyb3IoYCR7Y29tbWFuZH0gd3VyZGUgbmFjaCAke29wdGlvbnMudGltZW91dE1zfSBtcyBiZWVuZGV0YCkpOwogICAgICAgICAgICByZXR1cm47CiAgICAgICAgfQogICAgICAgIGlmIChzaWduYWwpIHsKICAgICAgICAgICAgcmVqZWN0KG5ldyBFcnJvcihgJHtjb21tYW5kfSB3dXJkZSBkdXJjaCAke3NpZ25hbH0gYmVlbmRldGApKTsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgIH0KICAgICAgICByZXNvbHZlKHsKICAgICAgICAgICAgc3Rkb3V0OiBCdWZmZXIuY29uY2F0KHN0ZG91dCkudG9TdHJpbmcoInV0ZjgiKSwKICAgICAgICAgICAgc3RkZXJyOiBCdWZmZXIuY29uY2F0KHN0ZGVycikudG9TdHJpbmcoInV0ZjgiKSwKICAgICAgICAgICAgY29kZTogY29kZSA/PyAxLAogICAgICAgIH0pOwogICAgfSk7Cn0pOwo=", - "dist/config.js": "aW1wb3J0IHBhdGggZnJvbSAibm9kZTpwYXRoIjsKaW1wb3J0IG9zIGZyb20gIm5vZGU6b3MiOwppbXBvcnQgeyBleGlzdHNTeW5jIH0gZnJvbSAibm9kZTpmcyI7CmltcG9ydCB7IGZpbGVVUkxUb1BhdGggfSBmcm9tICJub2RlOnVybCI7CmltcG9ydCB7IGxvYWREb3RFbnYgfSBmcm9tICIuL2Vudi5qcyI7CmNvbnN0IGN1cnJlbnRGaWxlID0gZmlsZVVSTFRvUGF0aChpbXBvcnQubWV0YS51cmwpOwpjb25zdCBhZ2VudFJvb3QgPSBwYXRoLnJlc29sdmUocGF0aC5kaXJuYW1lKGN1cnJlbnRGaWxlKSwgIi4uIik7CmNvbnN0IG9wdGlvbmFsID0gKHZhbHVlKSA9PiB7CiAgICBjb25zdCB0cmltbWVkID0gdmFsdWU/LnRyaW0oKTsKICAgIHJldHVybiB0cmltbWVkID8gdHJpbW1lZCA6IHVuZGVmaW5lZDsKfTsKY29uc3QgbnVtYmVyRnJvbUVudiA9ICh2YWx1ZSwgZmFsbGJhY2spID0+IHsKICAgIGlmICghdmFsdWUpCiAgICAgICAgcmV0dXJuIGZhbGxiYWNrOwogICAgY29uc3QgcGFyc2VkID0gTnVtYmVyKHZhbHVlKTsKICAgIHJldHVybiBOdW1iZXIuaXNGaW5pdGUocGFyc2VkKSAmJiBwYXJzZWQgPiAwID8gcGFyc2VkIDogZmFsbGJhY2s7Cn07CmNvbnN0IHNjYW5Gb3JtYXRGcm9tRW52ID0gKHZhbHVlKSA9PiB7CiAgICBpZiAodmFsdWUgPT09ICJwbmciIHx8IHZhbHVlID09PSAidGlmZiIgfHwgdmFsdWUgPT09ICJwZGYiKQogICAgICAgIHJldHVybiB2YWx1ZTsKICAgIHJldHVybiAicGRmIjsKfTsKY29uc3QgYm9vbGVhbkZyb21FbnYgPSAodmFsdWUsIGZhbGxiYWNrKSA9PiB7CiAgICBpZiAoIXZhbHVlKQogICAgICAgIHJldHVybiBmYWxsYmFjazsKICAgIHJldHVybiBbIjEiLCAidHJ1ZSIsICJ5ZXMiLCAiamEiLCAib24iXS5pbmNsdWRlcyh2YWx1ZS50cmltKCkudG9Mb3dlckNhc2UoKSk7Cn07CmNvbnN0IHBvc3Rwcm9jZXNzUHJvZmlsZUZyb21FbnYgPSAodmFsdWUpID0+IHsKICAgIGlmICh2YWx1ZSA9PT0gImRvY3VtZW50IiB8fCB2YWx1ZSA9PT0gInJlY2VpcHQiIHx8IHZhbHVlID09PSAicmF3IikKICAgICAgICByZXR1cm4gdmFsdWU7CiAgICByZXR1cm4gImRvY3VtZW50IjsKfTsKY29uc3QgZGVmYXVsdFBvc3Rwcm9jZXNzUHl0aG9uID0gKCkgPT4gewogICAgY29uc3QgbG9jYWxWZW52UHl0aG9uID0gcGF0aC5qb2luKGFnZW50Um9vdCwgIi52ZW52LW9wZW5jdiIsICJiaW4iLCAicHl0aG9uIik7CiAgICByZXR1cm4gZXhpc3RzU3luYyhsb2NhbFZlbnZQeXRob24pID8gbG9jYWxWZW52UHl0aG9uIDogInB5dGhvbjMiOwp9OwpleHBvcnQgY29uc3QgbG9hZENvbmZpZyA9ICgpID0+IHsKICAgIGxvYWREb3RFbnYocHJvY2Vzcy5lbnYuRkVERU9fQUdFTlRfRU5WIHx8ICIuZW52Iik7CiAgICBjb25zdCBmZWRlb1VybCA9IG9wdGlvbmFsKHByb2Nlc3MuZW52LkZFREVPX1VSTCk7CiAgICBjb25zdCBhZ2VudFRva2VuID0gb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fQUdFTlRfVE9LRU4pOwogICAgaWYgKCFmZWRlb1VybCkKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkZFREVPX1VSTCBmZWhsdCIpOwogICAgaWYgKCFhZ2VudFRva2VuKQogICAgICAgIHRocm93IG5ldyBFcnJvcigiRkVERU9fQUdFTlRfVE9LRU4gZmVobHQiKTsKICAgIHJldHVybiB7CiAgICAgICAgZmVkZW9Vcmw6IGZlZGVvVXJsLnJlcGxhY2UoL1wvKyQvLCAiIiksCiAgICAgICAgYWdlbnRUb2tlbiwKICAgICAgICBwb2xsU2Vjb25kczogbnVtYmVyRnJvbUVudihwcm9jZXNzLmVudi5GRURFT19QT0xMX1NFQ09ORFMsIDUpLAogICAgICAgIHdvcmtEaXI6IG9wdGlvbmFsKHByb2Nlc3MuZW52LkZFREVPX1dPUktfRElSKSB8fCBwYXRoLmpvaW4ob3MudG1wZGlyKCksICJmZWRlby1kZXZpY2UtYWdlbnQiKSwKICAgICAgICBzY2FubmVyTmFtZTogb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fU0NBTk5FUl9OQU1FKSwKICAgICAgICBwcmludGVyTmFtZTogb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fUFJJTlRFUl9OQU1FKSwKICAgICAgICBzY2FuRm9ybWF0OiBzY2FuRm9ybWF0RnJvbUVudihwcm9jZXNzLmVudi5GRURFT19TQ0FOX0ZPUk1BVCksCiAgICAgICAgc2NhblJlc29sdXRpb246IG51bWJlckZyb21FbnYocHJvY2Vzcy5lbnYuRkVERU9fU0NBTl9SRVNPTFVUSU9OLCAzMDApLAogICAgICAgIHNjYW5Nb2RlOiBvcHRpb25hbChwcm9jZXNzLmVudi5GRURFT19TQ0FOX01PREUpIHx8ICJDb2xvciIsCiAgICAgICAgc2NhblNvdXJjZTogb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fU0NBTl9TT1VSQ0UpLAogICAgICAgIHNjYW5UaW1lb3V0U2Vjb25kczogbnVtYmVyRnJvbUVudihwcm9jZXNzLmVudi5GRURFT19TQ0FOX1RJTUVPVVRfU0VDT05EUywgOTAwKSwKICAgICAgICBzY2FuUG9zdHByb2Nlc3M6IGJvb2xlYW5Gcm9tRW52KHByb2Nlc3MuZW52LkZFREVPX1NDQU5fUE9TVFBST0NFU1MsIGZhbHNlKSwKICAgICAgICBwb3N0cHJvY2Vzc1Byb2ZpbGU6IHBvc3Rwcm9jZXNzUHJvZmlsZUZyb21FbnYocHJvY2Vzcy5lbnYuRkVERU9fU0NBTl9QT1NUUFJPQ0VTU19QUk9GSUxFKSwKICAgICAgICBwb3N0cHJvY2Vzc1B5dGhvbjogb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fU0NBTl9QT1NUUFJPQ0VTU19QWVRIT04pIHx8IGRlZmF1bHRQb3N0cHJvY2Vzc1B5dGhvbigpLAogICAgICAgIHBvc3Rwcm9jZXNzU3RyaWN0OiBib29sZWFuRnJvbUVudihwcm9jZXNzLmVudi5GRURFT19TQ0FOX1BPU1RQUk9DRVNTX1NUUklDVCwgZmFsc2UpLAogICAgfTsKfTsK", + "dist/config.js": "aW1wb3J0IHBhdGggZnJvbSAibm9kZTpwYXRoIjsKaW1wb3J0IG9zIGZyb20gIm5vZGU6b3MiOwppbXBvcnQgeyBleGlzdHNTeW5jIH0gZnJvbSAibm9kZTpmcyI7CmltcG9ydCB7IGZpbGVVUkxUb1BhdGggfSBmcm9tICJub2RlOnVybCI7CmltcG9ydCB7IGxvYWREb3RFbnYgfSBmcm9tICIuL2Vudi5qcyI7CmNvbnN0IGN1cnJlbnRGaWxlID0gZmlsZVVSTFRvUGF0aChpbXBvcnQubWV0YS51cmwpOwpjb25zdCBhZ2VudFJvb3QgPSBwYXRoLnJlc29sdmUocGF0aC5kaXJuYW1lKGN1cnJlbnRGaWxlKSwgIi4uIik7CmNvbnN0IG9wdGlvbmFsID0gKHZhbHVlKSA9PiB7CiAgICBjb25zdCB0cmltbWVkID0gdmFsdWU/LnRyaW0oKTsKICAgIHJldHVybiB0cmltbWVkID8gdHJpbW1lZCA6IHVuZGVmaW5lZDsKfTsKY29uc3QgbnVtYmVyRnJvbUVudiA9ICh2YWx1ZSwgZmFsbGJhY2spID0+IHsKICAgIGlmICghdmFsdWUpCiAgICAgICAgcmV0dXJuIGZhbGxiYWNrOwogICAgY29uc3QgcGFyc2VkID0gTnVtYmVyKHZhbHVlKTsKICAgIHJldHVybiBOdW1iZXIuaXNGaW5pdGUocGFyc2VkKSAmJiBwYXJzZWQgPiAwID8gcGFyc2VkIDogZmFsbGJhY2s7Cn07CmNvbnN0IHNjYW5Gb3JtYXRGcm9tRW52ID0gKHZhbHVlKSA9PiB7CiAgICBpZiAodmFsdWUgPT09ICJwbmciIHx8IHZhbHVlID09PSAidGlmZiIgfHwgdmFsdWUgPT09ICJwZGYiKQogICAgICAgIHJldHVybiB2YWx1ZTsKICAgIHJldHVybiAicGRmIjsKfTsKY29uc3QgYm9vbGVhbkZyb21FbnYgPSAodmFsdWUsIGZhbGxiYWNrKSA9PiB7CiAgICBpZiAoIXZhbHVlKQogICAgICAgIHJldHVybiBmYWxsYmFjazsKICAgIHJldHVybiBbIjEiLCAidHJ1ZSIsICJ5ZXMiLCAiamEiLCAib24iXS5pbmNsdWRlcyh2YWx1ZS50cmltKCkudG9Mb3dlckNhc2UoKSk7Cn07CmNvbnN0IHBvc3Rwcm9jZXNzUHJvZmlsZUZyb21FbnYgPSAodmFsdWUpID0+IHsKICAgIGlmICh2YWx1ZSA9PT0gImRvY3VtZW50IiB8fCB2YWx1ZSA9PT0gInJlY2VpcHQiIHx8IHZhbHVlID09PSAicmF3IikKICAgICAgICByZXR1cm4gdmFsdWU7CiAgICByZXR1cm4gImRvY3VtZW50IjsKfTsKY29uc3QgZGVmYXVsdFBvc3Rwcm9jZXNzUHl0aG9uID0gKCkgPT4gewogICAgY29uc3QgbG9jYWxWZW52UHl0aG9uID0gcGF0aC5qb2luKGFnZW50Um9vdCwgIi52ZW52LW9wZW5jdiIsICJiaW4iLCAicHl0aG9uIik7CiAgICByZXR1cm4gZXhpc3RzU3luYyhsb2NhbFZlbnZQeXRob24pID8gbG9jYWxWZW52UHl0aG9uIDogInB5dGhvbjMiOwp9OwpleHBvcnQgY29uc3QgbG9hZENvbmZpZyA9ICgpID0+IHsKICAgIGxvYWREb3RFbnYocHJvY2Vzcy5lbnYuRkVERU9fQUdFTlRfRU5WIHx8ICIuZW52Iik7CiAgICBjb25zdCBmZWRlb1VybCA9IG9wdGlvbmFsKHByb2Nlc3MuZW52LkZFREVPX1VSTCk7CiAgICBjb25zdCBhZ2VudFRva2VuID0gb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fQUdFTlRfVE9LRU4pOwogICAgaWYgKCFmZWRlb1VybCkKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkZFREVPX1VSTCBmZWhsdCIpOwogICAgaWYgKCFhZ2VudFRva2VuKQogICAgICAgIHRocm93IG5ldyBFcnJvcigiRkVERU9fQUdFTlRfVE9LRU4gZmVobHQiKTsKICAgIHJldHVybiB7CiAgICAgICAgZmVkZW9Vcmw6IGZlZGVvVXJsLnJlcGxhY2UoL1wvKyQvLCAiIiksCiAgICAgICAgYWdlbnRUb2tlbiwKICAgICAgICBwb2xsU2Vjb25kczogbnVtYmVyRnJvbUVudihwcm9jZXNzLmVudi5GRURFT19QT0xMX1NFQ09ORFMsIDUpLAogICAgICAgIHdvcmtEaXI6IG9wdGlvbmFsKHByb2Nlc3MuZW52LkZFREVPX1dPUktfRElSKSB8fCBwYXRoLmpvaW4ob3MudG1wZGlyKCksICJmZWRlby1kZXZpY2UtYWdlbnQiKSwKICAgICAgICBzY2FubmVyTmFtZTogb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fU0NBTk5FUl9OQU1FKSwKICAgICAgICBwcmludGVyTmFtZTogb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fUFJJTlRFUl9OQU1FKSwKICAgICAgICBzY2FuRm9ybWF0OiBzY2FuRm9ybWF0RnJvbUVudihwcm9jZXNzLmVudi5GRURFT19TQ0FOX0ZPUk1BVCksCiAgICAgICAgc2NhblJlc29sdXRpb246IG51bWJlckZyb21FbnYocHJvY2Vzcy5lbnYuRkVERU9fU0NBTl9SRVNPTFVUSU9OLCAzMDApLAogICAgICAgIHNjYW5Nb2RlOiBvcHRpb25hbChwcm9jZXNzLmVudi5GRURFT19TQ0FOX01PREUpIHx8ICJDb2xvciIsCiAgICAgICAgc2NhblNvdXJjZTogb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fU0NBTl9TT1VSQ0UpLAogICAgICAgIHNjYW5UaW1lb3V0U2Vjb25kczogbnVtYmVyRnJvbUVudihwcm9jZXNzLmVudi5GRURFT19TQ0FOX1RJTUVPVVRfU0VDT05EUywgOTAwKSwKICAgICAgICBzY2FuUG9zdHByb2Nlc3M6IGJvb2xlYW5Gcm9tRW52KHByb2Nlc3MuZW52LkZFREVPX1NDQU5fUE9TVFBST0NFU1MsIGZhbHNlKSwKICAgICAgICBzY2FuQmFja2dyb3VuZENsZWFudXA6IGJvb2xlYW5Gcm9tRW52KHByb2Nlc3MuZW52LkZFREVPX1NDQU5fQkFDS0dST1VORF9DTEVBTlVQLCB0cnVlKSwKICAgICAgICBzY2FuT2NyOiBib29sZWFuRnJvbUVudihwcm9jZXNzLmVudi5GRURFT19TQ0FOX09DUiwgdHJ1ZSksCiAgICAgICAgc2Nhbk9jckxhbmd1YWdlOiBvcHRpb25hbChwcm9jZXNzLmVudi5GRURFT19TQ0FOX09DUl9MQU5HVUFHRSkgfHwgImRldStlbmciLAogICAgICAgIHBvc3Rwcm9jZXNzUHJvZmlsZTogcG9zdHByb2Nlc3NQcm9maWxlRnJvbUVudihwcm9jZXNzLmVudi5GRURFT19TQ0FOX1BPU1RQUk9DRVNTX1BST0ZJTEUpLAogICAgICAgIHBvc3Rwcm9jZXNzUHl0aG9uOiBvcHRpb25hbChwcm9jZXNzLmVudi5GRURFT19TQ0FOX1BPU1RQUk9DRVNTX1BZVEhPTikgfHwgZGVmYXVsdFBvc3Rwcm9jZXNzUHl0aG9uKCksCiAgICAgICAgcG9zdHByb2Nlc3NTdHJpY3Q6IGJvb2xlYW5Gcm9tRW52KHByb2Nlc3MuZW52LkZFREVPX1NDQU5fUE9TVFBST0NFU1NfU1RSSUNULCBmYWxzZSksCiAgICB9Owp9Owo=", "dist/env.js": "aW1wb3J0IHsgcmVhZEZpbGVTeW5jLCBleGlzdHNTeW5jIH0gZnJvbSAibm9kZTpmcyI7CmNvbnN0IHBhcnNlRW52TGluZSA9IChsaW5lKSA9PiB7CiAgICBjb25zdCB0cmltbWVkID0gbGluZS50cmltKCk7CiAgICBpZiAoIXRyaW1tZWQgfHwgdHJpbW1lZC5zdGFydHNXaXRoKCIjIikpCiAgICAgICAgcmV0dXJuIG51bGw7CiAgICBjb25zdCBzZXBhcmF0b3IgPSB0cmltbWVkLmluZGV4T2YoIj0iKTsKICAgIGlmIChzZXBhcmF0b3IgPT09IC0xKQogICAgICAgIHJldHVybiBudWxsOwogICAgY29uc3Qga2V5ID0gdHJpbW1lZC5zbGljZSgwLCBzZXBhcmF0b3IpLnRyaW0oKTsKICAgIGxldCB2YWx1ZSA9IHRyaW1tZWQuc2xpY2Uoc2VwYXJhdG9yICsgMSkudHJpbSgpOwogICAgaWYgKCh2YWx1ZS5zdGFydHNXaXRoKCJcIiIpICYmIHZhbHVlLmVuZHNXaXRoKCJcIiIpKSB8fAogICAgICAgICh2YWx1ZS5zdGFydHNXaXRoKCInIikgJiYgdmFsdWUuZW5kc1dpdGgoIiciKSkpIHsKICAgICAgICB2YWx1ZSA9IHZhbHVlLnNsaWNlKDEsIC0xKTsKICAgIH0KICAgIHJldHVybiB7IGtleSwgdmFsdWUgfTsKfTsKZXhwb3J0IGNvbnN0IGxvYWREb3RFbnYgPSAocGF0aCA9ICIuZW52IikgPT4gewogICAgaWYgKCFleGlzdHNTeW5jKHBhdGgpKQogICAgICAgIHJldHVybjsKICAgIGNvbnN0IGNvbnRlbnQgPSByZWFkRmlsZVN5bmMocGF0aCwgInV0ZjgiKTsKICAgIGZvciAoY29uc3QgbGluZSBvZiBjb250ZW50LnNwbGl0KC9ccj9cbi8pKSB7CiAgICAgICAgY29uc3QgcGFyc2VkID0gcGFyc2VFbnZMaW5lKGxpbmUpOwogICAgICAgIGlmICghcGFyc2VkKQogICAgICAgICAgICBjb250aW51ZTsKICAgICAgICBpZiAocHJvY2Vzcy5lbnZbcGFyc2VkLmtleV0gPT09IHVuZGVmaW5lZCkKICAgICAgICAgICAgcHJvY2Vzcy5lbnZbcGFyc2VkLmtleV0gPSBwYXJzZWQudmFsdWU7CiAgICB9Cn07Cg==", "dist/logger.js": "Y29uc3QgdGltZXN0YW1wID0gKCkgPT4gbmV3IERhdGUoKS50b0lTT1N0cmluZygpOwpleHBvcnQgY29uc3QgbG9nID0gewogICAgaW5mbyhtZXNzYWdlLCBtZXRhKSB7CiAgICAgICAgaWYgKG1ldGEgPT09IHVuZGVmaW5lZCkgewogICAgICAgICAgICBjb25zb2xlLmxvZyhgWyR7dGltZXN0YW1wKCl9XSBJTkZPICR7bWVzc2FnZX1gKTsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgIH0KICAgICAgICBjb25zb2xlLmxvZyhgWyR7dGltZXN0YW1wKCl9XSBJTkZPICR7bWVzc2FnZX1gLCBtZXRhKTsKICAgIH0sCiAgICB3YXJuKG1lc3NhZ2UsIG1ldGEpIHsKICAgICAgICBpZiAobWV0YSA9PT0gdW5kZWZpbmVkKSB7CiAgICAgICAgICAgIGNvbnNvbGUud2FybihgWyR7dGltZXN0YW1wKCl9XSBXQVJOICR7bWVzc2FnZX1gKTsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgIH0KICAgICAgICBjb25zb2xlLndhcm4oYFske3RpbWVzdGFtcCgpfV0gV0FSTiAke21lc3NhZ2V9YCwgbWV0YSk7CiAgICB9LAogICAgZXJyb3IobWVzc2FnZSwgbWV0YSkgewogICAgICAgIGlmIChtZXRhID09PSB1bmRlZmluZWQpIHsKICAgICAgICAgICAgY29uc29sZS5lcnJvcihgWyR7dGltZXN0YW1wKCl9XSBFUlJPUiAke21lc3NhZ2V9YCk7CiAgICAgICAgICAgIHJldHVybjsKICAgICAgICB9CiAgICAgICAgY29uc29sZS5lcnJvcihgWyR7dGltZXN0YW1wKCl9XSBFUlJPUiAke21lc3NhZ2V9YCwgbWV0YSk7CiAgICB9LAp9Owo=", - "dist/main.js": "IyEvdXNyL2Jpbi9lbnYgbm9kZQppbXBvcnQgb3MgZnJvbSAibm9kZTpvcyI7CmltcG9ydCBwYXRoIGZyb20gIm5vZGU6cGF0aCI7CmltcG9ydCB7IHJtIH0gZnJvbSAibm9kZTpmcy9wcm9taXNlcyI7CmltcG9ydCB7IEZlZGVvQXBpIH0gZnJvbSAiLi9hcGkuanMiOwppbXBvcnQgeyBsb2FkQ29uZmlnIH0gZnJvbSAiLi9jb25maWcuanMiOwppbXBvcnQgeyBsb2cgfSBmcm9tICIuL2xvZ2dlci5qcyI7CmltcG9ydCB7IGxpc3RQcmludGVycyB9IGZyb20gIi4vcHJpbnQvY3Vwcy5qcyI7CmltcG9ydCB7IGhhc1NhbmUsIGxpc3RTY2FubmVycywgcnVuU2NhbiB9IGZyb20gIi4vc2Nhbi9zYW5lLmpzIjsKY29uc3Qgc2xlZXAgPSAobXMpID0+IG5ldyBQcm9taXNlKChyZXNvbHZlKSA9PiBzZXRUaW1lb3V0KHJlc29sdmUsIG1zKSk7CmNvbnN0IHN0cmluZ2lmeUVycm9yID0gKGVycm9yKSA9PiB7CiAgICBpZiAoZXJyb3IgaW5zdGFuY2VvZiBFcnJvcikKICAgICAgICByZXR1cm4gZXJyb3IubWVzc2FnZTsKICAgIHJldHVybiBTdHJpbmcoZXJyb3IpOwp9Owpjb25zdCBtYWluID0gYXN5bmMgKCkgPT4gewogICAgY29uc3QgY29uZmlnID0gbG9hZENvbmZpZygpOwogICAgY29uc3QgYXBpID0gbmV3IEZlZGVvQXBpKGNvbmZpZyk7CiAgICBsb2cuaW5mbygiRkVERU8gR2Vyw6R0ZS1BZ2VudCBzdGFydGV0IiwgewogICAgICAgIHBsYXRmb3JtOiBwcm9jZXNzLnBsYXRmb3JtLAogICAgICAgIHdvcmtEaXI6IGNvbmZpZy53b3JrRGlyLAogICAgICAgIHBvbGxTZWNvbmRzOiBjb25maWcucG9sbFNlY29uZHMsCiAgICB9KTsKICAgIHdoaWxlICh0cnVlKSB7CiAgICAgICAgdHJ5IHsKICAgICAgICAgICAgY29uc3Qgc2Nhbm5lck5hbWVzID0gYXdhaXQgbGlzdFNjYW5uZXJzKCkuY2F0Y2goKGVycm9yKSA9PiB7CiAgICAgICAgICAgICAgICBsb2cud2FybigiU2Nhbm5lci1Fcmtlbm51bmcgZmVobGdlc2NobGFnZW4iLCBzdHJpbmdpZnlFcnJvcihlcnJvcikpOwogICAgICAgICAgICAgICAgcmV0dXJuIFtdOwogICAgICAgICAgICB9KTsKICAgICAgICAgICAgY29uc3QgcHJpbnRlck5hbWVzID0gYXdhaXQgbGlzdFByaW50ZXJzKCkuY2F0Y2goKCkgPT4gW10pOwogICAgICAgICAgICBjb25zdCBzY2FuQXZhaWxhYmxlID0gYXdhaXQgaGFzU2FuZSgpOwogICAgICAgICAgICBjb25zdCBoZWFydGJlYXRQYXlsb2FkID0gewogICAgICAgICAgICAgICAgY2FwYWJpbGl0aWVzOiB7CiAgICAgICAgICAgICAgICAgICAgc2Nhbjogc2NhbkF2YWlsYWJsZSwKICAgICAgICAgICAgICAgICAgICBwcmludDogcHJpbnRlck5hbWVzLmxlbmd0aCA+IDAsCiAgICAgICAgICAgICAgICAgICAgcGxhdGZvcm06IHByb2Nlc3MucGxhdGZvcm0sCiAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgc2Nhbm5lck5hbWVzLAogICAgICAgICAgICAgICAgcHJpbnRlck5hbWVzLAogICAgICAgICAgICAgICAgZGVidWdJbmZvOiB7CiAgICAgICAgICAgICAgICAgICAgaG9zdG5hbWU6IG9zLmhvc3RuYW1lKCksCiAgICAgICAgICAgICAgICAgICAgcmVsZWFzZTogb3MucmVsZWFzZSgpLAogICAgICAgICAgICAgICAgICAgIGFyY2g6IG9zLmFyY2goKSwKICAgICAgICAgICAgICAgICAgICBub2RlOiBwcm9jZXNzLnZlcnNpb24sCiAgICAgICAgICAgICAgICAgICAgdXB0aW1lU2Vjb25kczogTWF0aC5yb3VuZChvcy51cHRpbWUoKSksCiAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICB9OwogICAgICAgICAgICBjb25zdCBoZWFydGJlYXQgPSBhd2FpdCBhcGkuaGVhcnRiZWF0KGhlYXJ0YmVhdFBheWxvYWQpOwogICAgICAgICAgICBpZiAoaGVhcnRiZWF0LnBlbmRpbmdTY2FuSm9icyA+IDApIHsKICAgICAgICAgICAgICAgIGxvZy5pbmZvKGAke2hlYXJ0YmVhdC5wZW5kaW5nU2NhbkpvYnN9IFNjYW4tQXVmdHJhZy9BdWZ0csOkZ2Ugd2FydGVuYCk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgY29uc3QgbmV4dCA9IGF3YWl0IGFwaS5uZXh0U2NhbkpvYigpOwogICAgICAgICAgICBpZiAoIW5leHQuam9iKSB7CiAgICAgICAgICAgICAgICBhd2FpdCBzbGVlcChjb25maWcucG9sbFNlY29uZHMgKiAxMDAwKTsKICAgICAgICAgICAgICAgIGNvbnRpbnVlOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGxvZy5pbmZvKCJTY2FuLUF1ZnRyYWcgd2lyZCBhdXNnZWbDvGhydCIsIHsKICAgICAgICAgICAgICAgIGpvYklkOiBuZXh0LmpvYi5pZCwKICAgICAgICAgICAgICAgIHRlbmFudElkOiBuZXh0LmpvYi50ZW5hbnRJZCwKICAgICAgICAgICAgICAgIHNjYW5uZXJOYW1lOiBuZXh0LmpvYi5zY2FubmVyTmFtZSB8fCBjb25maWcuc2Nhbm5lck5hbWUgfHwgImRlZmF1bHQiLAogICAgICAgICAgICB9KTsKICAgICAgICAgICAgLy8gS2VlcCBvbmxpbmUgc3RhdHVzIGZyZXNoIGR1cmluZyBsb25nIHNjYW5zIHdpdGhvdXQgcHJvYmluZyBhbiBvY2N1cGllZCBVU0Igc2Nhbm5lci4KICAgICAgICAgICAgbGV0IGhlYXJ0YmVhdFJ1bm5pbmcgPSBmYWxzZTsKICAgICAgICAgICAgY29uc3QgaGVhcnRiZWF0VGltZXIgPSBzZXRJbnRlcnZhbChhc3luYyAoKSA9PiB7CiAgICAgICAgICAgICAgICBpZiAoaGVhcnRiZWF0UnVubmluZykKICAgICAgICAgICAgICAgICAgICByZXR1cm47CiAgICAgICAgICAgICAgICBoZWFydGJlYXRSdW5uaW5nID0gdHJ1ZTsKICAgICAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICAgICAgYXdhaXQgYXBpLmhlYXJ0YmVhdChoZWFydGJlYXRQYXlsb2FkKTsKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIGNhdGNoIChlcnJvcikgewogICAgICAgICAgICAgICAgICAgIGxvZy53YXJuKCJIZWFydGJlYXQgd8OkaHJlbmQgU2NhbiBmZWhsZ2VzY2hsYWdlbiIsIHN0cmluZ2lmeUVycm9yKGVycm9yKSk7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBmaW5hbGx5IHsKICAgICAgICAgICAgICAgICAgICBoZWFydGJlYXRSdW5uaW5nID0gZmFsc2U7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0sIDMwXzAwMCk7CiAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICBhd2FpdCBhcGkudXBkYXRlU2NhbkpvYlN0YXR1cyhuZXh0LmpvYi5pZCwgInJ1bm5pbmciKTsKICAgICAgICAgICAgICAgIGNvbnN0IGpvYklkID0gbmV4dC5qb2IuaWQ7CiAgICAgICAgICAgICAgICBjb25zdCByZXBvcnRQcm9ncmVzcyA9IGFzeW5jIChtZXNzYWdlKSA9PiB7CiAgICAgICAgICAgICAgICAgICAgYXdhaXQgYXBpLnVwZGF0ZVNjYW5Kb2JTdGF0dXMoam9iSWQsICJydW5uaW5nIiwgbWVzc2FnZSkuY2F0Y2goKGVycm9yKSA9PiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGxvZy53YXJuKCJTY2FuLVN0YXR1cyBrb25udGUgbmljaHQgw7xiZXJ0cmFnZW4gd2VyZGVuIiwgc3RyaW5naWZ5RXJyb3IoZXJyb3IpKTsKICAgICAgICAgICAgICAgICAgICB9KTsKICAgICAgICAgICAgICAgIH07CiAgICAgICAgICAgICAgICBjb25zdCBzY2FuUmVzdWx0ID0gYXdhaXQgcnVuU2Nhbihjb25maWcsIG5leHQuam9iLCByZXBvcnRQcm9ncmVzcyk7CiAgICAgICAgICAgICAgICBhd2FpdCByZXBvcnRQcm9ncmVzcyhgJHtzY2FuUmVzdWx0LnBhZ2VDb3VudH0gU2VpdGVuIGdlc2Nhbm50LiAke3NjYW5SZXN1bHQud2FybmluZ3Muam9pbigiICIpfWAudHJpbSgpKTsKICAgICAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICAgICAgYXdhaXQgYXBpLnVwbG9hZFNjYW4oam9iSWQsIHNjYW5SZXN1bHQpOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGAke3N0cmluZ2lmeUVycm9yKGVycm9yKX0gU2Nhbi1EYXRlaSBibGVpYnQgenVyIFdpZWRlcmhlcnN0ZWxsdW5nIHVudGVyICR7c2NhblJlc3VsdC5wYXRofS5gKTsKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIGF3YWl0IHJtKHBhdGguZGlybmFtZShzY2FuUmVzdWx0LnBhdGgpLCB7IHJlY3Vyc2l2ZTogdHJ1ZSwgZm9yY2U6IHRydWUgfSkuY2F0Y2goKGVycm9yKSA9PiB7CiAgICAgICAgICAgICAgICAgICAgbG9nLndhcm4oIlNjYW4gZ2VzcGVpY2hlcnQ7IHRlbXBvcsOkcmUgRGF0ZWllbiBrb25udGVuIG5pY2h0IGVudGZlcm50IHdlcmRlbiIsIHN0cmluZ2lmeUVycm9yKGVycm9yKSk7CiAgICAgICAgICAgICAgICB9KTsKICAgICAgICAgICAgICAgIGxvZy5pbmZvKCJTY2FuLUF1ZnRyYWcgYWJnZXNjaGxvc3NlbiIsIHsKICAgICAgICAgICAgICAgICAgICBqb2JJZDogbmV4dC5qb2IuaWQsCiAgICAgICAgICAgICAgICAgICAgZmlsZTogc2NhblJlc3VsdC5maWxlbmFtZSwKICAgICAgICAgICAgICAgICAgICBwYWdlczogc2NhblJlc3VsdC5wYWdlQ291bnQsCiAgICAgICAgICAgICAgICB9KTsKICAgICAgICAgICAgfQogICAgICAgICAgICBjYXRjaCAoZXJyb3IpIHsKICAgICAgICAgICAgICAgIGNvbnN0IG1lc3NhZ2UgPSBzdHJpbmdpZnlFcnJvcihlcnJvcik7CiAgICAgICAgICAgICAgICBsb2cuZXJyb3IoIlNjYW4tQXVmdHJhZyBmZWhsZ2VzY2hsYWdlbiIsIHsKICAgICAgICAgICAgICAgICAgICBqb2JJZDogbmV4dC5qb2IuaWQsCiAgICAgICAgICAgICAgICAgICAgbWVzc2FnZSwKICAgICAgICAgICAgICAgIH0pOwogICAgICAgICAgICAgICAgYXdhaXQgYXBpLnVwZGF0ZVNjYW5Kb2JTdGF0dXMobmV4dC5qb2IuaWQsICJmYWlsZWQiLCBtZXNzYWdlKTsKICAgICAgICAgICAgfQogICAgICAgICAgICBmaW5hbGx5IHsKICAgICAgICAgICAgICAgIGNsZWFySW50ZXJ2YWwoaGVhcnRiZWF0VGltZXIpOwogICAgICAgICAgICB9CiAgICAgICAgfQogICAgICAgIGNhdGNoIChlcnJvcikgewogICAgICAgICAgICBsb2cuZXJyb3IoIkFnZW50LVNjaGxlaWZlIGZlaGxnZXNjaGxhZ2VuIiwgc3RyaW5naWZ5RXJyb3IoZXJyb3IpKTsKICAgICAgICAgICAgYXdhaXQgc2xlZXAoY29uZmlnLnBvbGxTZWNvbmRzICogMTAwMCk7CiAgICAgICAgfQogICAgfQp9OwptYWluKCkuY2F0Y2goKGVycm9yKSA9PiB7CiAgICBsb2cuZXJyb3IoIkFnZW50IGtvbm50ZSBuaWNodCBnZXN0YXJ0ZXQgd2VyZGVuIiwgc3RyaW5naWZ5RXJyb3IoZXJyb3IpKTsKICAgIHByb2Nlc3MuZXhpdCgxKTsKfSk7Cg==", + "dist/main.js": "IyEvdXNyL2Jpbi9lbnYgbm9kZQppbXBvcnQgb3MgZnJvbSAibm9kZTpvcyI7CmltcG9ydCBwYXRoIGZyb20gIm5vZGU6cGF0aCI7CmltcG9ydCB7IHJtIH0gZnJvbSAibm9kZTpmcy9wcm9taXNlcyI7CmltcG9ydCB7IEZlZGVvQXBpIH0gZnJvbSAiLi9hcGkuanMiOwppbXBvcnQgeyBsb2FkQ29uZmlnIH0gZnJvbSAiLi9jb25maWcuanMiOwppbXBvcnQgeyBsb2cgfSBmcm9tICIuL2xvZ2dlci5qcyI7CmltcG9ydCB7IGxpc3RQcmludGVycyB9IGZyb20gIi4vcHJpbnQvY3Vwcy5qcyI7CmltcG9ydCB7IGhhc1NhbmUsIGxpc3RTY2FubmVycywgcnVuU2NhbiB9IGZyb20gIi4vc2Nhbi9zYW5lLmpzIjsKaW1wb3J0IHsgcHJvY2Vzc2luZ0NhcGFiaWxpdGllcyB9IGZyb20gIi4vc2Nhbi9wb3N0cHJvY2Vzcy5qcyI7CmNvbnN0IHNsZWVwID0gKG1zKSA9PiBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4gc2V0VGltZW91dChyZXNvbHZlLCBtcykpOwpjb25zdCBzdHJpbmdpZnlFcnJvciA9IChlcnJvcikgPT4gewogICAgaWYgKGVycm9yIGluc3RhbmNlb2YgRXJyb3IpCiAgICAgICAgcmV0dXJuIGVycm9yLm1lc3NhZ2U7CiAgICByZXR1cm4gU3RyaW5nKGVycm9yKTsKfTsKY29uc3QgbWFpbiA9IGFzeW5jICgpID0+IHsKICAgIGNvbnN0IGNvbmZpZyA9IGxvYWRDb25maWcoKTsKICAgIGNvbnN0IGFwaSA9IG5ldyBGZWRlb0FwaShjb25maWcpOwogICAgY29uc3QgcHJvY2Vzc2luZyA9IGF3YWl0IHByb2Nlc3NpbmdDYXBhYmlsaXRpZXMoY29uZmlnKTsKICAgIGxvZy5pbmZvKCJGRURFTyBHZXLDpHRlLUFnZW50IHN0YXJ0ZXQiLCB7CiAgICAgICAgcGxhdGZvcm06IHByb2Nlc3MucGxhdGZvcm0sCiAgICAgICAgd29ya0RpcjogY29uZmlnLndvcmtEaXIsCiAgICAgICAgcG9sbFNlY29uZHM6IGNvbmZpZy5wb2xsU2Vjb25kcywKICAgIH0pOwogICAgd2hpbGUgKHRydWUpIHsKICAgICAgICB0cnkgewogICAgICAgICAgICBjb25zdCBzY2FubmVyTmFtZXMgPSBhd2FpdCBsaXN0U2Nhbm5lcnMoKS5jYXRjaCgoZXJyb3IpID0+IHsKICAgICAgICAgICAgICAgIGxvZy53YXJuKCJTY2FubmVyLUVya2VubnVuZyBmZWhsZ2VzY2hsYWdlbiIsIHN0cmluZ2lmeUVycm9yKGVycm9yKSk7CiAgICAgICAgICAgICAgICByZXR1cm4gW107CiAgICAgICAgICAgIH0pOwogICAgICAgICAgICBjb25zdCBwcmludGVyTmFtZXMgPSBhd2FpdCBsaXN0UHJpbnRlcnMoKS5jYXRjaCgoKSA9PiBbXSk7CiAgICAgICAgICAgIGNvbnN0IHNjYW5BdmFpbGFibGUgPSBhd2FpdCBoYXNTYW5lKCk7CiAgICAgICAgICAgIGNvbnN0IGhlYXJ0YmVhdFBheWxvYWQgPSB7CiAgICAgICAgICAgICAgICBjYXBhYmlsaXRpZXM6IHsKICAgICAgICAgICAgICAgICAgICBzY2FuOiBzY2FuQXZhaWxhYmxlLAogICAgICAgICAgICAgICAgICAgIC4uLnByb2Nlc3NpbmcsCiAgICAgICAgICAgICAgICAgICAgcHJpbnQ6IHByaW50ZXJOYW1lcy5sZW5ndGggPiAwLAogICAgICAgICAgICAgICAgICAgIHBsYXRmb3JtOiBwcm9jZXNzLnBsYXRmb3JtLAogICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgIHNjYW5uZXJOYW1lcywKICAgICAgICAgICAgICAgIHByaW50ZXJOYW1lcywKICAgICAgICAgICAgICAgIGRlYnVnSW5mbzogewogICAgICAgICAgICAgICAgICAgIGhvc3RuYW1lOiBvcy5ob3N0bmFtZSgpLAogICAgICAgICAgICAgICAgICAgIHJlbGVhc2U6IG9zLnJlbGVhc2UoKSwKICAgICAgICAgICAgICAgICAgICBhcmNoOiBvcy5hcmNoKCksCiAgICAgICAgICAgICAgICAgICAgbm9kZTogcHJvY2Vzcy52ZXJzaW9uLAogICAgICAgICAgICAgICAgICAgIHVwdGltZVNlY29uZHM6IE1hdGgucm91bmQob3MudXB0aW1lKCkpLAogICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgfTsKICAgICAgICAgICAgY29uc3QgaGVhcnRiZWF0ID0gYXdhaXQgYXBpLmhlYXJ0YmVhdChoZWFydGJlYXRQYXlsb2FkKTsKICAgICAgICAgICAgaWYgKGhlYXJ0YmVhdC5wZW5kaW5nU2NhbkpvYnMgPiAwKSB7CiAgICAgICAgICAgICAgICBsb2cuaW5mbyhgJHtoZWFydGJlYXQucGVuZGluZ1NjYW5Kb2JzfSBTY2FuLUF1ZnRyYWcvQXVmdHLDpGdlIHdhcnRlbmApOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGNvbnN0IG5leHQgPSBhd2FpdCBhcGkubmV4dFNjYW5Kb2IoKTsKICAgICAgICAgICAgaWYgKCFuZXh0LmpvYikgewogICAgICAgICAgICAgICAgYXdhaXQgc2xlZXAoY29uZmlnLnBvbGxTZWNvbmRzICogMTAwMCk7CiAgICAgICAgICAgICAgICBjb250aW51ZTsKICAgICAgICAgICAgfQogICAgICAgICAgICBsb2cuaW5mbygiU2Nhbi1BdWZ0cmFnIHdpcmQgYXVzZ2Vmw7xocnQiLCB7CiAgICAgICAgICAgICAgICBqb2JJZDogbmV4dC5qb2IuaWQsCiAgICAgICAgICAgICAgICB0ZW5hbnRJZDogbmV4dC5qb2IudGVuYW50SWQsCiAgICAgICAgICAgICAgICBzY2FubmVyTmFtZTogbmV4dC5qb2Iuc2Nhbm5lck5hbWUgfHwgY29uZmlnLnNjYW5uZXJOYW1lIHx8ICJkZWZhdWx0IiwKICAgICAgICAgICAgfSk7CiAgICAgICAgICAgIC8vIEtlZXAgb25saW5lIHN0YXR1cyBmcmVzaCBkdXJpbmcgbG9uZyBzY2FucyB3aXRob3V0IHByb2JpbmcgYW4gb2NjdXBpZWQgVVNCIHNjYW5uZXIuCiAgICAgICAgICAgIGxldCBoZWFydGJlYXRSdW5uaW5nID0gZmFsc2U7CiAgICAgICAgICAgIGNvbnN0IGhlYXJ0YmVhdFRpbWVyID0gc2V0SW50ZXJ2YWwoYXN5bmMgKCkgPT4gewogICAgICAgICAgICAgICAgaWYgKGhlYXJ0YmVhdFJ1bm5pbmcpCiAgICAgICAgICAgICAgICAgICAgcmV0dXJuOwogICAgICAgICAgICAgICAgaGVhcnRiZWF0UnVubmluZyA9IHRydWU7CiAgICAgICAgICAgICAgICB0cnkgewogICAgICAgICAgICAgICAgICAgIGF3YWl0IGFwaS5oZWFydGJlYXQoaGVhcnRiZWF0UGF5bG9hZCk7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBjYXRjaCAoZXJyb3IpIHsKICAgICAgICAgICAgICAgICAgICBsb2cud2FybigiSGVhcnRiZWF0IHfDpGhyZW5kIFNjYW4gZmVobGdlc2NobGFnZW4iLCBzdHJpbmdpZnlFcnJvcihlcnJvcikpOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgZmluYWxseSB7CiAgICAgICAgICAgICAgICAgICAgaGVhcnRiZWF0UnVubmluZyA9IGZhbHNlOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9LCAzMF8wMDApOwogICAgICAgICAgICB0cnkgewogICAgICAgICAgICAgICAgYXdhaXQgYXBpLnVwZGF0ZVNjYW5Kb2JTdGF0dXMobmV4dC5qb2IuaWQsICJydW5uaW5nIik7CiAgICAgICAgICAgICAgICBjb25zdCBqb2JJZCA9IG5leHQuam9iLmlkOwogICAgICAgICAgICAgICAgY29uc3QgcmVwb3J0UHJvZ3Jlc3MgPSBhc3luYyAobWVzc2FnZSkgPT4gewogICAgICAgICAgICAgICAgICAgIGF3YWl0IGFwaS51cGRhdGVTY2FuSm9iU3RhdHVzKGpvYklkLCAicnVubmluZyIsIG1lc3NhZ2UpLmNhdGNoKChlcnJvcikgPT4gewogICAgICAgICAgICAgICAgICAgICAgICBsb2cud2FybigiU2Nhbi1TdGF0dXMga29ubnRlIG5pY2h0IMO8YmVydHJhZ2VuIHdlcmRlbiIsIHN0cmluZ2lmeUVycm9yKGVycm9yKSk7CiAgICAgICAgICAgICAgICAgICAgfSk7CiAgICAgICAgICAgICAgICB9OwogICAgICAgICAgICAgICAgY29uc3Qgc2NhblJlc3VsdCA9IGF3YWl0IHJ1blNjYW4oY29uZmlnLCBuZXh0LmpvYiwgcmVwb3J0UHJvZ3Jlc3MpOwogICAgICAgICAgICAgICAgYXdhaXQgcmVwb3J0UHJvZ3Jlc3MoYCR7c2NhblJlc3VsdC5wYWdlQ291bnR9IFNlaXRlbiBnZXNjYW5udC4gJHtzY2FuUmVzdWx0Lndhcm5pbmdzLmpvaW4oIiAiKX1gLnRyaW0oKSk7CiAgICAgICAgICAgICAgICB0cnkgewogICAgICAgICAgICAgICAgICAgIGF3YWl0IGFwaS51cGxvYWRTY2FuKGpvYklkLCBzY2FuUmVzdWx0KTsKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIGNhdGNoIChlcnJvcikgewogICAgICAgICAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcihgJHtzdHJpbmdpZnlFcnJvcihlcnJvcil9IFNjYW4tRGF0ZWkgYmxlaWJ0IHp1ciBXaWVkZXJoZXJzdGVsbHVuZyB1bnRlciAke3NjYW5SZXN1bHQucGF0aH0uYCk7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBhd2FpdCBybShwYXRoLmRpcm5hbWUoc2NhblJlc3VsdC5wYXRoKSwgeyByZWN1cnNpdmU6IHRydWUsIGZvcmNlOiB0cnVlIH0pLmNhdGNoKChlcnJvcikgPT4gewogICAgICAgICAgICAgICAgICAgIGxvZy53YXJuKCJTY2FuIGdlc3BlaWNoZXJ0OyB0ZW1wb3LDpHJlIERhdGVpZW4ga29ubnRlbiBuaWNodCBlbnRmZXJudCB3ZXJkZW4iLCBzdHJpbmdpZnlFcnJvcihlcnJvcikpOwogICAgICAgICAgICAgICAgfSk7CiAgICAgICAgICAgICAgICBsb2cuaW5mbygiU2Nhbi1BdWZ0cmFnIGFiZ2VzY2hsb3NzZW4iLCB7CiAgICAgICAgICAgICAgICAgICAgam9iSWQ6IG5leHQuam9iLmlkLAogICAgICAgICAgICAgICAgICAgIGZpbGU6IHNjYW5SZXN1bHQuZmlsZW5hbWUsCiAgICAgICAgICAgICAgICAgICAgcGFnZXM6IHNjYW5SZXN1bHQucGFnZUNvdW50LAogICAgICAgICAgICAgICAgfSk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgICAgICAgICBjb25zdCBtZXNzYWdlID0gc3RyaW5naWZ5RXJyb3IoZXJyb3IpOwogICAgICAgICAgICAgICAgbG9nLmVycm9yKCJTY2FuLUF1ZnRyYWcgZmVobGdlc2NobGFnZW4iLCB7CiAgICAgICAgICAgICAgICAgICAgam9iSWQ6IG5leHQuam9iLmlkLAogICAgICAgICAgICAgICAgICAgIG1lc3NhZ2UsCiAgICAgICAgICAgICAgICB9KTsKICAgICAgICAgICAgICAgIGF3YWl0IGFwaS51cGRhdGVTY2FuSm9iU3RhdHVzKG5leHQuam9iLmlkLCAiZmFpbGVkIiwgbWVzc2FnZSk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgZmluYWxseSB7CiAgICAgICAgICAgICAgICBjbGVhckludGVydmFsKGhlYXJ0YmVhdFRpbWVyKTsKICAgICAgICAgICAgfQogICAgICAgIH0KICAgICAgICBjYXRjaCAoZXJyb3IpIHsKICAgICAgICAgICAgbG9nLmVycm9yKCJBZ2VudC1TY2hsZWlmZSBmZWhsZ2VzY2hsYWdlbiIsIHN0cmluZ2lmeUVycm9yKGVycm9yKSk7CiAgICAgICAgICAgIGF3YWl0IHNsZWVwKGNvbmZpZy5wb2xsU2Vjb25kcyAqIDEwMDApOwogICAgICAgIH0KICAgIH0KfTsKbWFpbigpLmNhdGNoKChlcnJvcikgPT4gewogICAgbG9nLmVycm9yKCJBZ2VudCBrb25udGUgbmljaHQgZ2VzdGFydGV0IHdlcmRlbiIsIHN0cmluZ2lmeUVycm9yKGVycm9yKSk7CiAgICBwcm9jZXNzLmV4aXQoMSk7Cn0pOwo=", "dist/print/cups.js": "aW1wb3J0IHsgY29tbWFuZEV4aXN0cywgcnVuQ29tbWFuZCB9IGZyb20gIi4uL2NvbW1hbmRzLmpzIjsKZXhwb3J0IGNvbnN0IGhhc0N1cHMgPSAoKSA9PiBjb21tYW5kRXhpc3RzKCJscHN0YXQiKTsKZXhwb3J0IGNvbnN0IGxpc3RQcmludGVycyA9IGFzeW5jICgpID0+IHsKICAgIGlmICghYXdhaXQgaGFzQ3VwcygpKQogICAgICAgIHJldHVybiBbXTsKICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHJ1bkNvbW1hbmQoImxwc3RhdCIsIFsiLXAiXSwgeyB0aW1lb3V0TXM6IDEwXzAwMCB9KTsKICAgIGlmIChyZXN1bHQuY29kZSAhPT0gMCkKICAgICAgICByZXR1cm4gW107CiAgICByZXR1cm4gcmVzdWx0LnN0ZG91dAogICAgICAgIC5zcGxpdCgvXHI/XG4vKQogICAgICAgIC5tYXAoKGxpbmUpID0+IGxpbmUubWF0Y2goL15wcmludGVyXHMrKFxTKykvKT8uWzFdKQogICAgICAgIC5maWx0ZXIoKHByaW50ZXIpID0+IEJvb2xlYW4ocHJpbnRlcikpOwp9Owo=", - "dist/scan/postprocess.js": "aW1wb3J0IHBhdGggZnJvbSAibm9kZTpwYXRoIjsKaW1wb3J0IHsgZmlsZVVSTFRvUGF0aCB9IGZyb20gIm5vZGU6dXJsIjsKaW1wb3J0IHsgcnVuQ29tbWFuZCB9IGZyb20gIi4uL2NvbW1hbmRzLmpzIjsKY29uc3QgYWdlbnRSb290ID0gcGF0aC5yZXNvbHZlKHBhdGguZGlybmFtZShmaWxlVVJMVG9QYXRoKGltcG9ydC5tZXRhLnVybCkpLCAiLi4vLi4iKTsKY29uc3QgZG9jdW1lbnRTY3JpcHQgPSBwYXRoLmpvaW4oYWdlbnRSb290LCAic2NyaXB0cy9zY2FuX2RvY3VtZW50LnB5Iik7CmV4cG9ydCBjb25zdCBjaGVja1NjYW5SdW50aW1lID0gYXN5bmMgKGNvbmZpZywgcG9zdHByb2Nlc3MpID0+IHsKICAgIGNvbnN0IGltcG9ydHMgPSBwb3N0cHJvY2VzcyAmJiBjb25maWcucG9zdHByb2Nlc3NTdHJpY3QgPyAiaW1wb3J0IFBJTCwgY3YyLCBudW1weSIgOiAiaW1wb3J0IFBJTCI7CiAgICB0cnkgewogICAgICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHJ1bkNvbW1hbmQoY29uZmlnLnBvc3Rwcm9jZXNzUHl0aG9uLCBbIi1jIiwgaW1wb3J0c10sIHsgdGltZW91dE1zOiAxMF8wMDAgfSk7CiAgICAgICAgaWYgKHJlc3VsdC5jb2RlID09PSAwKQogICAgICAgICAgICByZXR1cm47CiAgICB9CiAgICBjYXRjaCB7CiAgICAgICAgLy8gUmVwb3J0IG1pc3NpbmcgcnVudGltZXMgYmVmb3JlIHRoZSBzY2FubmVyIGNvbnN1bWVzIGFueSBwYXBlci4KICAgIH0KICAgIHRocm93IG5ldyBFcnJvcigiUHl0aG9uL1BpbGxvdyAoYmVpIHN0cmlrdGVyIE5hY2hiZWFyYmVpdHVuZyB6dXPDpHR6bGljaCBPcGVuQ1YpIGZlaGx0LiBCaXR0ZSBucG0gcnVuIHNldHVwOm9wZW5jdiBhdXNmw7xocmVuIG9kZXIgcHl0aG9uMy1waWwgaW5zdGFsbGllcmVuLiIpOwp9OwpleHBvcnQgY29uc3QgY3JlYXRlU2NhbkRvY3VtZW50ID0gYXN5bmMgKGNvbmZpZywgcGFnZXMsIG91dHB1dFBhdGgsIHJlc29sdXRpb24sIHBvc3Rwcm9jZXNzLCBwcm9maWxlKSA9PiB7CiAgICBjb25zdCBhcmdzID0gW2RvY3VtZW50U2NyaXB0LCAiLS1vdXRwdXQiLCBvdXRwdXRQYXRoLCAiLS1yZXNvbHV0aW9uIiwgU3RyaW5nKHJlc29sdXRpb24pLCAiLS1wcm9maWxlIiwgcHJvZmlsZV07CiAgICBpZiAocG9zdHByb2Nlc3MpCiAgICAgICAgYXJncy5wdXNoKCItLXBvc3Rwcm9jZXNzIik7CiAgICBpZiAoY29uZmlnLnBvc3Rwcm9jZXNzU3RyaWN0KQogICAgICAgIGFyZ3MucHVzaCgiLS1zdHJpY3QiKTsKICAgIGFyZ3MucHVzaCgiLS1pbnB1dHMiLCAuLi5wYWdlcyk7CiAgICBjb25zdCByZXN1bHQgPSBhd2FpdCBydW5Db21tYW5kKGNvbmZpZy5wb3N0cHJvY2Vzc1B5dGhvbiwgYXJncywgewogICAgICAgIHRpbWVvdXRNczogY29uZmlnLnNjYW5UaW1lb3V0U2Vjb25kcyAqIDEwMDAsCiAgICB9KTsKICAgIGlmIChyZXN1bHQuY29kZSAhPT0gMCkKICAgICAgICB0aHJvdyBuZXcgRXJyb3IocmVzdWx0LnN0ZGVyciB8fCAiU2Nhbi1EYXRlaSBrb25udGUgbmljaHQgZXJzdGVsbHQgd2VyZGVuIik7CiAgICBjb25zdCBkZXRhaWxzID0gSlNPTi5wYXJzZShyZXN1bHQuc3Rkb3V0KTsKICAgIHJldHVybiB7CiAgICAgICAgcGF0aDogb3V0cHV0UGF0aCwKICAgICAgICBmaWxlbmFtZTogcGF0aC5iYXNlbmFtZShvdXRwdXRQYXRoKSwKICAgICAgICBtaW1lVHlwZTogb3V0cHV0UGF0aC5lbmRzV2l0aCgiLnBkZiIpID8gImFwcGxpY2F0aW9uL3BkZiIgOiBvdXRwdXRQYXRoLmVuZHNXaXRoKCIucG5nIikgPyAiaW1hZ2UvcG5nIiA6ICJpbWFnZS90aWZmIiwKICAgICAgICAuLi5kZXRhaWxzLAogICAgfTsKfTsK", - "dist/scan/sane.js": "aW1wb3J0IHsgbWtkaXIsIG1rZHRlbXAsIHJlYWRkaXIsIHN0YXQgfSBmcm9tICJub2RlOmZzL3Byb21pc2VzIjsKaW1wb3J0IHBhdGggZnJvbSAibm9kZTpwYXRoIjsKaW1wb3J0IHsgY29tbWFuZEV4aXN0cywgcnVuQ29tbWFuZCB9IGZyb20gIi4uL2NvbW1hbmRzLmpzIjsKaW1wb3J0IHsgY2hlY2tTY2FuUnVudGltZSwgY3JlYXRlU2NhbkRvY3VtZW50IH0gZnJvbSAiLi9wb3N0cHJvY2Vzcy5qcyI7CmNvbnN0IHN0cmluZ1NldHRpbmcgPSAoc2V0dGluZ3MsIGtleSkgPT4gewogICAgY29uc3QgdmFsdWUgPSBzZXR0aW5nc1trZXldOwogICAgcmV0dXJuIHR5cGVvZiB2YWx1ZSA9PT0gInN0cmluZyIgJiYgdmFsdWUudHJpbSgpID8gdmFsdWUudHJpbSgpIDogdW5kZWZpbmVkOwp9Owpjb25zdCBib29sZWFuU2V0dGluZyA9IChzZXR0aW5ncywga2V5LCBmYWxsYmFjaykgPT4gewogICAgY29uc3QgdmFsdWUgPSBzZXR0aW5nc1trZXldOwogICAgaWYgKHR5cGVvZiB2YWx1ZSA9PT0gImJvb2xlYW4iKQogICAgICAgIHJldHVybiB2YWx1ZTsKICAgIGlmICh0eXBlb2YgdmFsdWUgPT09ICJzdHJpbmciKQogICAgICAgIHJldHVybiBbIjEiLCAidHJ1ZSIsICJ5ZXMiLCAiamEiLCAib24iXS5pbmNsdWRlcyh2YWx1ZS50cmltKCkudG9Mb3dlckNhc2UoKSk7CiAgICByZXR1cm4gZmFsbGJhY2s7Cn07CmV4cG9ydCBjb25zdCBoYXNTYW5lID0gKCkgPT4gY29tbWFuZEV4aXN0cygic2NhbmltYWdlIik7CmV4cG9ydCBjb25zdCBsaXN0U2Nhbm5lcnMgPSBhc3luYyAoKSA9PiB7CiAgICBpZiAoIWF3YWl0IGhhc1NhbmUoKSkKICAgICAgICByZXR1cm4gW107CiAgICAvLyBTQU5FIGFsc28gcHJvYmVzIG5ldHdvcmsgYmFja2VuZHM7IHRoZXNlIGNhbiB0YWtlIG1vcmUgdGhhbiB0ZW4gc2Vjb25kcwogICAgLy8gZXZlbiB3aGVuIHRoZSBVU0Igc2Nhbm5lciBpcyBhbHJlYWR5IGF2YWlsYWJsZS4KICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHJ1bkNvbW1hbmQoInNjYW5pbWFnZSIsIFsiLUwiXSwgeyB0aW1lb3V0TXM6IDMwXzAwMCB9KTsKICAgIGlmIChyZXN1bHQuY29kZSAhPT0gMCkKICAgICAgICByZXR1cm4gW107CiAgICByZXR1cm4gcmVzdWx0LnN0ZG91dC5zcGxpdCgvXHI/XG4vKQogICAgICAgIC5tYXAoKGxpbmUpID0+IGxpbmUudHJpbSgpLm1hdGNoKC9eZGV2aWNlIGAoW14nXSspJy8pPy5bMV0pCiAgICAgICAgLmZpbHRlcigoZGV2aWNlKSA9PiBCb29sZWFuKGRldmljZSkpOwp9OwpleHBvcnQgY29uc3QgcnVuU2NhbiA9IGFzeW5jIChjb25maWcsIGpvYiwgcmVwb3J0UHJvZ3Jlc3MgPSBhc3luYyAoKSA9PiB7IH0pID0+IHsKICAgIGlmICghYXdhaXQgaGFzU2FuZSgpKQogICAgICAgIHRocm93IG5ldyBFcnJvcigic2NhbmltYWdlIGlzdCBuaWNodCBpbnN0YWxsaWVydCBvZGVyIG5pY2h0IGltIFBBVEgiKTsKICAgIGNvbnN0IHNldHRpbmdzID0gam9iLnNldHRpbmdzIHx8IHt9OwogICAgY29uc3QgZm9ybWF0ID0gc3RyaW5nU2V0dGluZyhzZXR0aW5ncywgImZvcm1hdCIpIHx8IGNvbmZpZy5zY2FuRm9ybWF0OwogICAgaWYgKCFbInBkZiIsICJwbmciLCAidGlmZiJdLmluY2x1ZGVzKGZvcm1hdCkpCiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCJTY2FuLUZvcm1hdCBtdXNzIFBERiwgUE5HIG9kZXIgVElGRiBzZWluIik7CiAgICBjb25zdCByZXNvbHV0aW9uID0gTnVtYmVyKHNldHRpbmdzLnJlc29sdXRpb24gPz8gY29uZmlnLnNjYW5SZXNvbHV0aW9uKTsKICAgIGlmICghTnVtYmVyLmlzRmluaXRlKHJlc29sdXRpb24pIHx8IHJlc29sdXRpb24gPCA3NSB8fCByZXNvbHV0aW9uID4gMTIwMCkgewogICAgICAgIHRocm93IG5ldyBFcnJvcigiRGllIFNjYW4tQXVmbMO2c3VuZyBtdXNzIHp3aXNjaGVuIDc1IHVuZCAxMjAwIGRwaSBsaWVnZW4iKTsKICAgIH0KICAgIGNvbnN0IG1vZGUgPSBzdHJpbmdTZXR0aW5nKHNldHRpbmdzLCAibW9kZSIpIHx8IGNvbmZpZy5zY2FuTW9kZTsKICAgIGNvbnN0IHNvdXJjZSA9IHN0cmluZ1NldHRpbmcoc2V0dGluZ3MsICJzb3VyY2UiKSB8fCBjb25maWcuc2NhblNvdXJjZTsKICAgIGNvbnN0IHNjYW5uZXJOYW1lID0gam9iLnNjYW5uZXJOYW1lIHx8IGNvbmZpZy5zY2FubmVyTmFtZTsKICAgIGNvbnN0IHBhcGVyU2l6ZSA9IHN0cmluZ1NldHRpbmcoc2V0dGluZ3MsICJwYXBlclNpemUiKSB8fCAiYXV0byI7CiAgICBpZiAoIVsiYXV0byIsICJBNCIsICJMZXR0ZXIiXS5pbmNsdWRlcyhwYXBlclNpemUpKQogICAgICAgIHRocm93IG5ldyBFcnJvcigiVW5iZWthbm50ZXMgUGFwaWVyZm9ybWF0Iik7CiAgICBjb25zdCBiYXRjaCA9IEJvb2xlYW4oc291cmNlICYmIC9hZGZ8ZG9jdW1lbnQgZmVlZGVyL2kudGVzdChzb3VyY2UpKTsKICAgIGlmIChiYXRjaCAmJiBmb3JtYXQgIT09ICJwZGYiKQogICAgICAgIHRocm93IG5ldyBFcnJvcigiRsO8ciBFaW56dWctIHVuZCBEdXBsZXgtU2NhbnMgYml0dGUgUERGIGFscyBGb3JtYXQgd8OkaGxlbiIpOwogICAgY29uc3QgcG9zdHByb2Nlc3MgPSBib29sZWFuU2V0dGluZyhzZXR0aW5ncywgInBvc3Rwcm9jZXNzIiwgY29uZmlnLnNjYW5Qb3N0cHJvY2Vzcyk7CiAgICBjb25zdCBwcm9maWxlID0gc3RyaW5nU2V0dGluZyhzZXR0aW5ncywgInBvc3Rwcm9jZXNzUHJvZmlsZSIpIHx8IGNvbmZpZy5wb3N0cHJvY2Vzc1Byb2ZpbGU7CiAgICBpZiAoIVsiZG9jdW1lbnQiLCAicmVjZWlwdCIsICJyYXciXS5pbmNsdWRlcyhwcm9maWxlKSkKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIlVuYmVrYW5udGVzIE5hY2hiZWFyYmVpdHVuZ3Nwcm9maWwiKTsKICAgIGF3YWl0IGNoZWNrU2NhblJ1bnRpbWUoY29uZmlnLCBwb3N0cHJvY2Vzcyk7CiAgICAvLyBFYWNoIHBoeXNpY2FsIHNjYW4gaGFzIGl0cyBvd24gZGlyZWN0b3J5LiBBIGZpbGVuYW1lIGNhbm5vdCBlc2NhcGUgaXQgb3Igb3ZlcndyaXRlIGFub3RoZXIgam9iLgogICAgYXdhaXQgbWtkaXIoY29uZmlnLndvcmtEaXIsIHsgcmVjdXJzaXZlOiB0cnVlLCBtb2RlOiAwbzcwMCB9KTsKICAgIGNvbnN0IGpvYkRpciA9IGF3YWl0IG1rZHRlbXAocGF0aC5qb2luKHBhdGgucmVzb2x2ZShjb25maWcud29ya0RpciksICJzY2FuLSIpKTsKICAgIGNvbnN0IHJlcXVlc3RlZCA9IHBhdGguYmFzZW5hbWUoKGpvYi5yZXF1ZXN0ZWRGaWxlbmFtZSB8fCAic2NhbiIpLnJlcGxhY2UoL1xcL2csICIvIikpOwogICAgY29uc3Qgc3RlbSA9IHJlcXVlc3RlZC5yZXBsYWNlKC9cLlteLl0qJC8sICIiKS5yZXBsYWNlKC9bXHgwMC1ceDFmXS9nLCAiIikudHJpbSgpIHx8ICJzY2FuIjsKICAgIGNvbnN0IG91dHB1dFBhdGggPSBwYXRoLmpvaW4oam9iRGlyLCBgJHtzdGVtfS4ke2Zvcm1hdH1gKTsKICAgIHRyeSB7CiAgICAgICAgLy8gRGV2aWNlL3NvdXJjZSBjb21lIGZpcnN0OiBjaGFuZ2luZyBzb3VyY2UgY2FuIGNoYW5nZSB0aGUgcmVtYWluaW5nIFNBTkUgb3B0aW9ucy4KICAgICAgICBjb25zdCBhcmdzID0gW107CiAgICAgICAgaWYgKHNjYW5uZXJOYW1lKQogICAgICAgICAgICBhcmdzLnB1c2goIi0tZGV2aWNlLW5hbWUiLCBzY2FubmVyTmFtZSk7CiAgICAgICAgaWYgKHNvdXJjZSkKICAgICAgICAgICAgYXJncy5wdXNoKCItLXNvdXJjZSIsIHNvdXJjZSk7CiAgICAgICAgYXJncy5wdXNoKCItLW1vZGUiLCBtb2RlLCAiLS1yZXNvbHV0aW9uIiwgU3RyaW5nKHJlc29sdXRpb24pLCAiLS1mb3JtYXQ9cG5nIik7CiAgICAgICAgaWYgKHBhcGVyU2l6ZSAhPT0gImF1dG8iKSB7CiAgICAgICAgICAgIGNvbnN0IFt3aWR0aCwgaGVpZ2h0XSA9IHBhcGVyU2l6ZSA9PT0gIkE0IiA/IFsyMTAsIDI5N10gOiBbMjE1LjksIDI3OS40XTsKICAgICAgICAgICAgaWYgKHNjYW5uZXJOYW1lPy5zdGFydHNXaXRoKCJmdWppdHN1OiIpKSB7CiAgICAgICAgICAgICAgICBhcmdzLnB1c2goIi0tcGFnZS13aWR0aCIsIFN0cmluZyh3aWR0aCksICItLXBhZ2UtaGVpZ2h0IiwgU3RyaW5nKGhlaWdodCkpOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGFyZ3MucHVzaCgiLWwiLCAiMCIsICItdCIsICIwIiwgIi14IiwgU3RyaW5nKHdpZHRoKSwgIi15IiwgU3RyaW5nKGhlaWdodCkpOwogICAgICAgIH0KICAgICAgICBpZiAoYmF0Y2gpIHsKICAgICAgICAgICAgLy8gUmVsYXRpdmUgcGF0dGVybiBhdm9pZHMgaW50ZXJwcmV0aW5nICclJyBpbiBhIHVzZXItY29uZmlndXJlZCB3b3JrIGRpcmVjdG9yeS4KICAgICAgICAgICAgYXJncy5wdXNoKCItLWJhdGNoPXBhZ2UtJTA2ZC5wbmciLCAiLS1iYXRjaC1zdGFydD0xIiwgIi0tYmF0Y2gtaW5jcmVtZW50PTEiKTsKICAgICAgICB9CiAgICAgICAgZWxzZSB7CiAgICAgICAgICAgIGFyZ3MucHVzaCgiLS1vdXRwdXQtZmlsZSIsICJwYWdlLTAwMDAwMS5wbmciKTsKICAgICAgICB9CiAgICAgICAgYXdhaXQgcmVwb3J0UHJvZ3Jlc3MoYmF0Y2ggPyAiRG9rdW1lbnRlIHdlcmRlbiBlaW5nZXpvZ2VuIHVuZCBnZXNjYW5udC4uLiIgOiAiU2VpdGUgd2lyZCBnZXNjYW5udC4uLiIpOwogICAgICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHJ1bkNvbW1hbmQoInNjYW5pbWFnZSIsIGFyZ3MsIHsKICAgICAgICAgICAgY3dkOiBqb2JEaXIsCiAgICAgICAgICAgIHRpbWVvdXRNczogY29uZmlnLnNjYW5UaW1lb3V0U2Vjb25kcyAqIDEwMDAsCiAgICAgICAgfSk7CiAgICAgICAgLy8gU0FORV9TVEFUVVNfTk9fRE9DUyAoNykgaXMgdGhlIGV4cGVjdGVkIGVuZCBvZiBhbiB1bmxpbWl0ZWQgQURGIGJhdGNoLgogICAgICAgIC8vIE5ldmVyIHR1cm4gcGFwZXIgamFtcywgSS9PIGVycm9ycyBvciBjYW5jZWxlZCBzY2FucyBpbnRvIGEgc3VjY2Vzc2Z1bCBwYXJ0aWFsIHVwbG9hZC4KICAgICAgICBpZiAocmVzdWx0LmNvZGUgIT09IDAgJiYgIShiYXRjaCAmJiByZXN1bHQuY29kZSA9PT0gNykpIHsKICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKHJlc3VsdC5zdGRlcnIudHJpbSgpIHx8IGBzY2FuaW1hZ2Ugd3VyZGUgbWl0IENvZGUgJHtyZXN1bHQuY29kZX0gYmVlbmRldGApOwogICAgICAgIH0KICAgICAgICBjb25zdCBuYW1lcyA9IChhd2FpdCByZWFkZGlyKGpvYkRpcikpCiAgICAgICAgICAgIC5maWx0ZXIoKG5hbWUpID0+IC9ecGFnZS1cZCtcLnBuZyQvLnRlc3QobmFtZSkpCiAgICAgICAgICAgIC5zb3J0KChhLCBiKSA9PiBOdW1iZXIoYS5tYXRjaCgvXGQrLylbMF0pIC0gTnVtYmVyKGIubWF0Y2goL1xkKy8pWzBdKSk7CiAgICAgICAgY29uc3QgcGFnZXMgPSBbXTsKICAgICAgICBmb3IgKGNvbnN0IFtpbmRleCwgbmFtZV0gb2YgbmFtZXMuZW50cmllcygpKSB7CiAgICAgICAgICAgIGNvbnN0IHBhZ2UgPSBwYXRoLmpvaW4oam9iRGlyLCBuYW1lKTsKICAgICAgICAgICAgLy8gU29tZSBzY2FuaW1hZ2UgdmVyc2lvbnMgbGVhdmUgYSB6ZXJvLWJ5dGUgZmlsZSBmb3IgdGhlIGZpbmFsIE5PX0RPQ1MgYXR0ZW1wdC4KICAgICAgICAgICAgaWYgKChhd2FpdCBzdGF0KHBhZ2UpKS5zaXplID09PSAwKSB7CiAgICAgICAgICAgICAgICBpZiAoYmF0Y2ggJiYgcmVzdWx0LmNvZGUgPT09IDcgJiYgaW5kZXggPT09IG5hbWVzLmxlbmd0aCAtIDEpCiAgICAgICAgICAgICAgICAgICAgY29udGludWU7CiAgICAgICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYFVudm9sbHN0w6RuZGlnZSBTY2FuLVNlaXRlOiAke25hbWV9YCk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgaWYgKE51bWJlcihuYW1lLm1hdGNoKC9cZCsvKVswXSkgIT09IGluZGV4ICsgMSkKICAgICAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcigiSW0gU2NhbiBmZWhsZW4gU2VpdGVuIik7CiAgICAgICAgICAgIHBhZ2VzLnB1c2gocGFnZSk7CiAgICAgICAgfQogICAgICAgIGlmICghcGFnZXMubGVuZ3RoKQogICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIktlaW5lIFNlaXRlbiBnZXNjYW5udC4gQml0dGUgUGFwaWVyIGluIGRlbiBFaW56dWcgbGVnZW4uIik7CiAgICAgICAgaWYgKHNvdXJjZT8udG9Mb3dlckNhc2UoKS5pbmNsdWRlcygiZHVwbGV4IikgJiYgcGFnZXMubGVuZ3RoICUgMiAhPT0gMCkgewogICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkRlciBEdXBsZXgtU2NhbiBpc3QgdW52b2xsc3TDpG5kaWc6IFp1IGVpbmVyIFZvcmRlcnNlaXRlIGZlaGx0IGRpZSBSw7xja3NlaXRlLiIpOwogICAgICAgIH0KICAgICAgICBhd2FpdCByZXBvcnRQcm9ncmVzcyhgJHtwYWdlcy5sZW5ndGh9IFNlaXRlbiBnZXNjYW5udC4gRGF0ZWkgd2lyZCBlcnN0ZWxsdC4uLmApOwogICAgICAgIHJldHVybiBhd2FpdCBjcmVhdGVTY2FuRG9jdW1lbnQoY29uZmlnLCBwYWdlcywgb3V0cHV0UGF0aCwgcmVzb2x1dGlvbiwgcG9zdHByb2Nlc3MsIHByb2ZpbGUpOwogICAgfQogICAgY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgY29uc3QgbWVzc2FnZSA9IGVycm9yIGluc3RhbmNlb2YgRXJyb3IgPyBlcnJvci5tZXNzYWdlIDogU3RyaW5nKGVycm9yKTsKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYCR7bWVzc2FnZX0gUm9oc2NhbnMgYmxlaWJlbiB6dXIgV2llZGVyaGVyc3RlbGx1bmcgaW4gJHtqb2JEaXJ9LmApOwogICAgfQp9Owo=", + "dist/scan/postprocess.js": "aW1wb3J0IHBhdGggZnJvbSAibm9kZTpwYXRoIjsKaW1wb3J0IHsgZmlsZVVSTFRvUGF0aCB9IGZyb20gIm5vZGU6dXJsIjsKaW1wb3J0IHsgcnVuQ29tbWFuZCB9IGZyb20gIi4uL2NvbW1hbmRzLmpzIjsKY29uc3QgYWdlbnRSb290ID0gcGF0aC5yZXNvbHZlKHBhdGguZGlybmFtZShmaWxlVVJMVG9QYXRoKGltcG9ydC5tZXRhLnVybCkpLCAiLi4vLi4iKTsKY29uc3QgZG9jdW1lbnRTY3JpcHQgPSBwYXRoLmpvaW4oYWdlbnRSb290LCAic2NyaXB0cy9zY2FuX2RvY3VtZW50LnB5Iik7CmV4cG9ydCBjb25zdCBjaGVja1NjYW5SdW50aW1lID0gYXN5bmMgKGNvbmZpZywgcG9zdHByb2Nlc3MsIG9wdGlvbnMpID0+IHsKICAgIGNvbnN0IGFyZ3MgPSBbZG9jdW1lbnRTY3JpcHQsICItLWNoZWNrLXJ1bnRpbWUiLCAiLS1vY3ItbGFuZ3VhZ2UiLCBvcHRpb25zLm9jckxhbmd1YWdlXTsKICAgIGlmIChwb3N0cHJvY2VzcykKICAgICAgICBhcmdzLnB1c2goIi0tcG9zdHByb2Nlc3MiKTsKICAgIGlmIChjb25maWcucG9zdHByb2Nlc3NTdHJpY3QpCiAgICAgICAgYXJncy5wdXNoKCItLXN0cmljdCIpOwogICAgaWYgKG9wdGlvbnMuYmFja2dyb3VuZENsZWFudXApCiAgICAgICAgYXJncy5wdXNoKCItLWJhY2tncm91bmQtY2xlYW51cCIpOwogICAgaWYgKG9wdGlvbnMub2NyKQogICAgICAgIGFyZ3MucHVzaCgiLS1vY3IiKTsKICAgIHRyeSB7CiAgICAgICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgcnVuQ29tbWFuZChjb25maWcucG9zdHByb2Nlc3NQeXRob24sIGFyZ3MsIHsgdGltZW91dE1zOiAxMF8wMDAgfSk7CiAgICAgICAgaWYgKHJlc3VsdC5jb2RlID09PSAwKQogICAgICAgICAgICByZXR1cm47CiAgICAgICAgdGhyb3cgbmV3IEVycm9yKHJlc3VsdC5zdGRlcnIudHJpbSgpKTsKICAgIH0KICAgIGNhdGNoIChlcnJvcikgewogICAgICAgIC8vIFJlcG9ydCBtaXNzaW5nIHJ1bnRpbWVzIGJlZm9yZSB0aGUgc2Nhbm5lciBjb25zdW1lcyBhbnkgcGFwZXIuCiAgICAgICAgY29uc3QgZGV0YWlsID0gZXJyb3IgaW5zdGFuY2VvZiBFcnJvciA/IGVycm9yLm1lc3NhZ2UgOiBTdHJpbmcoZXJyb3IpOwogICAgICAgIHRocm93IG5ldyBFcnJvcigiU2Nhbi1WZXJhcmJlaXR1bmcgbmljaHQgdmVyZsO8Z2Jhci4gQml0dGUgZGVuIEFnZW50ZW4gbWl0IFB5dGhvbi9QaWxsb3csIE9wZW5DViB1bmQgVGVzc2VyYWN0IHNhbXQgT0NSLVNwcmFjaHBha2V0ZW4gaW5zdGFsbGllcmVuLiAiICsgZGV0YWlsKTsKICAgIH0KfTsKZXhwb3J0IGNvbnN0IHByb2Nlc3NpbmdDYXBhYmlsaXRpZXMgPSBhc3luYyAoY29uZmlnKSA9PiB7CiAgICBjb25zdCBhdmFpbGFibGUgPSBhc3luYyAob3B0aW9ucykgPT4gewogICAgICAgIHRyeSB7CiAgICAgICAgICAgIGF3YWl0IGNoZWNrU2NhblJ1bnRpbWUoY29uZmlnLCBmYWxzZSwgb3B0aW9ucyk7CiAgICAgICAgICAgIHJldHVybiB0cnVlOwogICAgICAgIH0KICAgICAgICBjYXRjaCB7CiAgICAgICAgICAgIHJldHVybiBmYWxzZTsKICAgICAgICB9CiAgICB9OwogICAgY29uc3QgW29jciwgYmFja2dyb3VuZENsZWFudXBdID0gYXdhaXQgUHJvbWlzZS5hbGwoWwogICAgICAgIGF2YWlsYWJsZSh7IG9jcjogdHJ1ZSwgb2NyTGFuZ3VhZ2U6IGNvbmZpZy5zY2FuT2NyTGFuZ3VhZ2UsIGJhY2tncm91bmRDbGVhbnVwOiBmYWxzZSB9KSwKICAgICAgICBhdmFpbGFibGUoeyBvY3I6IGZhbHNlLCBvY3JMYW5ndWFnZTogY29uZmlnLnNjYW5PY3JMYW5ndWFnZSwgYmFja2dyb3VuZENsZWFudXA6IHRydWUgfSksCiAgICBdKTsKICAgIHJldHVybiB7IG9jciwgYmFja2dyb3VuZENsZWFudXAgfTsKfTsKZXhwb3J0IGNvbnN0IGNyZWF0ZVNjYW5Eb2N1bWVudCA9IGFzeW5jIChjb25maWcsIHBhZ2VzLCBvdXRwdXRQYXRoLCByZXNvbHV0aW9uLCBwb3N0cHJvY2VzcywgcHJvZmlsZSwgb3B0aW9ucykgPT4gewogICAgY29uc3QgYXJncyA9IFtkb2N1bWVudFNjcmlwdCwgIi0tb3V0cHV0Iiwgb3V0cHV0UGF0aCwgIi0tcmVzb2x1dGlvbiIsIFN0cmluZyhyZXNvbHV0aW9uKSwgIi0tcHJvZmlsZSIsIHByb2ZpbGVdOwogICAgaWYgKHBvc3Rwcm9jZXNzKQogICAgICAgIGFyZ3MucHVzaCgiLS1wb3N0cHJvY2VzcyIpOwogICAgaWYgKGNvbmZpZy5wb3N0cHJvY2Vzc1N0cmljdCkKICAgICAgICBhcmdzLnB1c2goIi0tc3RyaWN0Iik7CiAgICBpZiAob3B0aW9ucy5iYWNrZ3JvdW5kQ2xlYW51cCkKICAgICAgICBhcmdzLnB1c2goIi0tYmFja2dyb3VuZC1jbGVhbnVwIik7CiAgICBpZiAob3B0aW9ucy5vY3IpCiAgICAgICAgYXJncy5wdXNoKCItLW9jciIsICItLW9jci1sYW5ndWFnZSIsIG9wdGlvbnMub2NyTGFuZ3VhZ2UpOwogICAgYXJncy5wdXNoKCItLWlucHV0cyIsIC4uLnBhZ2VzKTsKICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHJ1bkNvbW1hbmQoY29uZmlnLnBvc3Rwcm9jZXNzUHl0aG9uLCBhcmdzLCB7CiAgICAgICAgdGltZW91dE1zOiBjb25maWcuc2NhblRpbWVvdXRTZWNvbmRzICogMTAwMCwKICAgIH0pOwogICAgaWYgKHJlc3VsdC5jb2RlICE9PSAwKQogICAgICAgIHRocm93IG5ldyBFcnJvcihyZXN1bHQuc3RkZXJyIHx8ICJTY2FuLURhdGVpIGtvbm50ZSBuaWNodCBlcnN0ZWxsdCB3ZXJkZW4iKTsKICAgIGNvbnN0IGRldGFpbHMgPSBKU09OLnBhcnNlKHJlc3VsdC5zdGRvdXQpOwogICAgcmV0dXJuIHsKICAgICAgICBwYXRoOiBvdXRwdXRQYXRoLAogICAgICAgIGZpbGVuYW1lOiBwYXRoLmJhc2VuYW1lKG91dHB1dFBhdGgpLAogICAgICAgIG1pbWVUeXBlOiBvdXRwdXRQYXRoLmVuZHNXaXRoKCIucGRmIikgPyAiYXBwbGljYXRpb24vcGRmIiA6IG91dHB1dFBhdGguZW5kc1dpdGgoIi5wbmciKSA/ICJpbWFnZS9wbmciIDogImltYWdlL3RpZmYiLAogICAgICAgIC4uLmRldGFpbHMsCiAgICB9Owp9Owo=", + "dist/scan/sane.js": "aW1wb3J0IHsgbWtkaXIsIG1rZHRlbXAsIHJlYWRkaXIsIHN0YXQgfSBmcm9tICJub2RlOmZzL3Byb21pc2VzIjsKaW1wb3J0IHBhdGggZnJvbSAibm9kZTpwYXRoIjsKaW1wb3J0IHsgY29tbWFuZEV4aXN0cywgcnVuQ29tbWFuZCB9IGZyb20gIi4uL2NvbW1hbmRzLmpzIjsKaW1wb3J0IHsgY2hlY2tTY2FuUnVudGltZSwgY3JlYXRlU2NhbkRvY3VtZW50IH0gZnJvbSAiLi9wb3N0cHJvY2Vzcy5qcyI7CmNvbnN0IHN0cmluZ1NldHRpbmcgPSAoc2V0dGluZ3MsIGtleSkgPT4gewogICAgY29uc3QgdmFsdWUgPSBzZXR0aW5nc1trZXldOwogICAgcmV0dXJuIHR5cGVvZiB2YWx1ZSA9PT0gInN0cmluZyIgJiYgdmFsdWUudHJpbSgpID8gdmFsdWUudHJpbSgpIDogdW5kZWZpbmVkOwp9Owpjb25zdCBib29sZWFuU2V0dGluZyA9IChzZXR0aW5ncywga2V5LCBmYWxsYmFjaykgPT4gewogICAgY29uc3QgdmFsdWUgPSBzZXR0aW5nc1trZXldOwogICAgaWYgKHR5cGVvZiB2YWx1ZSA9PT0gImJvb2xlYW4iKQogICAgICAgIHJldHVybiB2YWx1ZTsKICAgIGlmICh0eXBlb2YgdmFsdWUgPT09ICJzdHJpbmciKQogICAgICAgIHJldHVybiBbIjEiLCAidHJ1ZSIsICJ5ZXMiLCAiamEiLCAib24iXS5pbmNsdWRlcyh2YWx1ZS50cmltKCkudG9Mb3dlckNhc2UoKSk7CiAgICByZXR1cm4gZmFsbGJhY2s7Cn07CmV4cG9ydCBjb25zdCBoYXNTYW5lID0gKCkgPT4gY29tbWFuZEV4aXN0cygic2NhbmltYWdlIik7CmV4cG9ydCBjb25zdCBsaXN0U2Nhbm5lcnMgPSBhc3luYyAoKSA9PiB7CiAgICBpZiAoIWF3YWl0IGhhc1NhbmUoKSkKICAgICAgICByZXR1cm4gW107CiAgICAvLyBTQU5FIGFsc28gcHJvYmVzIG5ldHdvcmsgYmFja2VuZHM7IHRoZXNlIGNhbiB0YWtlIG1vcmUgdGhhbiB0ZW4gc2Vjb25kcwogICAgLy8gZXZlbiB3aGVuIHRoZSBVU0Igc2Nhbm5lciBpcyBhbHJlYWR5IGF2YWlsYWJsZS4KICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHJ1bkNvbW1hbmQoInNjYW5pbWFnZSIsIFsiLUwiXSwgeyB0aW1lb3V0TXM6IDMwXzAwMCB9KTsKICAgIGlmIChyZXN1bHQuY29kZSAhPT0gMCkKICAgICAgICByZXR1cm4gW107CiAgICByZXR1cm4gcmVzdWx0LnN0ZG91dC5zcGxpdCgvXHI/XG4vKQogICAgICAgIC5tYXAoKGxpbmUpID0+IGxpbmUudHJpbSgpLm1hdGNoKC9eZGV2aWNlIGAoW14nXSspJy8pPy5bMV0pCiAgICAgICAgLmZpbHRlcigoZGV2aWNlKSA9PiBCb29sZWFuKGRldmljZSkpOwp9OwpleHBvcnQgY29uc3QgcnVuU2NhbiA9IGFzeW5jIChjb25maWcsIGpvYiwgcmVwb3J0UHJvZ3Jlc3MgPSBhc3luYyAoKSA9PiB7IH0pID0+IHsKICAgIGlmICghYXdhaXQgaGFzU2FuZSgpKQogICAgICAgIHRocm93IG5ldyBFcnJvcigic2NhbmltYWdlIGlzdCBuaWNodCBpbnN0YWxsaWVydCBvZGVyIG5pY2h0IGltIFBBVEgiKTsKICAgIGNvbnN0IHNldHRpbmdzID0gam9iLnNldHRpbmdzIHx8IHt9OwogICAgY29uc3QgZm9ybWF0ID0gc3RyaW5nU2V0dGluZyhzZXR0aW5ncywgImZvcm1hdCIpIHx8IGNvbmZpZy5zY2FuRm9ybWF0OwogICAgaWYgKCFbInBkZiIsICJwbmciLCAidGlmZiJdLmluY2x1ZGVzKGZvcm1hdCkpCiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCJTY2FuLUZvcm1hdCBtdXNzIFBERiwgUE5HIG9kZXIgVElGRiBzZWluIik7CiAgICBjb25zdCByZXNvbHV0aW9uID0gTnVtYmVyKHNldHRpbmdzLnJlc29sdXRpb24gPz8gY29uZmlnLnNjYW5SZXNvbHV0aW9uKTsKICAgIGlmICghTnVtYmVyLmlzRmluaXRlKHJlc29sdXRpb24pIHx8IHJlc29sdXRpb24gPCA3NSB8fCByZXNvbHV0aW9uID4gMTIwMCkgewogICAgICAgIHRocm93IG5ldyBFcnJvcigiRGllIFNjYW4tQXVmbMO2c3VuZyBtdXNzIHp3aXNjaGVuIDc1IHVuZCAxMjAwIGRwaSBsaWVnZW4iKTsKICAgIH0KICAgIGNvbnN0IG1vZGUgPSBzdHJpbmdTZXR0aW5nKHNldHRpbmdzLCAibW9kZSIpIHx8IGNvbmZpZy5zY2FuTW9kZTsKICAgIGNvbnN0IHNvdXJjZSA9IHN0cmluZ1NldHRpbmcoc2V0dGluZ3MsICJzb3VyY2UiKSB8fCBjb25maWcuc2NhblNvdXJjZTsKICAgIGNvbnN0IHNjYW5uZXJOYW1lID0gam9iLnNjYW5uZXJOYW1lIHx8IGNvbmZpZy5zY2FubmVyTmFtZTsKICAgIGNvbnN0IHBhcGVyU2l6ZSA9IHN0cmluZ1NldHRpbmcoc2V0dGluZ3MsICJwYXBlclNpemUiKSB8fCAiYXV0byI7CiAgICBpZiAoIVsiYXV0byIsICJBNCIsICJMZXR0ZXIiXS5pbmNsdWRlcyhwYXBlclNpemUpKQogICAgICAgIHRocm93IG5ldyBFcnJvcigiVW5iZWthbm50ZXMgUGFwaWVyZm9ybWF0Iik7CiAgICBjb25zdCBiYXRjaCA9IEJvb2xlYW4oc291cmNlICYmIC9hZGZ8ZG9jdW1lbnQgZmVlZGVyL2kudGVzdChzb3VyY2UpKTsKICAgIGlmIChiYXRjaCAmJiBmb3JtYXQgIT09ICJwZGYiKQogICAgICAgIHRocm93IG5ldyBFcnJvcigiRsO8ciBFaW56dWctIHVuZCBEdXBsZXgtU2NhbnMgYml0dGUgUERGIGFscyBGb3JtYXQgd8OkaGxlbiIpOwogICAgY29uc3QgcG9zdHByb2Nlc3MgPSBib29sZWFuU2V0dGluZyhzZXR0aW5ncywgInBvc3Rwcm9jZXNzIiwgY29uZmlnLnNjYW5Qb3N0cHJvY2Vzcyk7CiAgICBjb25zdCBwcm9maWxlID0gc3RyaW5nU2V0dGluZyhzZXR0aW5ncywgInBvc3Rwcm9jZXNzUHJvZmlsZSIpIHx8IGNvbmZpZy5wb3N0cHJvY2Vzc1Byb2ZpbGU7CiAgICBpZiAoIVsiZG9jdW1lbnQiLCAicmVjZWlwdCIsICJyYXciXS5pbmNsdWRlcyhwcm9maWxlKSkKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIlVuYmVrYW5udGVzIE5hY2hiZWFyYmVpdHVuZ3Nwcm9maWwiKTsKICAgIGNvbnN0IHByb2Nlc3NpbmcgPSB7CiAgICAgICAgYmFja2dyb3VuZENsZWFudXA6IGJvb2xlYW5TZXR0aW5nKHNldHRpbmdzLCAiYmFja2dyb3VuZENsZWFudXAiLCBjb25maWcuc2NhbkJhY2tncm91bmRDbGVhbnVwKSwKICAgICAgICBvY3I6IGZvcm1hdCA9PT0gInBkZiIgJiYgYm9vbGVhblNldHRpbmcoc2V0dGluZ3MsICJvY3IiLCBjb25maWcuc2Nhbk9jciksCiAgICAgICAgb2NyTGFuZ3VhZ2U6IHN0cmluZ1NldHRpbmcoc2V0dGluZ3MsICJvY3JMYW5ndWFnZSIpIHx8IGNvbmZpZy5zY2FuT2NyTGFuZ3VhZ2UsCiAgICB9OwogICAgYXdhaXQgY2hlY2tTY2FuUnVudGltZShjb25maWcsIHBvc3Rwcm9jZXNzLCBwcm9jZXNzaW5nKTsKICAgIC8vIEVhY2ggcGh5c2ljYWwgc2NhbiBoYXMgaXRzIG93biBkaXJlY3RvcnkuIEEgZmlsZW5hbWUgY2Fubm90IGVzY2FwZSBpdCBvciBvdmVyd3JpdGUgYW5vdGhlciBqb2IuCiAgICBhd2FpdCBta2Rpcihjb25maWcud29ya0RpciwgeyByZWN1cnNpdmU6IHRydWUsIG1vZGU6IDBvNzAwIH0pOwogICAgY29uc3Qgam9iRGlyID0gYXdhaXQgbWtkdGVtcChwYXRoLmpvaW4ocGF0aC5yZXNvbHZlKGNvbmZpZy53b3JrRGlyKSwgInNjYW4tIikpOwogICAgY29uc3QgcmVxdWVzdGVkID0gcGF0aC5iYXNlbmFtZSgoam9iLnJlcXVlc3RlZEZpbGVuYW1lIHx8ICJzY2FuIikucmVwbGFjZSgvXFwvZywgIi8iKSk7CiAgICBjb25zdCBzdGVtID0gcmVxdWVzdGVkLnJlcGxhY2UoL1wuW14uXSokLywgIiIpLnJlcGxhY2UoL1tceDAwLVx4MWZdL2csICIiKS50cmltKCkgfHwgInNjYW4iOwogICAgY29uc3Qgb3V0cHV0UGF0aCA9IHBhdGguam9pbihqb2JEaXIsIGAke3N0ZW19LiR7Zm9ybWF0fWApOwogICAgdHJ5IHsKICAgICAgICAvLyBEZXZpY2Uvc291cmNlIGNvbWUgZmlyc3Q6IGNoYW5naW5nIHNvdXJjZSBjYW4gY2hhbmdlIHRoZSByZW1haW5pbmcgU0FORSBvcHRpb25zLgogICAgICAgIGNvbnN0IGFyZ3MgPSBbXTsKICAgICAgICBpZiAoc2Nhbm5lck5hbWUpCiAgICAgICAgICAgIGFyZ3MucHVzaCgiLS1kZXZpY2UtbmFtZSIsIHNjYW5uZXJOYW1lKTsKICAgICAgICBpZiAoc291cmNlKQogICAgICAgICAgICBhcmdzLnB1c2goIi0tc291cmNlIiwgc291cmNlKTsKICAgICAgICBhcmdzLnB1c2goIi0tbW9kZSIsIG1vZGUsICItLXJlc29sdXRpb24iLCBTdHJpbmcocmVzb2x1dGlvbiksICItLWZvcm1hdD1wbmciKTsKICAgICAgICBpZiAocGFwZXJTaXplICE9PSAiYXV0byIpIHsKICAgICAgICAgICAgY29uc3QgW3dpZHRoLCBoZWlnaHRdID0gcGFwZXJTaXplID09PSAiQTQiID8gWzIxMCwgMjk3XSA6IFsyMTUuOSwgMjc5LjRdOwogICAgICAgICAgICBpZiAoc2Nhbm5lck5hbWU/LnN0YXJ0c1dpdGgoImZ1aml0c3U6IikpIHsKICAgICAgICAgICAgICAgIGFyZ3MucHVzaCgiLS1wYWdlLXdpZHRoIiwgU3RyaW5nKHdpZHRoKSwgIi0tcGFnZS1oZWlnaHQiLCBTdHJpbmcoaGVpZ2h0KSk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgYXJncy5wdXNoKCItbCIsICIwIiwgIi10IiwgIjAiLCAiLXgiLCBTdHJpbmcod2lkdGgpLCAiLXkiLCBTdHJpbmcoaGVpZ2h0KSk7CiAgICAgICAgfQogICAgICAgIGlmIChiYXRjaCkgewogICAgICAgICAgICAvLyBSZWxhdGl2ZSBwYXR0ZXJuIGF2b2lkcyBpbnRlcnByZXRpbmcgJyUnIGluIGEgdXNlci1jb25maWd1cmVkIHdvcmsgZGlyZWN0b3J5LgogICAgICAgICAgICBhcmdzLnB1c2goIi0tYmF0Y2g9cGFnZS0lMDZkLnBuZyIsICItLWJhdGNoLXN0YXJ0PTEiLCAiLS1iYXRjaC1pbmNyZW1lbnQ9MSIpOwogICAgICAgIH0KICAgICAgICBlbHNlIHsKICAgICAgICAgICAgYXJncy5wdXNoKCItLW91dHB1dC1maWxlIiwgInBhZ2UtMDAwMDAxLnBuZyIpOwogICAgICAgIH0KICAgICAgICBhd2FpdCByZXBvcnRQcm9ncmVzcyhiYXRjaCA/ICJEb2t1bWVudGUgd2VyZGVuIGVpbmdlem9nZW4gdW5kIGdlc2Nhbm50Li4uIiA6ICJTZWl0ZSB3aXJkIGdlc2Nhbm50Li4uIik7CiAgICAgICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgcnVuQ29tbWFuZCgic2NhbmltYWdlIiwgYXJncywgewogICAgICAgICAgICBjd2Q6IGpvYkRpciwKICAgICAgICAgICAgdGltZW91dE1zOiBjb25maWcuc2NhblRpbWVvdXRTZWNvbmRzICogMTAwMCwKICAgICAgICB9KTsKICAgICAgICAvLyBTQU5FX1NUQVRVU19OT19ET0NTICg3KSBpcyB0aGUgZXhwZWN0ZWQgZW5kIG9mIGFuIHVubGltaXRlZCBBREYgYmF0Y2guCiAgICAgICAgLy8gTmV2ZXIgdHVybiBwYXBlciBqYW1zLCBJL08gZXJyb3JzIG9yIGNhbmNlbGVkIHNjYW5zIGludG8gYSBzdWNjZXNzZnVsIHBhcnRpYWwgdXBsb2FkLgogICAgICAgIGlmIChyZXN1bHQuY29kZSAhPT0gMCAmJiAhKGJhdGNoICYmIHJlc3VsdC5jb2RlID09PSA3KSkgewogICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IocmVzdWx0LnN0ZGVyci50cmltKCkgfHwgYHNjYW5pbWFnZSB3dXJkZSBtaXQgQ29kZSAke3Jlc3VsdC5jb2RlfSBiZWVuZGV0YCk7CiAgICAgICAgfQogICAgICAgIGNvbnN0IG5hbWVzID0gKGF3YWl0IHJlYWRkaXIoam9iRGlyKSkKICAgICAgICAgICAgLmZpbHRlcigobmFtZSkgPT4gL15wYWdlLVxkK1wucG5nJC8udGVzdChuYW1lKSkKICAgICAgICAgICAgLnNvcnQoKGEsIGIpID0+IE51bWJlcihhLm1hdGNoKC9cZCsvKVswXSkgLSBOdW1iZXIoYi5tYXRjaCgvXGQrLylbMF0pKTsKICAgICAgICBjb25zdCBwYWdlcyA9IFtdOwogICAgICAgIGZvciAoY29uc3QgW2luZGV4LCBuYW1lXSBvZiBuYW1lcy5lbnRyaWVzKCkpIHsKICAgICAgICAgICAgY29uc3QgcGFnZSA9IHBhdGguam9pbihqb2JEaXIsIG5hbWUpOwogICAgICAgICAgICAvLyBTb21lIHNjYW5pbWFnZSB2ZXJzaW9ucyBsZWF2ZSBhIHplcm8tYnl0ZSBmaWxlIGZvciB0aGUgZmluYWwgTk9fRE9DUyBhdHRlbXB0LgogICAgICAgICAgICBpZiAoKGF3YWl0IHN0YXQocGFnZSkpLnNpemUgPT09IDApIHsKICAgICAgICAgICAgICAgIGlmIChiYXRjaCAmJiByZXN1bHQuY29kZSA9PT0gNyAmJiBpbmRleCA9PT0gbmFtZXMubGVuZ3RoIC0gMSkKICAgICAgICAgICAgICAgICAgICBjb250aW51ZTsKICAgICAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcihgVW52b2xsc3TDpG5kaWdlIFNjYW4tU2VpdGU6ICR7bmFtZX1gKTsKICAgICAgICAgICAgfQogICAgICAgICAgICBpZiAoTnVtYmVyKG5hbWUubWF0Y2goL1xkKy8pWzBdKSAhPT0gaW5kZXggKyAxKQogICAgICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKCJJbSBTY2FuIGZlaGxlbiBTZWl0ZW4iKTsKICAgICAgICAgICAgcGFnZXMucHVzaChwYWdlKTsKICAgICAgICB9CiAgICAgICAgaWYgKCFwYWdlcy5sZW5ndGgpCiAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcigiS2VpbmUgU2VpdGVuIGdlc2Nhbm50LiBCaXR0ZSBQYXBpZXIgaW4gZGVuIEVpbnp1ZyBsZWdlbi4iKTsKICAgICAgICBpZiAoc291cmNlPy50b0xvd2VyQ2FzZSgpLmluY2x1ZGVzKCJkdXBsZXgiKSAmJiBwYWdlcy5sZW5ndGggJSAyICE9PSAwKSB7CiAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcigiRGVyIER1cGxleC1TY2FuIGlzdCB1bnZvbGxzdMOkbmRpZzogWnUgZWluZXIgVm9yZGVyc2VpdGUgZmVobHQgZGllIFLDvGNrc2VpdGUuIik7CiAgICAgICAgfQogICAgICAgIGF3YWl0IHJlcG9ydFByb2dyZXNzKGAke3BhZ2VzLmxlbmd0aH0gU2VpdGVuIGdlc2Nhbm50LiAke3Byb2Nlc3Npbmcub2NyID8gIkR1cmNoc3VjaGJhcmVzIFBERiBtaXQgT0NSIHdpcmQgZXJzdGVsbHQuLi4iIDogIkRhdGVpIHdpcmQgZXJzdGVsbHQuLi4ifWApOwogICAgICAgIHJldHVybiBhd2FpdCBjcmVhdGVTY2FuRG9jdW1lbnQoY29uZmlnLCBwYWdlcywgb3V0cHV0UGF0aCwgcmVzb2x1dGlvbiwgcG9zdHByb2Nlc3MsIHByb2ZpbGUsIHByb2Nlc3NpbmcpOwogICAgfQogICAgY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgY29uc3QgbWVzc2FnZSA9IGVycm9yIGluc3RhbmNlb2YgRXJyb3IgPyBlcnJvci5tZXNzYWdlIDogU3RyaW5nKGVycm9yKTsKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYCR7bWVzc2FnZX0gUm9oc2NhbnMgYmxlaWJlbiB6dXIgV2llZGVyaGVyc3RlbGx1bmcgaW4gJHtqb2JEaXJ9LmApOwogICAgfQp9Owo=", "dist/types.js": "ZXhwb3J0IHt9Owo=", - "scripts/scan_document.py": "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoiIiJBc3NlbWJsZSBvcmRlcmVkIHNjYW5uZXIgcGFnZXM7IE9wZW5DViBpcyBvcHRpb25hbCwgUERGIGNyZWF0aW9uIGlzIG5vdC4iIiIKaW1wb3J0IGFyZ3BhcnNlCmltcG9ydCBqc29uCmZyb20gcGF0aGxpYiBpbXBvcnQgUGF0aAoKZnJvbSBQSUwgaW1wb3J0IEltYWdlCgoKZGVmIGNyZWF0ZV9kb2N1bWVudChpbnB1dHMsIG91dHB1dCwgcmVzb2x1dGlvbiwgdXNlX3Bvc3Rwcm9jZXNzPUZhbHNlLCBwcm9maWxlPSJkb2N1bWVudCIsIHN0cmljdD1GYWxzZSk6CiAgICBpZiBub3QgaW5wdXRzOgogICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoIktlaW5lIFNlaXRlbiB2b3JoYW5kZW4iKQogICAgb3V0cHV0ID0gUGF0aChvdXRwdXQpCiAgICBmaWxlX2Zvcm1hdCA9IHsiLnBkZiI6ICJQREYiLCAiLnBuZyI6ICJQTkciLCAiLnRpZmYiOiAiVElGRiJ9LmdldChvdXRwdXQuc3VmZml4Lmxvd2VyKCkpCiAgICBpZiBub3QgZmlsZV9mb3JtYXQ6CiAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiVW5iZWthbm50ZXMgQXVzZ2FiZWZvcm1hdCIpCiAgICBpZiBmaWxlX2Zvcm1hdCAhPSAiUERGIiBhbmQgbGVuKGlucHV0cykgIT0gMToKICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJNZWhyc2VpdGlnZSBTY2FucyBiZW7DtnRpZ2VuIFBERiBhbHMgQXVzZ2FiZWZvcm1hdCIpCgogICAgd2FybmluZ3MgPSBbXQogICAgcHJvY2Vzc29yID0gTm9uZQogICAgaWYgdXNlX3Bvc3Rwcm9jZXNzOgogICAgICAgIHRyeToKICAgICAgICAgICAgZnJvbSBvcGVuY3ZfcG9zdHByb2Nlc3MgaW1wb3J0IHBvc3Rwcm9jZXNzCiAgICAgICAgICAgIHByb2Nlc3NvciA9IHBvc3Rwcm9jZXNzCiAgICAgICAgZXhjZXB0IEltcG9ydEVycm9yOgogICAgICAgICAgICBpZiBzdHJpY3Q6CiAgICAgICAgICAgICAgICByYWlzZQogICAgICAgICAgICB3YXJuaW5ncy5hcHBlbmQoIk9wZW5DViBmZWhsdDsgYWxsZSBTZWl0ZW4gd3VyZGVuIG9obmUgQmlsZGtvcnJla3R1ciDDvGJlcm5vbW1lbi4iKQoKICAgIHBhcnRpYWwgPSBvdXRwdXQud2l0aF9uYW1lKG91dHB1dC5uYW1lICsgIi5wYXJ0aWFsIikKICAgIHRyeToKICAgICAgICBmb3IgaW5kZXgsIGlucHV0X3BhdGggaW4gZW51bWVyYXRlKGlucHV0cyk6CiAgICAgICAgICAgIGlucHV0X3BhdGggPSBQYXRoKGlucHV0X3BhdGgpCiAgICAgICAgICAgIGltYWdlX3BhdGggPSBpbnB1dF9wYXRoCiAgICAgICAgICAgIGNvcnJlY3RlZCA9IGlucHV0X3BhdGgud2l0aF9uYW1lKGlucHV0X3BhdGguc3RlbSArICIuY29ycmVjdGVkLnBuZyIpCiAgICAgICAgICAgIGlmIHByb2Nlc3NvcjoKICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICBwcm9jZXNzb3IoaW5wdXRfcGF0aCwgY29ycmVjdGVkLCBwcm9maWxlKQogICAgICAgICAgICAgICAgICAgIGltYWdlX3BhdGggPSBjb3JyZWN0ZWQKICAgICAgICAgICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgICAgICAgICAgaWYgc3RyaWN0OgogICAgICAgICAgICAgICAgICAgICAgICByYWlzZQogICAgICAgICAgICAgICAgICAgIHdhcm5pbmdzLmFwcGVuZChmIlNlaXRlIHtpbmRleCArIDF9OiBCaWxka29ycmVrdHVyIGZlaGxnZXNjaGxhZ2VuOyBSb2hzY2FuIMO8YmVybm9tbWVuLiIpCgogICAgICAgICAgICAjIE9ubHkgb25lIGRlY29kZWQgcGFnZSBpcyBrZXB0IGluIG1lbW9yeSwgZXZlbiBmb3IgbGFyZ2UgZHVwbGV4IHN0YWNrcy4KICAgICAgICAgICAgd2l0aCBJbWFnZS5vcGVuKGltYWdlX3BhdGgpIGFzIGltYWdlOgogICAgICAgICAgICAgICAgd2l0aCBpbWFnZS5jb252ZXJ0KCJSR0IiKSBhcyByZ2I6CiAgICAgICAgICAgICAgICAgICAgaWYgZmlsZV9mb3JtYXQgPT0gIlBERiI6CiAgICAgICAgICAgICAgICAgICAgICAgIHJnYi5zYXZlKHBhcnRpYWwsICJQREYiLCByZXNvbHV0aW9uPXJlc29sdXRpb24sIHF1YWxpdHk9OTAsIGFwcGVuZD1pbmRleCA+IDApCiAgICAgICAgICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgICAgICAgICAgcmdiLnNhdmUocGFydGlhbCwgZmlsZV9mb3JtYXQsIGRwaT0ocmVzb2x1dGlvbiwgcmVzb2x1dGlvbikpCiAgICAgICAgICAgIGNvcnJlY3RlZC51bmxpbmsobWlzc2luZ19vaz1UcnVlKQogICAgICAgIHBhcnRpYWwucmVwbGFjZShvdXRwdXQpCiAgICBmaW5hbGx5OgogICAgICAgIHBhcnRpYWwudW5saW5rKG1pc3Npbmdfb2s9VHJ1ZSkKCiAgICByZXR1cm4geyJwYWdlQ291bnQiOiBsZW4oaW5wdXRzKSwgIndhcm5pbmdzIjogd2FybmluZ3N9CgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPSJTY2FuLVNlaXRlbiB6dSBlaW5lciBEYXRlaSB6dXNhbW1lbmbDvGhyZW4iKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1pbnB1dHMiLCBuYXJncz0iKyIsIHJlcXVpcmVkPVRydWUpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLW91dHB1dCIsIHJlcXVpcmVkPVRydWUpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLXJlc29sdXRpb24iLCB0eXBlPWZsb2F0LCByZXF1aXJlZD1UcnVlKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1wcm9maWxlIiwgY2hvaWNlcz1bImRvY3VtZW50IiwgInJlY2VpcHQiLCAicmF3Il0sIGRlZmF1bHQ9ImRvY3VtZW50IikKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcG9zdHByb2Nlc3MiLCBhY3Rpb249InN0b3JlX3RydWUiKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1zdHJpY3QiLCBhY3Rpb249InN0b3JlX3RydWUiKQogICAgYXJncyA9IHBhcnNlci5wYXJzZV9hcmdzKCkKICAgIHByaW50KGpzb24uZHVtcHMoY3JlYXRlX2RvY3VtZW50KGFyZ3MuaW5wdXRzLCBhcmdzLm91dHB1dCwgYXJncy5yZXNvbHV0aW9uLCBhcmdzLnBvc3Rwcm9jZXNzLCBhcmdzLnByb2ZpbGUsIGFyZ3Muc3RyaWN0KSkpCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIG1haW4oKQo=", + "scripts/scan_document.py": "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoiIiJQcmVwYXJlIG9yZGVyZWQgc2Nhbm5lciBwYWdlcyBhbmQgcHVibGlzaCBhbiBvcHRpb25hbGx5IHNlYXJjaGFibGUgUERGIGF0b21pY2FsbHkuIiIiCmltcG9ydCBhcmdwYXJzZQppbXBvcnQganNvbgppbXBvcnQgbWF0aApmcm9tIHBhdGhsaWIgaW1wb3J0IFBhdGgKaW1wb3J0IHJlCmltcG9ydCBzaWduYWwKaW1wb3J0IHN1YnByb2Nlc3MKaW1wb3J0IHRlbXBmaWxlCgpmcm9tIFBJTCBpbXBvcnQgSW1hZ2UsIFBkZlBhcnNlcgoKCmRlZiBydW5fb2NyX2NvbW1hbmQoYXJndW1lbnRzKToKICAgIHByb2Nlc3MgPSBzdWJwcm9jZXNzLlBvcGVuKGFyZ3VtZW50cywgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuUElQRSwgdGV4dD1UcnVlKQogICAgdHJ5OgogICAgICAgIHN0ZG91dCwgc3RkZXJyID0gcHJvY2Vzcy5jb21tdW5pY2F0ZSgpCiAgICBleGNlcHQgQmFzZUV4Y2VwdGlvbjoKICAgICAgICBwcm9jZXNzLmtpbGwoKQogICAgICAgIHByb2Nlc3Mud2FpdCgpCiAgICAgICAgcmFpc2UKICAgIGlmIHByb2Nlc3MucmV0dXJuY29kZSAhPSAwOgogICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigiT0NSIGZlaGxnZXNjaGxhZ2VuOiAiICsgc3RkZXJyLnN0cmlwKCkpCiAgICByZXR1cm4gc3Rkb3V0CgoKZGVmIGNoZWNrX29jcihsYW5ndWFnZSk6CiAgICBpZiBub3QgcmUuZnVsbG1hdGNoKHIiW2EtekEtWjAtOV9dKyg/OlwrW2EtekEtWjAtOV9dKykqIiwgbGFuZ3VhZ2UpOgogICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoIlVuZ8O8bHRpZ2UgT0NSLVNwcmFjaGUiKQogICAgdHJ5OgogICAgICAgIGxhbmd1YWdlcyA9IHJ1bl9vY3JfY29tbWFuZChbInRlc3NlcmFjdCIsICItLWxpc3QtbGFuZ3MiXSkuc3BsaXRsaW5lcygpCiAgICBleGNlcHQgRmlsZU5vdEZvdW5kRXJyb3IgYXMgZXJyb3I6CiAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJUZXNzZXJhY3QgZmVobHQuIEJpdHRlIHRlc3NlcmFjdC1vY3IgdW5kIGRpZSBTcHJhY2hwYWtldGUgdGVzc2VyYWN0LW9jci1kZXUvdGVzc2VyYWN0LW9jci1lbmcgaW5zdGFsbGllcmVuLiIpIGZyb20gZXJyb3IKICAgIG1pc3NpbmcgPSBzZXQobGFuZ3VhZ2Uuc3BsaXQoIisiKSkgLSB7bGluZS5zdHJpcCgpIGZvciBsaW5lIGluIGxhbmd1YWdlc30KICAgIGlmIG1pc3Npbmc6CiAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJPQ1ItU3ByYWNocGFrZXRlIGZlaGxlbjogIiArICIsICIuam9pbihzb3J0ZWQobWlzc2luZykpKQoKCmRlZiBjcmVhdGVfZG9jdW1lbnQoaW5wdXRzLCBvdXRwdXQsIHJlc29sdXRpb24sIHVzZV9wb3N0cHJvY2Vzcz1GYWxzZSwgcHJvZmlsZT0iZG9jdW1lbnQiLCBzdHJpY3Q9RmFsc2UsCiAgICAgICAgICAgICAgICAgICAgb2NyPUZhbHNlLCBvY3JfbGFuZ3VhZ2U9ImRldStlbmciLCBiYWNrZ3JvdW5kX2NsZWFudXA9RmFsc2UpOgogICAgaWYgbm90IGlucHV0czoKICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJLZWluZSBTZWl0ZW4gdm9yaGFuZGVuIikKICAgIGlmIG5vdCBtYXRoLmlzZmluaXRlKHJlc29sdXRpb24pIG9yIHJlc29sdXRpb24gPD0gMDoKICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJVbmfDvGx0aWdlIFNjYW4tQXVmbMO2c3VuZyIpCiAgICBvdXRwdXQgPSBQYXRoKG91dHB1dCkKICAgIGZpbGVfZm9ybWF0ID0geyIucGRmIjogIlBERiIsICIucG5nIjogIlBORyIsICIudGlmZiI6ICJUSUZGIn0uZ2V0KG91dHB1dC5zdWZmaXgubG93ZXIoKSkKICAgIGlmIG5vdCBmaWxlX2Zvcm1hdDoKICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJVbmJla2FubnRlcyBBdXNnYWJlZm9ybWF0IikKICAgIGlmIChsZW4oaW5wdXRzKSAhPSAxIG9yIG9jcikgYW5kIGZpbGVfZm9ybWF0ICE9ICJQREYiOgogICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoIk1laHJzZWl0aWdlIFNjYW5zIHVuZCBPQ1IgYmVuw7Z0aWdlbiBQREYgYWxzIEF1c2dhYmVmb3JtYXQiKQogICAgaWYgb2NyOgogICAgICAgIGNoZWNrX29jcihvY3JfbGFuZ3VhZ2UpCgogICAgd2FybmluZ3MgPSBbXQogICAgcHJvY2Vzc29yID0gTm9uZQogICAgaWYgdXNlX3Bvc3Rwcm9jZXNzOgogICAgICAgIHRyeToKICAgICAgICAgICAgZnJvbSBvcGVuY3ZfcG9zdHByb2Nlc3MgaW1wb3J0IHBvc3Rwcm9jZXNzCiAgICAgICAgICAgIHByb2Nlc3NvciA9IHBvc3Rwcm9jZXNzCiAgICAgICAgZXhjZXB0IEltcG9ydEVycm9yOgogICAgICAgICAgICBpZiBzdHJpY3Q6CiAgICAgICAgICAgICAgICByYWlzZQogICAgICAgICAgICB3YXJuaW5ncy5hcHBlbmQoIk9wZW5DViBmZWhsdDsgYWxsZSBTZWl0ZW4gd3VyZGVuIG9obmUgQmlsZGtvcnJla3R1ciDDvGJlcm5vbW1lbi4iKQogICAgaWYgYmFja2dyb3VuZF9jbGVhbnVwOgogICAgICAgIGZyb20gc2Nhbl9iYWNrZ3JvdW5kIGltcG9ydCBjbGVhbl9zY2FuX2JhY2tncm91bmQKCiAgICBwYXJ0aWFsID0gb3V0cHV0LndpdGhfbmFtZShvdXRwdXQubmFtZSArICIucGFydGlhbCIpCiAgICB0cnk6CiAgICAgICAgd2l0aCB0ZW1wZmlsZS5UZW1wb3JhcnlEaXJlY3RvcnkocHJlZml4PSJwcmVwYXJlZC0iLCBkaXI9b3V0cHV0LnBhcmVudCkgYXMgZGlyZWN0b3J5OgogICAgICAgICAgICBwcmVwYXJlZCA9IFtdCiAgICAgICAgICAgIGZvciBpbmRleCwgaW5wdXRfcGF0aCBpbiBlbnVtZXJhdGUoaW5wdXRzKToKICAgICAgICAgICAgICAgIGltYWdlX3BhdGggPSBQYXRoKGlucHV0X3BhdGgpCiAgICAgICAgICAgICAgICBjb3JyZWN0ZWQgPSBQYXRoKGRpcmVjdG9yeSkgLyBmImNvcnJlY3RlZC17aW5kZXg6MDZ9LnBuZyIKICAgICAgICAgICAgICAgIGlmIHByb2Nlc3NvcjoKICAgICAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgICAgIHByb2Nlc3NvcihpbWFnZV9wYXRoLCBjb3JyZWN0ZWQsIHByb2ZpbGUpCiAgICAgICAgICAgICAgICAgICAgICAgIGltYWdlX3BhdGggPSBjb3JyZWN0ZWQKICAgICAgICAgICAgICAgICAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgICAgICAgICAgICAgICAgICBpZiBzdHJpY3Q6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICByYWlzZQogICAgICAgICAgICAgICAgICAgICAgICB3YXJuaW5ncy5hcHBlbmQoZiJTZWl0ZSB7aW5kZXggKyAxfTogQmlsZGtvcnJla3R1ciBmZWhsZ2VzY2hsYWdlbjsgUm9oc2NhbiDDvGJlcm5vbW1lbi4iKQogICAgICAgICAgICAgICAgaWYgYmFja2dyb3VuZF9jbGVhbnVwOgogICAgICAgICAgICAgICAgICAgIGNsZWFuZWQgPSBQYXRoKGRpcmVjdG9yeSkgLyBmImNsZWFuZWQte2luZGV4OjA2fS5wbmciCiAgICAgICAgICAgICAgICAgICAgY2xlYW5fc2Nhbl9iYWNrZ3JvdW5kKGltYWdlX3BhdGgsIGNsZWFuZWQpCiAgICAgICAgICAgICAgICAgICAgaW1hZ2VfcGF0aCA9IGNsZWFuZWQKCiAgICAgICAgICAgICAgICAjIE9ubHkgb25lIGRlY29kZWQgcGFnZSBpcyBrZXB0IGluIG1lbW9yeSwgZXZlbiBmb3IgZHVwbGV4IHN0YWNrcy4KICAgICAgICAgICAgICAgIHdpdGggSW1hZ2Uub3BlbihpbWFnZV9wYXRoKSBhcyBpbWFnZToKICAgICAgICAgICAgICAgICAgICB3aXRoIGltYWdlLmNvbnZlcnQoIlJHQiIpIGFzIHJnYjoKICAgICAgICAgICAgICAgICAgICAgICAgaWYgb2NyOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFnZSA9IFBhdGgoZGlyZWN0b3J5KS5yZXNvbHZlKCkgLyBmInBhZ2Ute2luZGV4OjA2fS5wbmciCiAgICAgICAgICAgICAgICAgICAgICAgICAgICByZ2Iuc2F2ZShwYWdlLCAiUE5HIiwgZHBpPShyZXNvbHV0aW9uLCByZXNvbHV0aW9uKSkKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHByZXBhcmVkLmFwcGVuZChwYWdlKQogICAgICAgICAgICAgICAgICAgICAgICBlbGlmIGZpbGVfZm9ybWF0ID09ICJQREYiOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgcmdiLnNhdmUocGFydGlhbCwgIlBERiIsIHJlc29sdXRpb249cmVzb2x1dGlvbiwgcXVhbGl0eT05MCwgYXBwZW5kPWluZGV4ID4gMCkKICAgICAgICAgICAgICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJnYi5zYXZlKHBhcnRpYWwsIGZpbGVfZm9ybWF0LCBkcGk9KHJlc29sdXRpb24sIHJlc29sdXRpb24pKQogICAgICAgICAgICAgICAgY29ycmVjdGVkLnVubGluayhtaXNzaW5nX29rPVRydWUpCiAgICAgICAgICAgICAgICBpZiBiYWNrZ3JvdW5kX2NsZWFudXA6CiAgICAgICAgICAgICAgICAgICAgY2xlYW5lZC51bmxpbmsobWlzc2luZ19vaz1UcnVlKQogICAgICAgICAgICBpZiBvY3I6CiAgICAgICAgICAgICAgICBwYWdlX2xpc3QgPSBQYXRoKGRpcmVjdG9yeSkgLyAicGFnZXMudHh0IgogICAgICAgICAgICAgICAgaWYgYW55KCJcbiIgaW4gc3RyKHBhZ2UpIG9yICJcciIgaW4gc3RyKHBhZ2UpIGZvciBwYWdlIGluIHByZXBhcmVkKToKICAgICAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJEYXMgU2Nhbi1BcmJlaXRzdmVyemVpY2huaXMgZGFyZiBrZWluZW4gWmVpbGVudW1icnVjaCBlbnRoYWx0ZW4iKQogICAgICAgICAgICAgICAgcGFnZV9saXN0LndyaXRlX3RleHQoIlxuIi5qb2luKG1hcChzdHIsIHByZXBhcmVkKSkgKyAiXG4iLCBlbmNvZGluZz0idXRmOCIpCiAgICAgICAgICAgICAgICBwcmVmaXggPSBQYXRoKGRpcmVjdG9yeSkgLyAic2VhcmNoYWJsZSIKICAgICAgICAgICAgICAgIHJ1bl9vY3JfY29tbWFuZChbInRlc3NlcmFjdCIsIHN0cihwYWdlX2xpc3QpLCBzdHIocHJlZml4KSwgIi1sIiwgb2NyX2xhbmd1YWdlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiLS1kcGkiLCBzdHIocm91bmQocmVzb2x1dGlvbikpLCAicGRmIl0pCiAgICAgICAgICAgICAgICBzZWFyY2hhYmxlID0gcHJlZml4LndpdGhfc3VmZml4KCIucGRmIikKICAgICAgICAgICAgICAgIHdpdGggUGRmUGFyc2VyLlBkZlBhcnNlcihmaWxlbmFtZT1zdHIoc2VhcmNoYWJsZSkpIGFzIHBkZjoKICAgICAgICAgICAgICAgICAgICBpZiBsZW4ocGRmLnBhZ2VzKSAhPSBsZW4oaW5wdXRzKToKICAgICAgICAgICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJPQ1ItUERGIGlzdCB1bnZvbGxzdMOkbmRpZzsgUm9oc2NhbnMgYmxlaWJlbiBlcmhhbHRlbiIpCiAgICAgICAgICAgICAgICBzZWFyY2hhYmxlLnJlcGxhY2UocGFydGlhbCkKICAgICAgICAgICAgcGFydGlhbC5yZXBsYWNlKG91dHB1dCkKICAgIGZpbmFsbHk6CiAgICAgICAgcGFydGlhbC51bmxpbmsobWlzc2luZ19vaz1UcnVlKQogICAgcmV0dXJuIHsicGFnZUNvdW50IjogbGVuKGlucHV0cyksICJ3YXJuaW5ncyI6IHdhcm5pbmdzfQoKCmRlZiBpbnRlcnJ1cHRlZCgqXyk6CiAgICByYWlzZSBTeXN0ZW1FeGl0KCJTY2FuLVZlcmFyYmVpdHVuZyBhYmdlYnJvY2hlbiIpCgoKZGVmIG1haW4oKToKICAgICMgTGV0IGZpbmFsbHkgYmxvY2tzIHN0b3AgVGVzc2VyYWN0IGFuZCByZW1vdmUgaW5jb21wbGV0ZSBQREZzIG9uIGFnZW50IHRpbWVvdXQuCiAgICBzaWduYWwuc2lnbmFsKHNpZ25hbC5TSUdURVJNLCBpbnRlcnJ1cHRlZCkKICAgIHBhcnNlciA9IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPSJTY2FuLVNlaXRlbiB6dSBlaW5lciBEYXRlaSB6dXNhbW1lbmbDvGhyZW4iKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1pbnB1dHMiLCBuYXJncz0iKyIpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLW91dHB1dCIpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLXJlc29sdXRpb24iLCB0eXBlPWZsb2F0LCBkZWZhdWx0PTMwMCkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcHJvZmlsZSIsIGNob2ljZXM9WyJkb2N1bWVudCIsICJyZWNlaXB0IiwgInJhdyJdLCBkZWZhdWx0PSJkb2N1bWVudCIpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLXBvc3Rwcm9jZXNzIiwgYWN0aW9uPSJzdG9yZV90cnVlIikKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tc3RyaWN0IiwgYWN0aW9uPSJzdG9yZV90cnVlIikKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tYmFja2dyb3VuZC1jbGVhbnVwIiwgYWN0aW9uPSJzdG9yZV90cnVlIikKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tb2NyIiwgYWN0aW9uPSJzdG9yZV90cnVlIikKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tb2NyLWxhbmd1YWdlIiwgZGVmYXVsdD0iZGV1K2VuZyIpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLWNoZWNrLXJ1bnRpbWUiLCBhY3Rpb249InN0b3JlX3RydWUiKQogICAgYXJncyA9IHBhcnNlci5wYXJzZV9hcmdzKCkKICAgIGlmIGFyZ3MuY2hlY2tfcnVudGltZToKICAgICAgICBpZiBhcmdzLmJhY2tncm91bmRfY2xlYW51cCBvciAoYXJncy5wb3N0cHJvY2VzcyBhbmQgYXJncy5zdHJpY3QpOgogICAgICAgICAgICBpbXBvcnQgY3YyLCBudW1weQogICAgICAgIGlmIGFyZ3Mub2NyOgogICAgICAgICAgICBjaGVja19vY3IoYXJncy5vY3JfbGFuZ3VhZ2UpCiAgICAgICAgcHJpbnQoanNvbi5kdW1wcyh7InJlYWR5IjogVHJ1ZX0pKQogICAgICAgIHJldHVybgogICAgaWYgbm90IGFyZ3MuaW5wdXRzIG9yIG5vdCBhcmdzLm91dHB1dDoKICAgICAgICBwYXJzZXIuZXJyb3IoIi0taW5wdXRzIHVuZCAtLW91dHB1dCBzaW5kIGVyZm9yZGVybGljaCIpCiAgICBwcmludChqc29uLmR1bXBzKGNyZWF0ZV9kb2N1bWVudChhcmdzLmlucHV0cywgYXJncy5vdXRwdXQsIGFyZ3MucmVzb2x1dGlvbiwgYXJncy5wb3N0cHJvY2VzcywgYXJncy5wcm9maWxlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhcmdzLnN0cmljdCwgYXJncy5vY3IsIGFyZ3Mub2NyX2xhbmd1YWdlLCBhcmdzLmJhY2tncm91bmRfY2xlYW51cCkpKQoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICBtYWluKCkK", + "scripts/scan_background.py": "IiIiQ29uc2VydmF0aXZlIHBhcGVyIG1hc2tpbmcuIEtlZXAgdGhlIGNhbnZhcyBhbmQgZXZlcnkgcGl4ZWwgaW5zaWRlIHRoZSBwYXBlci4iIiIKZnJvbSBwYXRobGliIGltcG9ydCBQYXRoCgppbXBvcnQgY3YyCmltcG9ydCBudW1weSBhcyBucAoKCmRlZiB3aGl0ZW5fYmFja2dyb3VuZChpbWFnZSk6CiAgICBoZWlnaHQsIHdpZHRoID0gaW1hZ2Uuc2hhcGVbOjJdCiAgICBzY2FsZSA9IG1pbigxLjAsIDEwMDAuMCAvIG1heChoZWlnaHQsIHdpZHRoKSkKICAgIHNtYWxsID0gY3YyLnJlc2l6ZShpbWFnZSwgTm9uZSwgZng9c2NhbGUsIGZ5PXNjYWxlLCBpbnRlcnBvbGF0aW9uPWN2Mi5JTlRFUl9BUkVBKQogICAgZ3JheSA9IGN2Mi5jdnRDb2xvcihzbWFsbCwgY3YyLkNPTE9SX0JHUjJHUkFZKQogICAgYm9yZGVyID0gbnAuY29uY2F0ZW5hdGUoKGdyYXlbMF0sIGdyYXlbLTFdLCBncmF5WzosIDBdLCBncmF5WzosIC0xXSkpCiAgICBiYWNrZ3JvdW5kID0gZmxvYXQobnAubWVkaWFuKGJvcmRlcikpCiAgICAjIFdoaXRlIHN1cnJvdW5kaW5ncyBhbHJlYWR5IG5lZWQgbm8gY29ycmVjdGlvbi4gTmV2ZXIgc2VhcmNoIGluc2lkZSBhCiAgICAjIHdoaXRlIGRvY3VtZW50IGZvciBhbiBpc29sYXRlZCB0ZXh0IGJveCBvciBsb2dvIHRvIGtlZXAgaW5zdGVhZC4KICAgIGlmIGJhY2tncm91bmQgPj0gMjM1IG9yIG5wLnBlcmNlbnRpbGUoYm9yZGVyLCA3NSkgLSBucC5wZXJjZW50aWxlKGJvcmRlciwgMjUpID4gNTU6CiAgICAgICAgcmV0dXJuIGltYWdlCgogICAgdGhyZXNob2xkLCBfID0gY3YyLnRocmVzaG9sZChncmF5LCAwLCAyNTUsIGN2Mi5USFJFU0hfQklOQVJZICsgY3YyLlRIUkVTSF9PVFNVKQogICAgbWFzayA9IG5wLnVpbnQ4KGdyYXkgPiBtYXgodGhyZXNob2xkLCBiYWNrZ3JvdW5kICsgMjApKSAqIDI1NQogICAgbWFzayA9IGN2Mi5tb3JwaG9sb2d5RXgobWFzaywgY3YyLk1PUlBIX0NMT1NFLCBucC5vbmVzKCg1LCA1KSwgbnAudWludDgpKQogICAgY29udG91cnMsIF8gPSBjdjIuZmluZENvbnRvdXJzKG1hc2ssIGN2Mi5SRVRSX0VYVEVSTkFMLCBjdjIuQ0hBSU5fQVBQUk9YX1NJTVBMRSkKICAgIGNhbmRpZGF0ZXMgPSBbXQogICAgYXJlYSA9IGdyYXkuc2l6ZQogICAgZm9yIGNvbnRvdXIgaW4gY29udG91cnM6CiAgICAgICAgY29udG91cl9hcmVhID0gY3YyLmNvbnRvdXJBcmVhKGNvbnRvdXIpCiAgICAgICAgaWYgbm90IDAuMDE1ICogYXJlYSA8IGNvbnRvdXJfYXJlYSA8IDAuOTg1ICogYXJlYToKICAgICAgICAgICAgY29udGludWUKICAgICAgICBwZXJpbWV0ZXIgPSBjdjIuYXJjTGVuZ3RoKGNvbnRvdXIsIFRydWUpCiAgICAgICAgcG9seWdvbiA9IGN2Mi5hcHByb3hQb2x5RFAoY29udG91ciwgMC4wMTggKiBwZXJpbWV0ZXIsIFRydWUpCiAgICAgICAgaWYgbGVuKHBvbHlnb24pICE9IDQgb3Igbm90IGN2Mi5pc0NvbnRvdXJDb252ZXgocG9seWdvbik6CiAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgIyBVc2UgdGhlIHF1YWRyaWxhdGVyYWwgb25seSB0byByZWNvZ25pemUgYSBzaGVldCwgdGhlbiBrZWVwIGl0cyBmdWxsCiAgICAgICAgIyBjb250b3VyIGh1bGwgc28gcm91bmRlZCBvciBzbGlnaHRseSB1bmV2ZW4gZWRnZXMgY2Fubm90IGxvc2UgY29udGVudC4KICAgICAgICBwb2x5Z29uID0gY3YyLmNvbnZleEh1bGwoY29udG91cikKICAgICAgICBwYXBlciA9IG5wLnplcm9zX2xpa2UoZ3JheSkKICAgICAgICBjdjIuZmlsbFBvbHkocGFwZXIsIFtwb2x5Z29uXSwgMjU1KQogICAgICAgIGluc2lkZSA9IGN2Mi5lcm9kZShwYXBlciwgbnAub25lcygoNywgNyksIG5wLnVpbnQ4KSkgPiAwCiAgICAgICAgcmluZyA9IChjdjIuZGlsYXRlKHBhcGVyLCBucC5vbmVzKCg5LCA5KSwgbnAudWludDgpKSA+IDApICYgKHBhcGVyID09IDApCiAgICAgICAgaWYgaW5zaWRlLnN1bSgpIDwgMTAwIG9yIHJpbmcuc3VtKCkgPCA1MDoKICAgICAgICAgICAgY29udGludWUKICAgICAgICBwYXBlcl9saWdodCA9IGZsb2F0KG5wLm1lZGlhbihncmF5W2luc2lkZV0pKQogICAgICAgICMgQSBwYXBlciBlZGdlIG11c3Qgc2VwYXJhdGUgYSBtb3N0bHkgbGlnaHQgc2hlZXQgZnJvbSBkYXJrZXIgc2Nhbm5lcgogICAgICAgICMgYmFja2luZyBhbGwgYXJvdW5kIGl0LiBBbWJpZ3VvdXMgcGhvdG9zL3BhZ2VzIGFyZSBsZWZ0IHVudG91Y2hlZC4KICAgICAgICBpZiBwYXBlcl9saWdodCA8IGJhY2tncm91bmQgKyAzMCBvciBucC5tZWFuKGdyYXlbcmluZ10gPCBwYXBlcl9saWdodCAtIDIwKSA8IDAuODU6CiAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgY2FuZGlkYXRlcy5hcHBlbmQocG9seWdvbikKCiAgICBpZiBub3QgY2FuZGlkYXRlczoKICAgICAgICByZXR1cm4gaW1hZ2UKICAgIGtlZXBfc21hbGwgPSBucC56ZXJvc19saWtlKGdyYXkpCiAgICBjdjIuZmlsbFBvbHkoa2VlcF9zbWFsbCwgY2FuZGlkYXRlcywgMjU1KQogICAgIyBEbyBub3QgZXJhc2UgYW5vdGhlciBzaWduaWZpY2FudCBvYmplY3QgdGhhdCB3ZSBjb3VsZCBub3QgY2xhc3NpZnkuCiAgICBvdXRzaWRlID0gKGtlZXBfc21hbGwgPT0gMCkKICAgIGlmIG5wLm1lYW4oZ3JheVtvdXRzaWRlXSA+IGJhY2tncm91bmQgKyAzMCkgPiAwLjA0OgogICAgICAgIHJldHVybiBpbWFnZQogICAga2VlcCA9IG5wLnplcm9zKChoZWlnaHQsIHdpZHRoKSwgZHR5cGU9bnAudWludDgpCiAgICBwb2x5Z29ucyA9IFtucC5yaW50KHBvbHlnb24gLyBzY2FsZSkuYXN0eXBlKG5wLmludDMyKSBmb3IgcG9seWdvbiBpbiBjYW5kaWRhdGVzXQogICAgY3YyLmZpbGxQb2x5KGtlZXAsIHBvbHlnb25zLCAyNTUpCiAgICAjIFJvdW5kaW5nL2Rvd25zYW1wbGluZyB1bmNlcnRhaW50eSBncm93cyB3aXRoIHRoZSBvcmlnaW5hbCBzY2FuIERQSS4KICAgIHBhZGRpbmcgPSBtYXgoMiwgaW50KG5wLmNlaWwoMiAvIHNjYWxlKSkpCiAgICBrZWVwID0gY3YyLmRpbGF0ZShrZWVwLCBucC5vbmVzKCgyICogcGFkZGluZyArIDEsIDIgKiBwYWRkaW5nICsgMSksIG5wLnVpbnQ4KSkKICAgIHJlc3VsdCA9IGltYWdlLmNvcHkoKQogICAgcmVzdWx0W2tlZXAgPT0gMF0gPSAyNTUKICAgIHJldHVybiByZXN1bHQKCgpkZWYgY2xlYW5fc2Nhbl9iYWNrZ3JvdW5kKGlucHV0X3BhdGg6IFBhdGgsIG91dHB1dF9wYXRoOiBQYXRoKToKICAgIGltYWdlID0gY3YyLmltcmVhZChzdHIoaW5wdXRfcGF0aCksIGN2Mi5JTVJFQURfQ09MT1IpCiAgICBpZiBpbWFnZSBpcyBOb25lOgogICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigiU2Nhbi1CaWxkIGtvbm50ZSBuaWNodCBnZWxlc2VuIHdlcmRlbiIpCiAgICBpZiBub3QgY3YyLmltd3JpdGUoc3RyKG91dHB1dF9wYXRoKSwgd2hpdGVuX2JhY2tncm91bmQoaW1hZ2UpKToKICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoIkJlcmVpbmlndGVzIFNjYW4tQmlsZCBrb25udGUgbmljaHQgZ2VzcGVpY2hlcnQgd2VyZGVuIikK", "scripts/opencv_postprocess.py": "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwppbXBvcnQgYXJncGFyc2UKaW1wb3J0IG1hdGgKZnJvbSBwYXRobGliIGltcG9ydCBQYXRoCgppbXBvcnQgY3YyCmltcG9ydCBudW1weSBhcyBucApmcm9tIFBJTCBpbXBvcnQgSW1hZ2UKCgpkZWYgb3JkZXJfcG9pbnRzKHBvaW50cyk6CiAgICByZWN0ID0gbnAuemVyb3MoKDQsIDIpLCBkdHlwZT0iZmxvYXQzMiIpCiAgICBwb2ludF9zdW0gPSBwb2ludHMuc3VtKGF4aXM9MSkKICAgIHBvaW50X2RpZmYgPSBucC5kaWZmKHBvaW50cywgYXhpcz0xKQoKICAgIHJlY3RbMF0gPSBwb2ludHNbbnAuYXJnbWluKHBvaW50X3N1bSldCiAgICByZWN0WzJdID0gcG9pbnRzW25wLmFyZ21heChwb2ludF9zdW0pXQogICAgcmVjdFsxXSA9IHBvaW50c1tucC5hcmdtaW4ocG9pbnRfZGlmZildCiAgICByZWN0WzNdID0gcG9pbnRzW25wLmFyZ21heChwb2ludF9kaWZmKV0KICAgIHJldHVybiByZWN0CgoKZGVmIGZvdXJfcG9pbnRfdHJhbnNmb3JtKGltYWdlLCBwb2ludHMpOgogICAgcmVjdCA9IG9yZGVyX3BvaW50cyhwb2ludHMpCiAgICB0b3BfbGVmdCwgdG9wX3JpZ2h0LCBib3R0b21fcmlnaHQsIGJvdHRvbV9sZWZ0ID0gcmVjdAoKICAgIHdpZHRoX2EgPSBucC5saW5hbGcubm9ybShib3R0b21fcmlnaHQgLSBib3R0b21fbGVmdCkKICAgIHdpZHRoX2IgPSBucC5saW5hbGcubm9ybSh0b3BfcmlnaHQgLSB0b3BfbGVmdCkKICAgIG1heF93aWR0aCA9IGludChtYXgod2lkdGhfYSwgd2lkdGhfYikpCgogICAgaGVpZ2h0X2EgPSBucC5saW5hbGcubm9ybSh0b3BfcmlnaHQgLSBib3R0b21fcmlnaHQpCiAgICBoZWlnaHRfYiA9IG5wLmxpbmFsZy5ub3JtKHRvcF9sZWZ0IC0gYm90dG9tX2xlZnQpCiAgICBtYXhfaGVpZ2h0ID0gaW50KG1heChoZWlnaHRfYSwgaGVpZ2h0X2IpKQoKICAgIGRlc3RpbmF0aW9uID0gbnAuYXJyYXkoWwogICAgICAgIFswLCAwXSwKICAgICAgICBbbWF4X3dpZHRoIC0gMSwgMF0sCiAgICAgICAgW21heF93aWR0aCAtIDEsIG1heF9oZWlnaHQgLSAxXSwKICAgICAgICBbMCwgbWF4X2hlaWdodCAtIDFdLAogICAgXSwgZHR5cGU9ImZsb2F0MzIiKQoKICAgIG1hdHJpeCA9IGN2Mi5nZXRQZXJzcGVjdGl2ZVRyYW5zZm9ybShyZWN0LCBkZXN0aW5hdGlvbikKICAgIHJldHVybiBjdjIud2FycFBlcnNwZWN0aXZlKGltYWdlLCBtYXRyaXgsIChtYXhfd2lkdGgsIG1heF9oZWlnaHQpLCBib3JkZXJWYWx1ZT0oMjU1LCAyNTUsIDI1NSkpCgoKZGVmIHJvdGF0ZV9ib3VuZChpbWFnZSwgYW5nbGUpOgogICAgaGVpZ2h0LCB3aWR0aCA9IGltYWdlLnNoYXBlWzoyXQogICAgY2VudGVyID0gKHdpZHRoIC8gMiwgaGVpZ2h0IC8gMikKICAgIG1hdHJpeCA9IGN2Mi5nZXRSb3RhdGlvbk1hdHJpeDJEKGNlbnRlciwgYW5nbGUsIDEuMCkKICAgIGNvcyA9IGFicyhtYXRyaXhbMCwgMF0pCiAgICBzaW4gPSBhYnMobWF0cml4WzAsIDFdKQoKICAgIG5ld193aWR0aCA9IGludCgoaGVpZ2h0ICogc2luKSArICh3aWR0aCAqIGNvcykpCiAgICBuZXdfaGVpZ2h0ID0gaW50KChoZWlnaHQgKiBjb3MpICsgKHdpZHRoICogc2luKSkKCiAgICBtYXRyaXhbMCwgMl0gKz0gKG5ld193aWR0aCAvIDIpIC0gY2VudGVyWzBdCiAgICBtYXRyaXhbMSwgMl0gKz0gKG5ld19oZWlnaHQgLyAyKSAtIGNlbnRlclsxXQoKICAgIHJldHVybiBjdjIud2FycEFmZmluZShpbWFnZSwgbWF0cml4LCAobmV3X3dpZHRoLCBuZXdfaGVpZ2h0KSwgYm9yZGVyVmFsdWU9KDI1NSwgMjU1LCAyNTUpKQoKCmRlZiBkZXNrZXdfYnlfdGV4dF9hbmdsZShpbWFnZSk6CiAgICBncmF5ID0gY3YyLmN2dENvbG9yKGltYWdlLCBjdjIuQ09MT1JfQkdSMkdSQVkpCiAgICBpbnZlcnRlZCA9IGN2Mi5iaXR3aXNlX25vdChncmF5KQogICAgdGhyZXNob2xkID0gY3YyLnRocmVzaG9sZChpbnZlcnRlZCwgMCwgMjU1LCBjdjIuVEhSRVNIX0JJTkFSWSB8IGN2Mi5USFJFU0hfT1RTVSlbMV0KICAgIGNvb3JkaW5hdGVzID0gbnAuY29sdW1uX3N0YWNrKG5wLndoZXJlKHRocmVzaG9sZCA+IDApKQoKICAgIGlmIGxlbihjb29yZGluYXRlcykgPCA1MDA6CiAgICAgICAgcmV0dXJuIGltYWdlCgogICAgYW5nbGUgPSBjdjIubWluQXJlYVJlY3QoY29vcmRpbmF0ZXMpWy0xXQogICAgaWYgYW5nbGUgPCAtNDU6CiAgICAgICAgYW5nbGUgPSAtKDkwICsgYW5nbGUpCiAgICBlbHNlOgogICAgICAgIGFuZ2xlID0gLWFuZ2xlCgogICAgaWYgYWJzKGFuZ2xlKSA8IDAuMiBvciBhYnMoYW5nbGUpID4gODoKICAgICAgICByZXR1cm4gaW1hZ2UKCiAgICByZXR1cm4gcm90YXRlX2JvdW5kKGltYWdlLCBhbmdsZSkKCgpkZWYgZmluZF9kb2N1bWVudF9jb250b3VyKGltYWdlLCBwcm9maWxlKToKICAgIHJhdGlvID0gaW1hZ2Uuc2hhcGVbMF0gLyA5MDAuMAogICAgcmVzaXplZCA9IGN2Mi5yZXNpemUoaW1hZ2UsIChpbnQoaW1hZ2Uuc2hhcGVbMV0gLyByYXRpbyksIDkwMCkpCiAgICBncmF5ID0gY3YyLmN2dENvbG9yKHJlc2l6ZWQsIGN2Mi5DT0xPUl9CR1IyR1JBWSkKICAgIGdyYXkgPSBjdjIuR2F1c3NpYW5CbHVyKGdyYXksICg1LCA1KSwgMCkKCiAgICBlZGdlcyA9IGN2Mi5DYW5ueShncmF5LCA0NSwgMTQwKQogICAga2VybmVsID0gY3YyLmdldFN0cnVjdHVyaW5nRWxlbWVudChjdjIuTU9SUEhfUkVDVCwgKDcsIDcpKQogICAgZWRnZXMgPSBjdjIubW9ycGhvbG9neUV4KGVkZ2VzLCBjdjIuTU9SUEhfQ0xPU0UsIGtlcm5lbCkKCiAgICBjb250b3VycywgXyA9IGN2Mi5maW5kQ29udG91cnMoZWRnZXMsIGN2Mi5SRVRSX0VYVEVSTkFMLCBjdjIuQ0hBSU5fQVBQUk9YX1NJTVBMRSkKICAgIGNvbnRvdXJzID0gc29ydGVkKGNvbnRvdXJzLCBrZXk9Y3YyLmNvbnRvdXJBcmVhLCByZXZlcnNlPVRydWUpWzo4XQoKICAgIG1pbl9hcmVhID0gcmVzaXplZC5zaGFwZVswXSAqIHJlc2l6ZWQuc2hhcGVbMV0gKiAoMC4wMyBpZiBwcm9maWxlID09ICJyZWNlaXB0IiBlbHNlIDAuMTIpCgogICAgZm9yIGNvbnRvdXIgaW4gY29udG91cnM6CiAgICAgICAgaWYgY3YyLmNvbnRvdXJBcmVhKGNvbnRvdXIpIDwgbWluX2FyZWE6CiAgICAgICAgICAgIGNvbnRpbnVlCgogICAgICAgIHBlcmltZXRlciA9IGN2Mi5hcmNMZW5ndGgoY29udG91ciwgVHJ1ZSkKICAgICAgICBhcHByb3ggPSBjdjIuYXBwcm94UG9seURQKGNvbnRvdXIsIDAuMDI1ICogcGVyaW1ldGVyLCBUcnVlKQogICAgICAgIGlmIGxlbihhcHByb3gpID09IDQ6CiAgICAgICAgICAgIHJldHVybiBhcHByb3gucmVzaGFwZSg0LCAyKSAqIHJhdGlvCgogICAgcmV0dXJuIE5vbmUKCgpkZWYgdHJpbV9saWdodF9ib3JkZXIoaW1hZ2UpOgogICAgZ3JheSA9IGN2Mi5jdnRDb2xvcihpbWFnZSwgY3YyLkNPTE9SX0JHUjJHUkFZKQogICAgbWFzayA9IGN2Mi50aHJlc2hvbGQoZ3JheSwgMjQ1LCAyNTUsIGN2Mi5USFJFU0hfQklOQVJZX0lOVilbMV0KICAgIGtlcm5lbCA9IGN2Mi5nZXRTdHJ1Y3R1cmluZ0VsZW1lbnQoY3YyLk1PUlBIX1JFQ1QsICg5LCA5KSkKICAgIG1hc2sgPSBjdjIubW9ycGhvbG9neUV4KG1hc2ssIGN2Mi5NT1JQSF9DTE9TRSwga2VybmVsKQoKICAgIGNvbnRvdXJzLCBfID0gY3YyLmZpbmRDb250b3VycyhtYXNrLCBjdjIuUkVUUl9FWFRFUk5BTCwgY3YyLkNIQUlOX0FQUFJPWF9TSU1QTEUpCiAgICBpZiBub3QgY29udG91cnM6CiAgICAgICAgcmV0dXJuIGltYWdlCgogICAgY29udG91ciA9IG1heChjb250b3Vycywga2V5PWN2Mi5jb250b3VyQXJlYSkKICAgIGlmIGN2Mi5jb250b3VyQXJlYShjb250b3VyKSA8IGltYWdlLnNoYXBlWzBdICogaW1hZ2Uuc2hhcGVbMV0gKiAwLjAyOgogICAgICAgIHJldHVybiBpbWFnZQoKICAgIHgsIHksIHdpZHRoLCBoZWlnaHQgPSBjdjIuYm91bmRpbmdSZWN0KGNvbnRvdXIpCiAgICBwYWRkaW5nID0gbWF4KDEyLCBpbnQobWluKHdpZHRoLCBoZWlnaHQpICogMC4wMjUpKQogICAgeCA9IG1heCgwLCB4IC0gcGFkZGluZykKICAgIHkgPSBtYXgoMCwgeSAtIHBhZGRpbmcpCiAgICB3aWR0aCA9IG1pbihpbWFnZS5zaGFwZVsxXSAtIHgsIHdpZHRoICsgcGFkZGluZyAqIDIpCiAgICBoZWlnaHQgPSBtaW4oaW1hZ2Uuc2hhcGVbMF0gLSB5LCBoZWlnaHQgKyBwYWRkaW5nICogMikKICAgIHJldHVybiBpbWFnZVt5OnkgKyBoZWlnaHQsIHg6eCArIHdpZHRoXQoKCmRlZiBlbmhhbmNlX3JlY2VpcHQoaW1hZ2UpOgogICAgZ3JheSA9IGN2Mi5jdnRDb2xvcihpbWFnZSwgY3YyLkNPTE9SX0JHUjJHUkFZKQogICAgY2xhaGUgPSBjdjIuY3JlYXRlQ0xBSEUoY2xpcExpbWl0PTIuMCwgdGlsZUdyaWRTaXplPSg4LCA4KSkKICAgIGdyYXkgPSBjbGFoZS5hcHBseShncmF5KQogICAgZ3JheSA9IGN2Mi5mYXN0TmxNZWFuc0Rlbm9pc2luZyhncmF5LCBOb25lLCA4LCA3LCAyMSkKICAgIGdyYXkgPSBjdjIubm9ybWFsaXplKGdyYXksIE5vbmUsIDAsIDI1NSwgY3YyLk5PUk1fTUlOTUFYKQogICAgcmV0dXJuIGN2Mi5jdnRDb2xvcihncmF5LCBjdjIuQ09MT1JfR1JBWTJCR1IpCgoKZGVmIGVuaGFuY2VfZG9jdW1lbnQoaW1hZ2UpOgogICAgbGFiID0gY3YyLmN2dENvbG9yKGltYWdlLCBjdjIuQ09MT1JfQkdSMkxBQikKICAgIGxfY2hhbm5lbCwgYV9jaGFubmVsLCBiX2NoYW5uZWwgPSBjdjIuc3BsaXQobGFiKQogICAgY2xhaGUgPSBjdjIuY3JlYXRlQ0xBSEUoY2xpcExpbWl0PTEuNiwgdGlsZUdyaWRTaXplPSg4LCA4KSkKICAgIGxfY2hhbm5lbCA9IGNsYWhlLmFwcGx5KGxfY2hhbm5lbCkKICAgIHJldHVybiBjdjIuY3Z0Q29sb3IoY3YyLm1lcmdlKChsX2NoYW5uZWwsIGFfY2hhbm5lbCwgYl9jaGFubmVsKSksIGN2Mi5DT0xPUl9MQUIyQkdSKQoKCmRlZiBhdXRvX3JvdGF0ZV9wcm9maWxlKGltYWdlLCBwcm9maWxlKToKICAgIGhlaWdodCwgd2lkdGggPSBpbWFnZS5zaGFwZVs6Ml0KCiAgICBpZiBwcm9maWxlID09ICJyZWNlaXB0IiBhbmQgd2lkdGggPiBoZWlnaHQ6CiAgICAgICAgcmV0dXJuIGN2Mi5yb3RhdGUoaW1hZ2UsIGN2Mi5ST1RBVEVfOTBfQ0xPQ0tXSVNFKQoKICAgIHJldHVybiBpbWFnZQoKCmRlZiBwb3N0cHJvY2VzcyhpbnB1dF9wYXRoLCBvdXRwdXRfcGF0aCwgcHJvZmlsZSk6CiAgICBpbWFnZSA9IGN2Mi5pbXJlYWQoc3RyKGlucHV0X3BhdGgpLCBjdjIuSU1SRUFEX0NPTE9SKQogICAgaWYgaW1hZ2UgaXMgTm9uZToKICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoZiJPcGVuQ1Yga29ubnRlIHtpbnB1dF9wYXRofSBuaWNodCBsZXNlbiIpCgogICAgY29udG91ciA9IGZpbmRfZG9jdW1lbnRfY29udG91cihpbWFnZSwgcHJvZmlsZSkKICAgIGlmIGNvbnRvdXIgaXMgbm90IE5vbmU6CiAgICAgICAgcHJvY2Vzc2VkID0gZm91cl9wb2ludF90cmFuc2Zvcm0oaW1hZ2UsIGNvbnRvdXIuYXN0eXBlKCJmbG9hdDMyIikpCiAgICBlbHNlOgogICAgICAgIHByb2Nlc3NlZCA9IHRyaW1fbGlnaHRfYm9yZGVyKGltYWdlKQoKICAgIHByb2Nlc3NlZCA9IGRlc2tld19ieV90ZXh0X2FuZ2xlKHByb2Nlc3NlZCkKICAgIHByb2Nlc3NlZCA9IHRyaW1fbGlnaHRfYm9yZGVyKHByb2Nlc3NlZCkKICAgIHByb2Nlc3NlZCA9IGF1dG9fcm90YXRlX3Byb2ZpbGUocHJvY2Vzc2VkLCBwcm9maWxlKQoKICAgIGlmIHByb2ZpbGUgPT0gInJlY2VpcHQiOgogICAgICAgIHByb2Nlc3NlZCA9IGVuaGFuY2VfcmVjZWlwdChwcm9jZXNzZWQpCiAgICBlbGlmIHByb2ZpbGUgIT0gInJhdyI6CiAgICAgICAgcHJvY2Vzc2VkID0gZW5oYW5jZV9kb2N1bWVudChwcm9jZXNzZWQpCgogICAgc2F2ZV9vdXRwdXQocHJvY2Vzc2VkLCBvdXRwdXRfcGF0aCkKCgpkZWYgc2F2ZV9vdXRwdXQoaW1hZ2UsIG91dHB1dF9wYXRoKToKICAgIHN1ZmZpeCA9IG91dHB1dF9wYXRoLnN1ZmZpeC5sb3dlcigpCgogICAgaWYgc3VmZml4ID09ICIucGRmIjoKICAgICAgICByZ2IgPSBjdjIuY3Z0Q29sb3IoaW1hZ2UsIGN2Mi5DT0xPUl9CR1IyUkdCKQogICAgICAgIHBpbF9pbWFnZSA9IEltYWdlLmZyb21hcnJheShyZ2IpCiAgICAgICAgaWYgcGlsX2ltYWdlLm1vZGUgIT0gIlJHQiI6CiAgICAgICAgICAgIHBpbF9pbWFnZSA9IHBpbF9pbWFnZS5jb252ZXJ0KCJSR0IiKQogICAgICAgIHBpbF9pbWFnZS5zYXZlKG91dHB1dF9wYXRoLCAiUERGIiwgcmVzb2x1dGlvbj0zMDAuMCkKICAgICAgICByZXR1cm4KCiAgICBpZiBzdWZmaXggaW4geyIuanBnIiwgIi5qcGVnIn06CiAgICAgICAgY3YyLmltd3JpdGUoc3RyKG91dHB1dF9wYXRoKSwgaW1hZ2UsIFtjdjIuSU1XUklURV9KUEVHX1FVQUxJVFksIDkyXSkKICAgICAgICByZXR1cm4KCiAgICBpZiBzdWZmaXggPT0gIi5wbmciOgogICAgICAgIGN2Mi5pbXdyaXRlKHN0cihvdXRwdXRfcGF0aCksIGltYWdlLCBbY3YyLklNV1JJVEVfUE5HX0NPTVBSRVNTSU9OLCAzXSkKICAgICAgICByZXR1cm4KCiAgICBpZiBzdWZmaXggaW4geyIudGlmIiwgIi50aWZmIn06CiAgICAgICAgY3YyLmltd3JpdGUoc3RyKG91dHB1dF9wYXRoKSwgaW1hZ2UpCiAgICAgICAgcmV0dXJuCgogICAgcmFpc2UgUnVudGltZUVycm9yKGYiTmljaHQgdW50ZXJzdMO8dHp0ZXMgQXVzZ2FiZWZvcm1hdDoge3N1ZmZpeH0iKQoKCmRlZiBtYWluKCk6CiAgICBwYXJzZXIgPSBhcmdwYXJzZS5Bcmd1bWVudFBhcnNlcihkZXNjcmlwdGlvbj0iRkVERU8gU2Nhbi1OYWNoYmVhcmJlaXR1bmcgbWl0IE9wZW5DViIpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLWlucHV0IiwgcmVxdWlyZWQ9VHJ1ZSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tb3V0cHV0IiwgcmVxdWlyZWQ9VHJ1ZSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcHJvZmlsZSIsIGRlZmF1bHQ9ImRvY3VtZW50IiwgY2hvaWNlcz1bImRvY3VtZW50IiwgInJlY2VpcHQiLCAicmF3Il0pCiAgICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3MoKQoKICAgIHBvc3Rwcm9jZXNzKFBhdGgoYXJncy5pbnB1dCksIFBhdGgoYXJncy5vdXRwdXQpLCBhcmdzLnByb2ZpbGUpCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIG1haW4oKQo=", "scripts/setup-opencv.sh": "IyEvdXNyL2Jpbi9lbnYgc2gKc2V0IC1ldQoKU0NSSVBUX0RJUj0iJChDRFBBVEg9IGNkIC0tICIkKGRpcm5hbWUgLS0gIiQwIikiICYmIHB3ZCkiCkFHRU5UX0RJUj0iJChDRFBBVEg9IGNkIC0tICIkU0NSSVBUX0RJUi8uLiIgJiYgcHdkKSIKVkVOVl9ESVI9IiR7RkVERU9fU0NBTl9QT1NUUFJPQ0VTU19WRU5WOi0kQUdFTlRfRElSLy52ZW52LW9wZW5jdn0iClBZVEhPTl9CSU49IiR7UFlUSE9OOi1weXRob24zfSIKCmVjaG8gIkZFREVPIE9wZW5DVi1VbWdlYnVuZyB3aXJkIHZvcmJlcmVpdGV0IgplY2hvICJBZ2VudDogJEFHRU5UX0RJUiIKZWNobyAiVmVudjogICRWRU5WX0RJUiIKCmlmICEgY29tbWFuZCAtdiAiJFBZVEhPTl9CSU4iID4vZGV2L251bGwgMj4mMTsgdGhlbgogIGVjaG8gIkZlaGxlcjogJFBZVEhPTl9CSU4gd3VyZGUgbmljaHQgZ2VmdW5kZW4uIiA+JjIKICBleGl0IDEKZmkKCiIkUFlUSE9OX0JJTiIgLW0gdmVudiAiJFZFTlZfRElSIgoiJFZFTlZfRElSL2Jpbi9weXRob24iIC1tIHBpcCBpbnN0YWxsIC0tdXBncmFkZSBwaXAKIiRWRU5WX0RJUi9iaW4vcHl0aG9uIiAtbSBwaXAgaW5zdGFsbCAtciAiJEFHRU5UX0RJUi9yZXF1aXJlbWVudHMtb3BlbmN2LnR4dCIKIiRWRU5WX0RJUi9iaW4vcHl0aG9uIiAtYyAiaW1wb3J0IGN2MiwgUElMLCBudW1weTsgcHJpbnQoJ09wZW5DViBPSycpIgoKZWNobwplY2hvICJGZXJ0aWcuIFZlcndlbmRlIGluIC5lbnY6IgplY2hvICJGRURFT19TQ0FOX1BPU1RQUk9DRVNTPXRydWUiCmVjaG8gIkZFREVPX1NDQU5fUE9TVFBST0NFU1NfUFlUSE9OPSRWRU5WX0RJUi9iaW4vcHl0aG9uIgo=", "requirements-opencv.txt": "b3BlbmN2LXB5dGhvbi1oZWFkbGVzcz49NC45ClBpbGxvdz49MTAuMApudW1weT49MS4yNgo=", diff --git a/frontend/components/AgentSetupWizard.vue b/frontend/components/AgentSetupWizard.vue index e61c436b..cd6df84f 100644 --- a/frontend/components/AgentSetupWizard.vue +++ b/frontend/components/AgentSetupWizard.vue @@ -38,7 +38,14 @@ const installCommand = computed(() => 'bash ' + filename.value) const tokenReady = computed(() => /^fedeo_agent_[a-f0-9]{64}$/.test(token.value.trim())) const online = computed(() => agentSetupReady(agent.value, checkingSince.value, now.value)) const scannerOptions = computed(() => (agent.value?.scannerNames || []).map(value => ({ label: value, value }))) -const readyToSave = computed(() => online.value && agent.value?.capabilities?.scan && scannerOptions.value.some(item => item.value === selectedScanner.value)) +const setupScanDefaults = computed(() => profile.value === 'ix1600' ? { ...ix1600ScanDefaults } : { + format: 'pdf', resolution: 300, mode: 'Color', paperSize: 'auto', postprocess: false, + backgroundCleanup: true, ocr: true, ocrLanguage: 'deu+eng', ...agent.value?.scanDefaults, +}) +const processingMissing = computed(() => + (setupScanDefaults.value.format === 'pdf' && setupScanDefaults.value.ocr && !agent.value?.capabilities?.ocr) || + (setupScanDefaults.value.backgroundCleanup && !agent.value?.capabilities?.backgroundCleanup)) +const readyToSave = computed(() => online.value && agent.value?.capabilities?.scan && !processingMissing.value && scannerOptions.value.some(item => item.value === selectedScanner.value)) const endpointPreview = computed(() => { try { return agentBackendUrl(backendUrl.value) + '/instance-agent/heartbeat' } catch { return '' } }) @@ -129,10 +136,7 @@ const finishSetup = async () => { if (!agent.value || !readyToSave.value || busy.value) return busy.value = true try { - const scanDefaults = profile.value === 'ix1600' ? { ...ix1600ScanDefaults } : { - format: 'pdf', resolution: 300, mode: 'Color', paperSize: 'auto', postprocess: false, ...agent.value.scanDefaults, - } - await admin.updateInstanceAgent(agent.value.id, { preferredScannerName: selectedScanner.value, scanDefaults }) + await admin.updateInstanceAgent(agent.value.id, { preferredScannerName: selectedScanner.value, scanDefaults: setupScanDefaults.value }) saved.value = true token.value = '' stopPolling() @@ -212,7 +216,7 @@ onBeforeUnmount(() => {

Installation herunterladen

-

Die Datei enthält die Agent-Software und deine Konfiguration. Sie prüft Node.js ab Version 20, SANE und Python/Pillow und installiert fehlende Systempakete mit sudo.

+

Die Datei enthält die Agent-Software und deine Konfiguration. Sie installiert bei Bedarf Node.js ab Version 20, SANE, Bildverarbeitung und OCR für Deutsch/Englisch mit sudo.

Die Installation erfolgt in deinem Benutzerverzeichnis. Starte die Datei auf dem Rechner mit dem Scanner.

Linux-Installation herunterladen @@ -253,6 +257,8 @@ onBeforeUnmount(() => { + @@ -263,7 +269,7 @@ onBeforeUnmount(() => { { label: 'Bisherige Einstellungen / allgemeiner Scanner', value: 'existing' } ]" value-key="value" label-key="label" class="w-full" /> -

Das iX1600-Profil speichert den gesamten Stapel mit Vorder- und Rückseiten in einem PDF. Weitere Einstellungen kannst du anschließend in der Scannerverwaltung ändern.

+

Das iX1600-Profil speichert Vorder- und Rückseiten als durchsuchbares PDF. Erkannter Scannerhintergrund außerhalb des Papiers wird weiß; das A4-Format bleibt erhalten.

Scanner erneut suchen Einrichtung abschließen diff --git a/frontend/components/FileScanModal.vue b/frontend/components/FileScanModal.vue index 1b2cbd95..938575bd 100644 --- a/frontend/components/FileScanModal.vue +++ b/frontend/components/FileScanModal.vue @@ -34,6 +34,9 @@ const scanForm = reactive({ source: "ADF Duplex", paperSize: "A4", postprocess: false, + backgroundCleanup: true, + ocr: true, + ocrLanguage: "deu+eng", postprocessProfile: "document" }) @@ -49,7 +52,8 @@ watch(() => scanForm.format, (format) => { const applyIx1600Preset = () => { Object.assign(scanForm, { format: "pdf", resolution: 300, mode: "Color", source: "ADF Duplex", - paperSize: "A4", postprocess: false, postprocessProfile: "document" + paperSize: "A4", postprocess: false, postprocessProfile: "document", + backgroundCleanup: true, ocr: true, ocrLanguage: "deu+eng" }) } @@ -83,6 +87,9 @@ const applyAgentDefaults = (agent) => { scanForm.source = agent.scanDefaults?.source || "ADF Duplex" scanForm.paperSize = agent.scanDefaults?.paperSize || "A4" scanForm.postprocess = Boolean(agent.scanDefaults?.postprocess) + scanForm.backgroundCleanup = agent.scanDefaults?.backgroundCleanup !== false + scanForm.ocr = agent.scanDefaults?.ocr !== false + scanForm.ocrLanguage = agent.scanDefaults?.ocrLanguage || "deu+eng" scanForm.postprocessProfile = agent.scanDefaults?.postprocessProfile || "document" if (isFeederScan.value) scanForm.format = "pdf" @@ -147,6 +154,11 @@ const waitForJob = async (jobId) => { const startScan = async () => { if (!selectedAgentId.value || scanInProgress.value) return + if ((scanForm.format === "pdf" && scanForm.ocr && !selectedAgent.value?.capabilities?.ocr) || + (scanForm.backgroundCleanup && !selectedAgent.value?.capabilities?.backgroundCleanup)) { + toast.add({ title: "Agent aktualisieren", description: "Für OCR und Hintergrundbereinigung benötigt der Scanner-Rechner die aktuelle Agent-Installation mit Tesseract und OpenCV.", color: "error" }) + return + } scanInProgress.value = true statusMessage.value = "Scan-Auftrag wird erstellt..." @@ -166,6 +178,9 @@ const startScan = async () => { source: scanForm.source || null, paperSize: scanForm.paperSize, postprocess: scanForm.postprocess, + backgroundCleanup: scanForm.backgroundCleanup, + ocr: scanForm.format === "pdf" && scanForm.ocr, + ocrLanguage: scanForm.ocrLanguage, postprocessProfile: scanForm.postprocessProfile }, target: { @@ -319,10 +334,23 @@ loadAgents()
+
+ +

Erkannte Flächen außerhalb von Bon oder Dokument werden weiß. Das Seitenformat bleibt erhalten; bei unsicheren Rändern bleibt der Scan unverändert.

+ + + + +

Texterkennung erfolgt vor dem Upload auf dem Scanner-Rechner, auch für normale Dateien ohne Eingangsbeleg.

+
+
diff --git a/frontend/composables/useAdmin.ts b/frontend/composables/useAdmin.ts index 29b3816b..53e72c45 100644 --- a/frontend/composables/useAdmin.ts +++ b/frontend/composables/useAdmin.ts @@ -144,6 +144,9 @@ export type InstanceAgent = { resolution?: number mode?: string source?: string | null + backgroundCleanup?: boolean + ocr?: boolean + ocrLanguage?: string [key: string]: any } lastSeenAt?: string | null diff --git a/frontend/pages/administration/scanners.vue b/frontend/pages/administration/scanners.vue index 33044f01..9af9b01f 100644 --- a/frontend/pages/administration/scanners.vue +++ b/frontend/pages/administration/scanners.vue @@ -31,6 +31,9 @@ const editState = reactive({ source: "", paperSize: "A4", postprocess: false, + backgroundCleanup: true, + ocr: true, + ocrLanguage: "deu+eng", postprocessProfile: "document", }) @@ -69,13 +72,17 @@ const applyAgentToForm = (agent: InstanceAgent | null) => { editState.source = agent.scanDefaults?.source || "" editState.paperSize = agent.scanDefaults?.paperSize || "A4" editState.postprocess = Boolean(agent.scanDefaults?.postprocess) + editState.backgroundCleanup = agent.scanDefaults?.backgroundCleanup !== false + editState.ocr = agent.scanDefaults?.ocr !== false + editState.ocrLanguage = agent.scanDefaults?.ocrLanguage || "deu+eng" editState.postprocessProfile = agent.scanDefaults?.postprocessProfile || "document" } const applyIx1600Preset = () => { Object.assign(editState, { format: "pdf", resolution: 300, mode: "Color", source: "ADF Duplex", - paperSize: "A4", postprocess: false, postprocessProfile: "document" + paperSize: "A4", postprocess: false, postprocessProfile: "document", + backgroundCleanup: true, ocr: true, ocrLanguage: "deu+eng" }) } @@ -132,6 +139,9 @@ const saveAgent = async () => { source: editState.source || null, paperSize: editState.paperSize, postprocess: editState.postprocess, + backgroundCleanup: editState.backgroundCleanup, + ocr: editState.format === "pdf" && editState.ocr, + ocrLanguage: editState.ocrLanguage, postprocessProfile: editState.postprocessProfile, }, }) @@ -312,8 +322,21 @@ onMounted(async () => { ]" value-key="value" label-key="label" /> +
+ +

Erkannte Flächen außerhalb von Bon oder Dokument werden weiß. Das Seitenformat bleibt erhalten; bei unsicheren Rändern bleibt der Scan unverändert.

+ + + + +

Texterkennung erfolgt vor dem Upload auf dem Scanner-Rechner, auch für normale Dateien ohne Eingangsbeleg.

+
+
- + { assert.equal(ui.requests.length, 0) }) +test('setup cannot finish with an agent missing the processing required by its profile', async t => { + const ui = await setup(t, { ...baseAgent }) + ui.report({ ...baseAgent, lastSeenAt: new Date().toISOString(), scannerNames: [scanner], capabilities: { scan: true } }) + await ui.run('checkConnection()') + ui.run('profile.value="ix1600"; step.value=3') + await ui.run('finishSetup()') + assert.equal(ui.run('processingMissing.value'), true) + assert.equal(ui.requests.length, 0) +}) + test('connection errors stop automatic requests and are visible for retry', async t => { const ui = await setup(t, { ...baseAgent }, { getInstanceAgents: async () => { throw Error('offline') } }) ui.run('step.value=2') diff --git a/frontend/tests/file-scan.test.cjs b/frontend/tests/file-scan.test.cjs index f8744533..10e9580d 100644 --- a/frontend/tests/file-scan.test.cjs +++ b/frontend/tests/file-scan.test.cjs @@ -4,7 +4,7 @@ const fs = require('node:fs') const vm = require('node:vm') const vue = require('vue') -async function setup(getJob, scanData = {}, agents = [{ id: 'agent', active: true, capabilities: { scan: true }, scannerNames: ['fujitsu:ScanSnap iX1600:test'] }]) { +async function setup(getJob, scanData = {}, agents = [{ id: 'agent', active: true, capabilities: { scan: true, ocr: true, backgroundCleanup: true }, scannerNames: ['fujitsu:ScanSnap iX1600:test'] }]) { const notices = [] const requests = [] let unmount @@ -38,6 +38,9 @@ test('duplex scanning continues past the former three-minute polling limit', asy const body = ui.requests.find(request => request.url === '/api/scan-jobs').options.body assert.equal(body.settings.source, 'ADF Duplex') assert.equal(body.settings.paperSize, 'A4') + assert.equal(body.settings.ocr, true) + assert.equal(body.settings.ocrLanguage, 'deu+eng') + assert.equal(body.settings.backgroundCleanup, true) assert.equal(body.target.folder, 'folder-1') }) @@ -72,7 +75,7 @@ test('iX1600 preset resets image settings and normalizes the output filename to }) test('setup test scan uses the requested agent even when another agent is first', async () => { - const agents = ['first', 'configured'].map(id => ({ id, active: true, capabilities: { scan: true }, scannerNames: ['scanner-' + id] })) + const agents = ['first', 'configured'].map(id => ({ id, active: true, capabilities: { scan: true, ocr: true, backgroundCleanup: true }, scannerNames: ['scanner-' + id] })) const ui = await setup(() => ({ job: { status: 'completed' } }), { agentId: 'configured' }, agents) await ui.run('startScan()') assert.equal(ui.requests.find(request => request.url === '/api/scan-jobs').options.body.agentId, 'configured') @@ -83,3 +86,10 @@ test('an unavailable setup agent never silently scans with another device', asyn await ui.run('startScan()') assert.equal(ui.requests.filter(request => request.url === '/api/scan-jobs').length, 0) }) + +test('an old agent cannot silently produce an image-only PDF when OCR was requested', async () => { + const ui = await setup(() => ({}), {}, [{ id: 'old', active: true, capabilities: { scan: true }, scannerNames: ['scanner'] }]) + await ui.run('startScan()') + assert.equal(ui.requests.filter(request => request.url === '/api/scan-jobs').length, 0) + assert.equal(ui.notices[0].title, 'Agent aktualisieren') +}) diff --git a/frontend/utils/agentInstaller.mjs b/frontend/utils/agentInstaller.mjs index 2c35842e..b72a7591 100644 --- a/frontend/utils/agentInstaller.mjs +++ b/frontend/utils/agentInstaller.mjs @@ -26,6 +26,7 @@ export function renderAgentInstaller(bundle, options) { 'FEDEO_POLL_SECONDS=5', 'FEDEO_SCAN_FORMAT=pdf', 'FEDEO_SCAN_RESOLUTION=300', 'FEDEO_SCAN_MODE=Color', 'FEDEO_SCAN_TIMEOUT_SECONDS=900', 'FEDEO_SCAN_POSTPROCESS=false', 'FEDEO_SCAN_POSTPROCESS_PROFILE=document', '', + 'FEDEO_SCAN_BACKGROUND_CLEANUP=true', 'FEDEO_SCAN_OCR=true', 'FEDEO_SCAN_OCR_LANGUAGE=deu+eng', '', ].join('\n') const replacements = { AGENT_ID: options.agentId, AUTOSTART: String(Boolean(options.autostart)), @@ -40,6 +41,7 @@ export function renderAgentInstaller(bundle, options) { export const ix1600ScanDefaults = { format: 'pdf', resolution: 300, mode: 'Color', source: 'ADF Duplex', paperSize: 'A4', postprocess: false, postprocessProfile: 'document', + backgroundCleanup: true, ocr: true, ocrLanguage: 'deu+eng', } export function agentSetupReady(agent, since = 0, now = Date.now()) { -- 2.49.1 From 9fe5a9b1da8f092102172e078b458b0276b942cc Mon Sep 17 00:00:00 2001 From: flfeders Date: Fri, 25 Sep 2026 10:10:30 +0200 Subject: [PATCH 4/5] fix(selfhost): reuse locally cached digest images during updates --- docs/selfhost-updates.md | 4 +- scripts/selfhost-update.py | 16 ++++-- scripts/update/tests/test_updater.py | 83 +++++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 8 deletions(-) diff --git a/docs/selfhost-updates.md b/docs/selfhost-updates.md index c6d41e1e..41167cd9 100644 --- a/docs/selfhost-updates.md +++ b/docs/selfhost-updates.md @@ -151,7 +151,7 @@ Aufträge liegen dauerhaft und root-lesbar unter `.updates/service/`. Nach einem 1. Exklusive Host-Sperre erwerben; offene Wiederherstellungsfälle blockieren neue Updates. 2. Release samt Prüfsummen laden, Manifest/Commit prüfen und die FEDEO-Images auf die exakten Digests setzen. 3. Aktuelle und neue Compose-Konfiguration mit Docker Compose validieren. Projektname, Container-Identitäten, Mounts, Netzwerke und Zugangsdaten erhalten. Die Shell-Umgebung überschreibt dabei keine Werte aus der Instanz-`.env`. -4. Speicherplatz prüfen: mindestens 5 GiB frei am Docker-Datenverzeichnis vor dem Pull sowie geschätzte Backup-Größe plus Reserve im Betriebsverzeichnis. Images laden, Backup-Platz erneut prüfen. +4. Speicherplatz prüfen: mindestens 5 GiB frei am Docker-Datenverzeichnis vor dem Pull sowie geschätzte Backup-Größe plus Reserve im Betriebsverzeichnis. Images mit exakt gepinntem SHA-256-Digest nur laden, wenn sie lokal fehlen (`pull --policy missing`); vorhandene Tags ohne passenden Digest reichen dafür nicht aus. Bewegliche Infrastruktur-Tags weiterhin aktualisieren (`pull --policy always`). Ein fehlendes, nicht herunterladbares Image beendet den Auftrag vor der Wartung. Backup-Platz erneut prüfen. Während der anschließenden Infrastrukturstarts und Migrationen erfolgt kein weiterer Pull (`up`/`run --pull never`), auch nicht bei einer abweichenden Pull-Policy in einer Override-Datei. 5. Schreibende Dienste und Proxy anhalten. PostgreSQL bleibt für `pg_dump` aktiv. 6. Konfiguration, Container-Metadaten, PostgreSQL-Dump im Custom-Format, globale PostgreSQL-Rollen, gestoppten MinIO-Dateispeicher und Zertifikate sichern. Dump und TAR auf Lesbarkeit prüfen und SHA-256-Prüfsummen schreiben. `backup/complete.json` markiert ein vollständig geschriebenes Backup; ein Restore-Probelauf auf einer getrennten Instanz wird dadurch nicht ersetzt. Beim Übergang vom alten Matrix-Stack enthält das TAR zusätzlich `matrix/`. Container-Metadaten enthalten die ursprünglichen Image-IDs, Mounts und Neustartrichtlinien; `complete.json` nennt die stillgelegten Dienste und erhaltenen Container. Vor dem Dateiaustausch wird deren automatischer Neustart deaktiviert. Das Zielrelease enthält keine Matrix-Dienste und keine Backend-Abhängigkeit auf Synapse mehr. @@ -163,6 +163,8 @@ Status und Phasen werden atomar in `.updates/status.json` und pro Auftrag in `st ## Fehler und Wiederherstellung +Bei einer nicht erreichbaren Registry können bereits lokal vorhandene, exakt passende Digest-Images weiterverwendet werden. Das hilft beispielsweise bei einem `401 Unauthorized` der MinIO-Registry, wenn sowohl MinIO als auch `mc` mit den Ziel-Digests bereits auf diesem Host liegen. Der Host-Updater muss diese Pull-Logik bereits enthalten; bestehende Dienstinstallationen nach Austausch des Hostskripts mit dem Dienstinstaller aktualisieren. Veröffentlichte Release-Assets werden nicht nachträglich verändert. Für Instanzen ohne diese Images bleibt eine geprüfte Bezugsquelle notwendig. Der lokale Cache ersetzt weder Sicherheitsupdates noch eine dauerhafte Versorgung mit gepflegten Images. + - **Download, Konfiguration oder Pull fehlgeschlagen:** Der laufende Stack bleibt bestehen. - **Backup oder Stop fehlgeschlagen:** Der Updater versucht, die zuvor laufenden Container wieder zu starten. Gelingt das nicht, bleibt der Wiederherstellungsstatus gesetzt. - **Ab Austausch der Dateien/Infrastruktur:** Keine automatische Rückkehr zu älteren Images. Bei einem Fehler werden Backend, Frontend und Proxy nach Möglichkeit angehalten. Das Datenbankschema oder das Speicherformat könnten bereits verändert sein. diff --git a/scripts/selfhost-update.py b/scripts/selfhost-update.py index 362b1601..a8e0e892 100644 --- a/scripts/selfhost-update.py +++ b/scripts/selfhost-update.py @@ -566,7 +566,13 @@ class Updater: if not self.managed and not accept_compose: raise UpdateError("Compose-Datei ist noch nicht verwaltet oder wurde bearbeitet. compose.diff prüfen, Anpassungen in die Override-Datei übertragen und --accept-compose-replacement verwenden.") self.save("pulling") - self.compose(True, "pull", timeout=3600) + # Exact digests are immutable: reuse local copies even when their registry is offline. + # Mutable infrastructure tags must still be refreshed before entering maintenance. + for policy in ("missing", "always"): + services = [name for name, service in self.candidate["services"].items() + if bool(re.search(r"@sha256:[a-f0-9]{64}$", service["image"])) == (policy == "missing")] + if services: + self.compose(True, "pull", "--policy", policy, *services, timeout=3600) self.check_space(self.backup_size, self.home) if any(sha(Path(path).read_bytes()) != value for path, value in self.before.items()): raise UpdateError("Konfiguration wurde während der Vorbereitung verändert; erneut prüfen.") @@ -590,13 +596,13 @@ class Updater: atomic(self.root / ".env.example", (self.job / ".env.example").read_bytes()) atomic(self.root / "scripts/selfhost-setup.sh", (self.job / "selfhost-setup.sh").read_bytes()) os.chmod(self.root / "scripts/selfhost-setup.sh", 0o700) - self.compose(True, "up", "-d", "--no-deps", "--wait", "--wait-timeout", "180", "db", "minio", "node-exporter") - self.compose(True, "run", "--rm", "--no-deps", "createbuckets") + self.compose(True, "up", "--pull", "never", "-d", "--no-deps", "--wait", "--wait-timeout", "180", "db", "minio", "node-exporter") + self.compose(True, "run", "--pull", "never", "--rm", "--no-deps", "createbuckets") self.save("migrating", migrationContainer="fedeo-update-" + self.job.name) - self.compose(True, "run", "--rm", "--no-deps", "--name", self.state["migrationContainer"], + self.compose(True, "run", "--pull", "never", "--rm", "--no-deps", "--name", self.state["migrationContainer"], "-e", "FEDEO_RUN_MIGRATIONS=false", "--entrypoint", "npm", "backend", "run", "migrate") self.save("starting") - self.compose(True, "up", "-d", "--wait", "--wait-timeout", "600", timeout=900) + self.compose(True, "up", "--pull", "never", "-d", "--wait", "--wait-timeout", "600", timeout=900) self.save("verifying") self.verify_matrix_stopped(retired=True) for name, image in self.images.items(): diff --git a/scripts/update/tests/test_updater.py b/scripts/update/tests/test_updater.py index 9dc5d270..2718de7b 100644 --- a/scripts/update/tests/test_updater.py +++ b/scripts/update/tests/test_updater.py @@ -79,6 +79,32 @@ class Runner: self.matrix_containers = {} self.on_stop = None self.real_tar = False + self.compose_configs = {} + self.local_images = set() + self.unavailable_images = set() + self.downloads = [] + + @staticmethod + def image_reference(image): + # Docker resolves tag@digest using the repository and digest, not the tag. + if '@' not in image: + return image + name, digest = image.split('@', 1) + repository, slash, basename = name.rpartition('/') + return repository + slash + basename.split(':', 1)[0] + '@' + digest + + def pull_image(self, image, policy): + reference = self.image_reference(image) + if policy == 'never': + if reference not in self.local_images: + raise u.UpdateError('Image missing locally') + return + if policy == 'missing' and reference in self.local_images: + return + self.downloads.append(reference) + if reference in self.unavailable_images: + raise u.UpdateError('Registry unavailable for ' + reference) + self.local_images.add(reference) def run(self, args, output=None, input_file=None, **kwargs): self.calls.append(args) @@ -108,6 +134,7 @@ class Runner: capture_output=True, text=True) if result.returncode: raise u.UpdateError('Compose invalid: ' + result.stderr) + self.compose_configs[args[args.index('-f') + 1]] = json.loads(result.stdout) return result.stdout if 'ps' in args: if args[:2] == ['docker', 'ps']: @@ -121,8 +148,17 @@ class Runner: container['State']['Running'] = 'start' in args if 'stop' in args and self.on_stop: self.on_stop() - if 'pull' in args and self.on_pull: - self.on_pull() + if args[:2] == ['docker', 'compose'] and any(command in args for command in ('pull', 'up', 'run')): + services = self.compose_configs[args[args.index('-f') + 1]]['services'] + selected = [service for name, service in services.items() if name in args] + if not selected: + selected = list(services.values()) + for service in selected: + policy = (args[args.index('--policy') + 1] if '--policy' in args else 'always') if 'pull' in args else ( + args[args.index('--pull') + 1] if '--pull' in args else service.get('pull_policy', 'missing')) + self.pull_image(service['image'], policy) + if 'pull' in args and self.on_pull: + self.on_pull() if any('pg_database_size' in part for part in args): return '1000' if args[0] == 'du': @@ -493,6 +529,49 @@ class UpdateTests(unittest.TestCase): self.run_update() self.assert_untouched() + def test_cached_exact_images_work_with_unavailable_registries_and_forced_pull_overrides(self): + self.updater.execute('check', '3.1.0') + services = self.updater.candidate['services'] + refs = {name: self.runner.image_reference(service['image']) for name, service in services.items()} + self.runner.local_images.update(refs.values()) + self.runner.unavailable_images.update(ref for ref in refs.values() if '@sha256:' in ref) + (self.root / 'docker-compose.override.yml').write_text( + 'services:\n' + ''.join(f' {name}:\n pull_policy: always\n' for name in services)) + result = self.run_update() + self.assertEqual(result['status'], 'succeeded') + self.assertEqual(set(self.runner.downloads), {refs[name] for name in ('db', 'traefik', 'node-exporter')}) + self.assertEqual(len(self.runner.downloads), 3) + + def test_missing_digest_is_downloaded_even_if_tag_or_another_digest_is_cached(self): + self.updater.execute('check', '3.1.0') + services = self.updater.candidate['services'] + for service in services.values(): + image = service['image'] + if '@' in image: + self.runner.local_images.add(image.split('@')[0]) + self.runner.local_images.add(self.runner.image_reference(image).split('@')[0] + '@sha256:' + '0' * 64) + self.assertEqual(self.run_update()['status'], 'succeeded') + self.assertEqual(set(self.runner.downloads), {self.runner.image_reference(s['image']) for s in services.values()}) + + def test_missing_unavailable_digest_stops_before_maintenance_despite_cached_tag(self): + self.updater.execute('check', '3.1.0') + image = self.updater.candidate['services']['minio']['image'] + self.runner.local_images.add(image.split('@')[0]) + self.runner.unavailable_images.add(self.runner.image_reference(image)) + with self.assertRaisesRegex(u.UpdateError, 'Registry unavailable'): + self.run_update() + self.assert_untouched() + self.assertFalse(self.updater.status()['recoveryRequired']) + + def test_cached_mutable_tag_still_requires_a_successful_registry_refresh(self): + self.updater.execute('check', '3.1.0') + services = self.updater.candidate['services'] + self.runner.local_images.update(self.runner.image_reference(s['image']) for s in services.values()) + self.runner.unavailable_images.add(services['db']['image']) + with self.assertRaisesRegex(u.UpdateError, 'Registry unavailable'): + self.run_update() + self.assert_untouched() + def test_configuration_changed_during_pull_is_detected(self): self.runner.on_pull = lambda: (self.root / '.env').write_bytes(self.original_env + b'\nNEW=value\n') with self.assertRaisesRegex(u.UpdateError, 'während'): -- 2.49.1 From eaa5627062a46c9ede0793d862fd2b1f6007b1a9 Mon Sep 17 00:00:00 2001 From: flfeders Date: Fri, 25 Sep 2026 11:14:09 +0200 Subject: [PATCH 5/5] fix(documents): initialize new documents as drafts --- frontend/pages/createDocument/edit/[[id]].vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/pages/createDocument/edit/[[id]].vue b/frontend/pages/createDocument/edit/[[id]].vue index a9c20f31..44534237 100644 --- a/frontend/pages/createDocument/edit/[[id]].vue +++ b/frontend/pages/createDocument/edit/[[id]].vue @@ -35,6 +35,7 @@ const guard = ref(true) const itemInfo = ref({ type: "invoices", + state: "Entwurf", taxType: "Standard", customer: null, contact: null, @@ -2094,7 +2095,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {