UNPKG

matrix-react-sdk

Version:
579 lines (544 loc) 71.3 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.IndexedDBLogStore = exports.ConsoleLogger = void 0; exports.cleanup = cleanup; exports.flush = flush; exports.getLogsForReport = getLogsForReport; exports.init = init; exports.tryInitStorage = tryInitStorage; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _logger = require("matrix-js-sdk/src/logger"); var _randomstring = require("matrix-js-sdk/src/randomstring"); var _JSON = require("../utils/JSON"); /* Copyright 2024 New Vector Ltd. Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2018 New Vector Ltd Copyright 2017 OpenMarket Ltd SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ // This module contains all the code needed to log the console, persist it to // disk and submit bug reports. Rationale is as follows: // - Monkey-patching the console is preferable to having a log library because // we can catch logs by other libraries more easily, without having to all // depend on the same log framework / pass the logger around. // - We use IndexedDB to persists logs because it has generous disk space // limits compared to local storage. IndexedDB does not work in incognito // mode, in which case this module will not be able to write logs to disk. // However, the logs will still be stored in-memory, so can still be // submitted in a bug report should the user wish to: we can also store more // logs in-memory than in local storage, which does work in incognito mode. // We also need to handle the case where there are 2+ tabs. Each JS runtime // generates a random string which serves as the "ID" for that tab/session. // These IDs are stored along with the log lines. // - Bug reports are sent as a POST over HTTPS: it purposefully does not use // Matrix as bug reports may be made when Matrix is not responsive (which may // be the cause of the bug). We send the most recent N MB of UTF-8 log data, // starting with the most recent, which we know because the "ID"s are // actually timestamps. We then purge the remaining logs. We also do this // purge on startup to prevent logs from accumulating. // the frequency with which we flush to indexeddb const FLUSH_RATE_MS = 30 * 1000; /** the length of log data we keep in indexeddb (and include in the reports), if there are more than 24 hours of logs */ const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB /** the length of log data we keep in indexeddb (and include in the reports), if there are less than 24 hours of logs */ const MAX_LOG_SIZE_24H = 1024 * 1024 * 100; // 100 MB const consoleFunctionsToLevels = { log: "I", info: "I", warn: "W", error: "E", debug: "D" }; // A class which monkey-patches the global console and stores log lines. class ConsoleLogger { constructor() { (0, _defineProperty2.default)(this, "logs", ""); (0, _defineProperty2.default)(this, "originalFunctions", {}); } monkeyPatch(consoleObj) { // Monkey-patch console logging Object.keys(consoleFunctionsToLevels).forEach(fnName => { const level = consoleFunctionsToLevels[fnName]; const originalFn = consoleObj[fnName].bind(consoleObj); this.originalFunctions[fnName] = originalFn; consoleObj[fnName] = (...args) => { this.log(level, ...args); originalFn(...args); }; }); } bypassRageshake(fnName, ...args) { this.originalFunctions[fnName]?.(...args); } log(level, ...args) { // We don't know what locale the user may be running so use ISO strings const ts = new Date().toISOString(); // Convert objects and errors to helpful things args = args.map(arg => { if (arg instanceof DOMException) { return arg.message + ` (${arg.name} | ${arg.code})`; } else if (arg instanceof Error) { return arg.message + (arg.stack ? `\n${arg.stack}` : ""); } else if (typeof arg === "object") { return JSON.stringify(arg, (0, _JSON.getCircularReplacer)()); } else { return arg; } }); // Some browsers support string formatting which we're not doing here // so the lines are a little more ugly but easy to implement / quick to // run. // Example line: // 2017-01-18T11:23:53.214Z W Failed to set badge count let line = `${ts} ${level} ${args.join(" ")}\n`; // Do some cleanup line = line.replace(/token=[a-zA-Z0-9-]+/gm, "token=xxxxx"); // Using + really is the quickest way in JS // http://jsperf.com/concat-vs-plus-vs-join this.logs += line; } /** * Retrieve log lines to flush to disk. * @param {boolean} keepLogs True to not delete logs after flushing. * @return {string} \n delimited log lines to flush. */ flush(keepLogs) { // The ConsoleLogger doesn't care how these end up on disk, it just // flushes them to the caller. if (keepLogs) { return this.logs; } const logsToFlush = this.logs; this.logs = ""; return logsToFlush; } } // A class which stores log lines in an IndexedDB instance. exports.ConsoleLogger = ConsoleLogger; class IndexedDBLogStore { constructor(indexedDB, logger) { (0, _defineProperty2.default)(this, "id", void 0); (0, _defineProperty2.default)(this, "index", 0); (0, _defineProperty2.default)(this, "db", null); (0, _defineProperty2.default)(this, "flushPromise", null); (0, _defineProperty2.default)(this, "flushAgainPromise", null); this.indexedDB = indexedDB; this.logger = logger; this.id = "instance-" + (0, _randomstring.randomString)(16); } /** * @return {Promise} Resolves when the store is ready. */ connect() { const req = this.indexedDB.open("logs"); return new Promise((resolve, reject) => { req.onsuccess = () => { this.db = req.result; // Periodically flush logs to local storage / indexeddb window.setInterval(this.flush.bind(this), FLUSH_RATE_MS); resolve(); }; req.onerror = () => { const err = "Failed to open log database: " + req.error?.name; _logger.logger.error(err); reject(new Error(err)); }; // First time: Setup the object store req.onupgradeneeded = () => { const db = req.result; const logObjStore = db.createObjectStore("logs", { keyPath: ["id", "index"] }); // Keys in the database look like: [ "instance-148938490", 0 ] // Later on we need to query everything based on an instance id. // In order to do this, we need to set up indexes "id". logObjStore.createIndex("id", "id", { unique: false }); logObjStore.add(this.generateLogEntry(new Date() + " ::: Log database was created.")); const lastModifiedStore = db.createObjectStore("logslastmod", { keyPath: "id" }); lastModifiedStore.add(this.generateLastModifiedTime()); }; }); } /** * Flush logs to disk. * * There are guards to protect against race conditions in order to ensure * that all previous flushes have completed before the most recent flush. * Consider without guards: * - A calls flush() periodically. * - B calls flush() and wants to send logs immediately afterwards. * - If B doesn't wait for A's flush to complete, B will be missing the * contents of A's flush. * To protect against this, we set 'flushPromise' when a flush is ongoing. * Subsequent calls to flush() during this period will chain another flush, * then keep returning that same chained flush. * * This guarantees that we will always eventually do a flush when flush() is * called. * * @return {Promise} Resolved when the logs have been flushed. */ flush() { // check if a flush() operation is ongoing if (this.flushPromise) { if (this.flushAgainPromise) { // this is the 3rd+ time we've called flush() : return the same promise. return this.flushAgainPromise; } // queue up a flush to occur immediately after the pending one completes. this.flushAgainPromise = this.flushPromise.then(() => { return this.flush(); }).then(() => { this.flushAgainPromise = null; }); return this.flushAgainPromise; } // there is no flush promise or there was but it has finished, so do // a brand new one, destroying the chain which may have been built up. this.flushPromise = new Promise((resolve, reject) => { if (!this.db) { // not connected yet or user rejected access for us to r/w to the db. reject(new Error("No connected database")); return; } const lines = this.logger.flush(); if (lines.length === 0) { resolve(); return; } const txn = this.db.transaction(["logs", "logslastmod"], "readwrite"); const objStore = txn.objectStore("logs"); txn.oncomplete = event => { resolve(); }; txn.onerror = () => { _logger.logger.error("Failed to flush logs : ", txn.error); reject(new Error("Failed to write logs: " + txn.error?.message)); }; objStore.add(this.generateLogEntry(lines)); const lastModStore = txn.objectStore("logslastmod"); lastModStore.put(this.generateLastModifiedTime()); }).then(() => { this.flushPromise = null; }); return this.flushPromise; } /** * Consume the most recent logs and return them. Older logs which are not * returned are deleted at the same time, so this can be called at startup * to do house-keeping to keep the logs from growing too large. * * @return {Promise<Object[]>} Resolves to an array of objects. The array is * sorted in time (oldest first) based on when the log file was created (the * log ID). The objects have said log ID in an "id" field and "lines" which * is a big string with all the new-line delimited logs. */ async consume() { const allLogIds = await this.fetchLogIds(); let removeLogIds = []; const logs = []; let size = 0; for (let i = 0; i < allLogIds.length; i++) { const instanceId = allLogIds[i]; const { lines, truncated } = await this.fetchLogs(instanceId, size); // always add the returned logs: fetchLogs will truncate once it hits the size limit, // so we'll go over the max but only by one fragment's worth. logs.push({ lines, id: instanceId }); size += lines.length; // If fetchLogs truncated we'll now be at or over the size limit, // in which case we should stop and remove the rest of the log files. if (truncated) { _logger.logger.log(`rageshake: reached size limit while processing instance ${i + 1}/${allLogIds.length} (${instanceId}), with ${size} bytes of logs: will drop further instances`); // the remaining log IDs should be removed. If we go out of // bounds this is just [] removeLogIds = allLogIds.slice(i + 1); break; } } if (removeLogIds.length > 0) { _logger.logger.log(`rageshake: removing logs: ${removeLogIds}`); // Don't await this because it's non-fatal if we can't clean up // logs. Promise.all(removeLogIds.map(id => this.deleteLogs(id))).then(() => { _logger.logger.log(`Removed ${removeLogIds.length} old logs.`); }, err => { _logger.logger.error(err); }); } return logs; } /** * Fetch all the application instance names from the database. */ fetchLogIds() { const db = this.db; if (!db) return Promise.reject("DB unavailable"); // To gather all the log IDs, query for all records in logslastmod. const o = db.transaction("logslastmod", "readonly").objectStore("logslastmod"); return selectQuery(o, undefined, cursor => { return { id: cursor.value.id, ts: cursor.value.ts }; }).then(res => { // Sort IDs by timestamp (newest first) return res.sort((a, b) => { return b.ts - a.ts; }).map(a => a.id); }); } /** * Fetch logs for a given application instance from the database, stopping once we hit the size limit. * * @param id - Application instance to fetch logs for. * @param sizeSoFar - Amount of logs we have already retrieved from other instances. * * @returns An object with the properties: * * `lines`: the concatenated logs for this ID * * `truncated`: whether the output was truncated due to hitting the size limit. */ fetchLogs(id, sizeSoFar) { const db = this.db; if (!db) return Promise.reject("DB unavailable"); const objectStore = db.transaction("logs", "readonly").objectStore("logs"); /** Determine whether we should stop collecting logs after this batch. * * @param sizeSoFar - The total amount of logs collected so far. * @param logBatchTimestamp - The timestamp of the most recent batch of logs collected. * * @returns `true` if we should stop after this batch. */ function shouldTruncateAfterLogBatch(sizeSoFar, logBatchTimestamp) { // First check if we have exceeded the absolute limit if (sizeSoFar >= MAX_LOG_SIZE_24H) { return true; } // Otherwise, check if the most recent batch is more than 24H old, and we have exceeded the limit for logs over 24H if (Date.now() - logBatchTimestamp >= 24 * 3600 * 1000 && sizeSoFar >= MAX_LOG_SIZE) { return true; } // Otherwise, we're good. return false; } return new Promise((resolve, reject) => { const query = objectStore.index("id").openCursor(IDBKeyRange.only(id), "prev"); let lines = ""; query.onerror = () => { reject(new Error("Query failed: " + query.error?.message)); }; query.onsuccess = () => { const cursor = query.result; if (!cursor) { // end of results resolve({ lines, truncated: false }); return; } const newLines = cursor.value.lines; // The query returns log chunks in reverse time order, so prepend this new chunk to the buffer. lines = newLines + lines; sizeSoFar += newLines.length; // If we have now exceeded the size limit, stop. if (shouldTruncateAfterLogBatch(sizeSoFar, cursor.value.ts ?? 0)) { resolve({ lines, truncated: true }); } else { cursor.continue(); } }; }); } /** * Delete logs for a given application instance. * * @param id - Application instance to delete logs for. */ deleteLogs(id) { const db = this.db; if (!db) return Promise.reject("DB unavailable"); return new Promise((resolve, reject) => { const txn = db.transaction(["logs", "logslastmod"], "readwrite"); const o = txn.objectStore("logs"); // only load the key path, not the data which may be huge const query = o.index("id").openKeyCursor(IDBKeyRange.only(id)); query.onsuccess = () => { const cursor = query.result; if (!cursor) { return; } o.delete(cursor.primaryKey); cursor.continue(); }; txn.oncomplete = () => { resolve(); }; txn.onerror = () => { reject(new Error("Failed to delete logs for " + `'${id}' : ${query.error?.message}`)); }; // delete last modified entries const lastModStore = txn.objectStore("logslastmod"); lastModStore.delete(id); }); } /** Generate the object to be stored in the `logs` store */ generateLogEntry(lines) { return { id: this.id, lines: lines, index: this.index++, /** The timestamp at which the line was *flushed* (not necessarily when it was written). */ ts: Date.now() }; } generateLastModifiedTime() { return { id: this.id, ts: Date.now() }; } } /** * Helper method to collect results from a Cursor and promiseify it. * @param {ObjectStore|Index} store The store to perform openCursor on. * @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor. * @param {Function} resultMapper A function which is repeatedly called with a * Cursor. * Return the data you want to keep. * @return {Promise<T[]>} Resolves to an array of whatever you returned from * resultMapper. */ exports.IndexedDBLogStore = IndexedDBLogStore; function selectQuery(store, keyRange, resultMapper) { const query = store.openCursor(keyRange); return new Promise((resolve, reject) => { const results = []; query.onerror = () => { reject(new Error("Query failed: " + query.error?.message)); }; // collect results query.onsuccess = () => { const cursor = query.result; if (!cursor) { resolve(results); return; // end of results } results.push(resultMapper(cursor)); cursor.continue(); }; }); } /** * Configure rage shaking support for sending bug reports. * Modifies globals. * @param {boolean} setUpPersistence When true (default), the persistence will * be set up immediately for the logs. * @return {Promise} Resolves when set up. */ function init(setUpPersistence = true) { if (global.mx_rage_initPromise) { return global.mx_rage_initPromise; } global.mx_rage_logger = new ConsoleLogger(); global.mx_rage_logger.monkeyPatch(window.console); // log unhandled rejections in the rageshake window.addEventListener("unhandledrejection", event => { global.mx_rage_logger.log("error", `Unhandled promise rejection: ${event.reason}`); }); if (setUpPersistence) { return tryInitStorage(); } global.mx_rage_initPromise = Promise.resolve(); return global.mx_rage_initPromise; } /** * Try to start up the rageshake storage for logs. If not possible (client unsupported) * then this no-ops. * @return {Promise} Resolves when complete. */ function tryInitStorage() { if (global.mx_rage_initStoragePromise) { return global.mx_rage_initStoragePromise; } _logger.logger.log("Configuring rageshake persistence..."); // just *accessing* indexedDB throws an exception in firefox with // indexeddb disabled. let indexedDB; try { indexedDB = window.indexedDB; } catch (e) {} if (indexedDB) { global.mx_rage_store = new IndexedDBLogStore(indexedDB, global.mx_rage_logger); global.mx_rage_initStoragePromise = global.mx_rage_store.connect(); // Fire off a task in the background which will clean up old logs in the store global.mx_rage_initStoragePromise.then(() => { global.mx_rage_store.consume().catch(e => { _logger.logger.error("Error cleaning up rageshake store", e); }); }); return global.mx_rage_initStoragePromise; } global.mx_rage_initStoragePromise = Promise.resolve(); return global.mx_rage_initStoragePromise; } function flush() { if (!global.mx_rage_store) { return; } global.mx_rage_store.flush(); } /** * Clean up old logs. * * @deprecated There is no need to call this explicitly: it will be done as a side-effect of {@link tryInitStorage}, * or {@link init} with `setUpPersistence: true`. * * @return {Promise} Resolves if cleaned logs. */ async function cleanup() { if (!global.mx_rage_store) { return; } await global.mx_rage_store.consume(); } /** * Get a recent snapshot of the logs, ready for attaching to a bug report * * @return {Array<{lines: string, id, string}>} list of log data */ async function getLogsForReport() { if (!global.mx_rage_logger) { throw new Error("No console logger, did you forget to call init()?"); } // If in incognito mode, store is null, but we still want bug report // sending to work going off the in-memory console logs. if (global.mx_rage_store) { // flush most recent logs await global.mx_rage_store.flush(); return global.mx_rage_store.consume(); } else { return [{ lines: global.mx_rage_logger.flush(true), id: "-" }]; } } //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJfbG9nZ2VyIiwicmVxdWlyZSIsIl9yYW5kb21zdHJpbmciLCJfSlNPTiIsIkZMVVNIX1JBVEVfTVMiLCJNQVhfTE9HX1NJWkUiLCJNQVhfTE9HX1NJWkVfMjRIIiwiY29uc29sZUZ1bmN0aW9uc1RvTGV2ZWxzIiwibG9nIiwiaW5mbyIsIndhcm4iLCJlcnJvciIsImRlYnVnIiwiQ29uc29sZUxvZ2dlciIsImNvbnN0cnVjdG9yIiwiX2RlZmluZVByb3BlcnR5MiIsImRlZmF1bHQiLCJtb25rZXlQYXRjaCIsImNvbnNvbGVPYmoiLCJPYmplY3QiLCJrZXlzIiwiZm9yRWFjaCIsImZuTmFtZSIsImxldmVsIiwib3JpZ2luYWxGbiIsImJpbmQiLCJvcmlnaW5hbEZ1bmN0aW9ucyIsImFyZ3MiLCJieXBhc3NSYWdlc2hha2UiLCJ0cyIsIkRhdGUiLCJ0b0lTT1N0cmluZyIsIm1hcCIsImFyZyIsIkRPTUV4Y2VwdGlvbiIsIm1lc3NhZ2UiLCJuYW1lIiwiY29kZSIsIkVycm9yIiwic3RhY2siLCJKU09OIiwic3RyaW5naWZ5IiwiZ2V0Q2lyY3VsYXJSZXBsYWNlciIsImxpbmUiLCJqb2luIiwicmVwbGFjZSIsImxvZ3MiLCJmbHVzaCIsImtlZXBMb2dzIiwibG9nc1RvRmx1c2giLCJleHBvcnRzIiwiSW5kZXhlZERCTG9nU3RvcmUiLCJpbmRleGVkREIiLCJsb2dnZXIiLCJpZCIsInJhbmRvbVN0cmluZyIsImNvbm5lY3QiLCJyZXEiLCJvcGVuIiwiUHJvbWlzZSIsInJlc29sdmUiLCJyZWplY3QiLCJvbnN1Y2Nlc3MiLCJkYiIsInJlc3VsdCIsIndpbmRvdyIsInNldEludGVydmFsIiwib25lcnJvciIsImVyciIsIm9udXBncmFkZW5lZWRlZCIsImxvZ09ialN0b3JlIiwiY3JlYXRlT2JqZWN0U3RvcmUiLCJrZXlQYXRoIiwiY3JlYXRlSW5kZXgiLCJ1bmlxdWUiLCJhZGQiLCJnZW5lcmF0ZUxvZ0VudHJ5IiwibGFzdE1vZGlmaWVkU3RvcmUiLCJnZW5lcmF0ZUxhc3RNb2RpZmllZFRpbWUiLCJmbHVzaFByb21pc2UiLCJmbHVzaEFnYWluUHJvbWlzZSIsInRoZW4iLCJsaW5lcyIsImxlbmd0aCIsInR4biIsInRyYW5zYWN0aW9uIiwib2JqU3RvcmUiLCJvYmplY3RTdG9yZSIsIm9uY29tcGxldGUiLCJldmVudCIsImxhc3RNb2RTdG9yZSIsInB1dCIsImNvbnN1bWUiLCJhbGxMb2dJZHMiLCJmZXRjaExvZ0lkcyIsInJlbW92ZUxvZ0lkcyIsInNpemUiLCJpIiwiaW5zdGFuY2VJZCIsInRydW5jYXRlZCIsImZldGNoTG9ncyIsInB1c2giLCJzbGljZSIsImFsbCIsImRlbGV0ZUxvZ3MiLCJvIiwic2VsZWN0UXVlcnkiLCJ1bmRlZmluZWQiLCJjdXJzb3IiLCJ2YWx1ZSIsInJlcyIsInNvcnQiLCJhIiwiYiIsInNpemVTb0ZhciIsInNob3VsZFRydW5jYXRlQWZ0ZXJMb2dCYXRjaCIsImxvZ0JhdGNoVGltZXN0YW1wIiwibm93IiwicXVlcnkiLCJpbmRleCIsIm9wZW5DdXJzb3IiLCJJREJLZXlSYW5nZSIsIm9ubHkiLCJuZXdMaW5lcyIsImNvbnRpbnVlIiwib3BlbktleUN1cnNvciIsImRlbGV0ZSIsInByaW1hcnlLZXkiLCJzdG9yZSIsImtleVJhbmdlIiwicmVzdWx0TWFwcGVyIiwicmVzdWx0cyIsImluaXQiLCJzZXRVcFBlcnNpc3RlbmNlIiwiZ2xvYmFsIiwibXhfcmFnZV9pbml0UHJvbWlzZSIsIm14X3JhZ2VfbG9nZ2VyIiwiY29uc29sZSIsImFkZEV2ZW50TGlzdGVuZXIiLCJyZWFzb24iLCJ0cnlJbml0U3RvcmFnZSIsIm14X3JhZ2VfaW5pdFN0b3JhZ2VQcm9taXNlIiwiZSIsIm14X3JhZ2Vfc3RvcmUiLCJjYXRjaCIsImNsZWFudXAiLCJnZXRMb2dzRm9yUmVwb3J0Il0sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3JhZ2VzaGFrZS9yYWdlc2hha2UudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLypcbkNvcHlyaWdodCAyMDI0IE5ldyBWZWN0b3IgTHRkLlxuQ29weXJpZ2h0IDIwMTkgVGhlIE1hdHJpeC5vcmcgRm91bmRhdGlvbiBDLkkuQy5cbkNvcHlyaWdodCAyMDE4IE5ldyBWZWN0b3IgTHRkXG5Db3B5cmlnaHQgMjAxNyBPcGVuTWFya2V0IEx0ZFxuXG5TUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQUdQTC0zLjAtb25seSBPUiBHUEwtMy4wLW9ubHlcblBsZWFzZSBzZWUgTElDRU5TRSBmaWxlcyBpbiB0aGUgcmVwb3NpdG9yeSByb290IGZvciBmdWxsIGRldGFpbHMuXG4qL1xuXG4vLyBUaGlzIG1vZHVsZSBjb250YWlucyBhbGwgdGhlIGNvZGUgbmVlZGVkIHRvIGxvZyB0aGUgY29uc29sZSwgcGVyc2lzdCBpdCB0b1xuLy8gZGlzayBhbmQgc3VibWl0IGJ1ZyByZXBvcnRzLiBSYXRpb25hbGUgaXMgYXMgZm9sbG93czpcbi8vICAtIE1vbmtleS1wYXRjaGluZyB0aGUgY29uc29sZSBpcyBwcmVmZXJhYmxlIHRvIGhhdmluZyBhIGxvZyBsaWJyYXJ5IGJlY2F1c2Vcbi8vICAgIHdlIGNhbiBjYXRjaCBsb2dzIGJ5IG90aGVyIGxpYnJhcmllcyBtb3JlIGVhc2lseSwgd2l0aG91dCBoYXZpbmcgdG8gYWxsXG4vLyAgICBkZXBlbmQgb24gdGhlIHNhbWUgbG9nIGZyYW1ld29yayAvIHBhc3MgdGhlIGxvZ2dlciBhcm91bmQuXG4vLyAgLSBXZSB1c2UgSW5kZXhlZERCIHRvIHBlcnNpc3RzIGxvZ3MgYmVjYXVzZSBpdCBoYXMgZ2VuZXJvdXMgZGlzayBzcGFjZVxuLy8gICAgbGltaXRzIGNvbXBhcmVkIHRvIGxvY2FsIHN0b3JhZ2UuIEluZGV4ZWREQiBkb2VzIG5vdCB3b3JrIGluIGluY29nbml0b1xuLy8gICAgbW9kZSwgaW4gd2hpY2ggY2FzZSB0aGlzIG1vZHVsZSB3aWxsIG5vdCBiZSBhYmxlIHRvIHdyaXRlIGxvZ3MgdG8gZGlzay5cbi8vICAgIEhvd2V2ZXIsIHRoZSBsb2dzIHdpbGwgc3RpbGwgYmUgc3RvcmVkIGluLW1lbW9yeSwgc28gY2FuIHN0aWxsIGJlXG4vLyAgICBzdWJtaXR0ZWQgaW4gYSBidWcgcmVwb3J0IHNob3VsZCB0aGUgdXNlciB3aXNoIHRvOiB3ZSBjYW4gYWxzbyBzdG9yZSBtb3JlXG4vLyAgICBsb2dzIGluLW1lbW9yeSB0aGFuIGluIGxvY2FsIHN0b3JhZ2UsIHdoaWNoIGRvZXMgd29yayBpbiBpbmNvZ25pdG8gbW9kZS5cbi8vICAgIFdlIGFsc28gbmVlZCB0byBoYW5kbGUgdGhlIGNhc2Ugd2hlcmUgdGhlcmUgYXJlIDIrIHRhYnMuIEVhY2ggSlMgcnVudGltZVxuLy8gICAgZ2VuZXJhdGVzIGEgcmFuZG9tIHN0cmluZyB3aGljaCBzZXJ2ZXMgYXMgdGhlIFwiSURcIiBmb3IgdGhhdCB0YWIvc2Vzc2lvbi5cbi8vICAgIFRoZXNlIElEcyBhcmUgc3RvcmVkIGFsb25nIHdpdGggdGhlIGxvZyBsaW5lcy5cbi8vICAtIEJ1ZyByZXBvcnRzIGFyZSBzZW50IGFzIGEgUE9TVCBvdmVyIEhUVFBTOiBpdCBwdXJwb3NlZnVsbHkgZG9lcyBub3QgdXNlXG4vLyAgICBNYXRyaXggYXMgYnVnIHJlcG9ydHMgbWF5IGJlIG1hZGUgd2hlbiBNYXRyaXggaXMgbm90IHJlc3BvbnNpdmUgKHdoaWNoIG1heVxuLy8gICAgYmUgdGhlIGNhdXNlIG9mIHRoZSBidWcpLiBXZSBzZW5kIHRoZSBtb3N0IHJlY2VudCBOIE1CIG9mIFVURi04IGxvZyBkYXRhLFxuLy8gICAgc3RhcnRpbmcgd2l0aCB0aGUgbW9zdCByZWNlbnQsIHdoaWNoIHdlIGtub3cgYmVjYXVzZSB0aGUgXCJJRFwicyBhcmVcbi8vICAgIGFjdHVhbGx5IHRpbWVzdGFtcHMuIFdlIHRoZW4gcHVyZ2UgdGhlIHJlbWFpbmluZyBsb2dzLiBXZSBhbHNvIGRvIHRoaXNcbi8vICAgIHB1cmdlIG9uIHN0YXJ0dXAgdG8gcHJldmVudCBsb2dzIGZyb20gYWNjdW11bGF0aW5nLlxuXG4vLyB0aGUgZnJlcXVlbmN5IHdpdGggd2hpY2ggd2UgZmx1c2ggdG8gaW5kZXhlZGRiXG5pbXBvcnQgeyBsb2dnZXIgfSBmcm9tIFwibWF0cml4LWpzLXNkay9zcmMvbG9nZ2VyXCI7XG5pbXBvcnQgeyByYW5kb21TdHJpbmcgfSBmcm9tIFwibWF0cml4LWpzLXNkay9zcmMvcmFuZG9tc3RyaW5nXCI7XG5cbmltcG9ydCB7IGdldENpcmN1bGFyUmVwbGFjZXIgfSBmcm9tIFwiLi4vdXRpbHMvSlNPTlwiO1xuXG5jb25zdCBGTFVTSF9SQVRFX01TID0gMzAgKiAxMDAwO1xuXG4vKiogdGhlIGxlbmd0aCBvZiBsb2cgZGF0YSB3ZSBrZWVwIGluIGluZGV4ZWRkYiAoYW5kIGluY2x1ZGUgaW4gdGhlIHJlcG9ydHMpLCBpZiB0aGVyZSBhcmUgbW9yZSB0aGFuIDI0IGhvdXJzIG9mIGxvZ3MgKi9cbmNvbnN0IE1BWF9MT0dfU0laRSA9IDEwMjQgKiAxMDI0ICogNTsgLy8gNSBNQlxuXG4vKiogdGhlIGxlbmd0aCBvZiBsb2cgZGF0YSB3ZSBrZWVwIGluIGluZGV4ZWRkYiAoYW5kIGluY2x1ZGUgaW4gdGhlIHJlcG9ydHMpLCBpZiB0aGVyZSBhcmUgbGVzcyB0aGFuIDI0IGhvdXJzIG9mIGxvZ3MgKi9cbmNvbnN0IE1BWF9MT0dfU0laRV8yNEggPSAxMDI0ICogMTAyNCAqIDEwMDsgLy8gMTAwIE1CXG5cbnR5cGUgTG9nRnVuY3Rpb24gPSAoLi4uYXJnczogKEVycm9yIHwgRE9NRXhjZXB0aW9uIHwgb2JqZWN0IHwgc3RyaW5nKVtdKSA9PiB2b2lkO1xuY29uc3QgY29uc29sZUZ1bmN0aW9uc1RvTGV2ZWxzID0ge1xuICAgIGxvZzogXCJJXCIsXG4gICAgaW5mbzogXCJJXCIsXG4gICAgd2FybjogXCJXXCIsXG4gICAgZXJyb3I6IFwiRVwiLFxuICAgIGRlYnVnOiBcIkRcIixcbn0gYXMgY29uc3Q7XG50eXBlIExvZ0Z1bmN0aW9uTmFtZSA9IGtleW9mIHR5cGVvZiBjb25zb2xlRnVuY3Rpb25zVG9MZXZlbHM7XG5cbi8vIEEgY2xhc3Mgd2hpY2ggbW9ua2V5LXBhdGNoZXMgdGhlIGdsb2JhbCBjb25zb2xlIGFuZCBzdG9yZXMgbG9nIGxpbmVzLlxuZXhwb3J0IGNsYXNzIENvbnNvbGVMb2dnZXIge1xuICAgIHByaXZhdGUgbG9ncyA9IFwiXCI7XG4gICAgcHJpdmF0ZSBvcmlnaW5hbEZ1bmN0aW9uczogeyBba2V5IGluIExvZ0Z1bmN0aW9uTmFtZV0/OiBMb2dGdW5jdGlvbiB9ID0ge307XG5cbiAgICBwdWJsaWMgbW9ua2V5UGF0Y2goY29uc29sZU9iajogQ29uc29sZSk6IHZvaWQge1xuICAgICAgICAvLyBNb25rZXktcGF0Y2ggY29uc29sZSBsb2dnaW5nXG4gICAgICAgIChPYmplY3Qua2V5cyhjb25zb2xlRnVuY3Rpb25zVG9MZXZlbHMpIGFzIExvZ0Z1bmN0aW9uTmFtZVtdKS5mb3JFYWNoKChmbk5hbWU6IExvZ0Z1bmN0aW9uTmFtZSkgPT4ge1xuICAgICAgICAgICAgY29uc3QgbGV2ZWwgPSBjb25zb2xlRnVuY3Rpb25zVG9MZXZlbHNbZm5OYW1lXTtcbiAgICAgICAgICAgIGNvbnN0IG9yaWdpbmFsRm4gPSBjb25zb2xlT2JqW2ZuTmFtZV0uYmluZChjb25zb2xlT2JqKTtcbiAgICAgICAgICAgIHRoaXMub3JpZ2luYWxGdW5jdGlvbnNbZm5OYW1lXSA9IG9yaWdpbmFsRm47XG4gICAgICAgICAgICBjb25zb2xlT2JqW2ZuTmFtZV0gPSAoLi4uYXJncykgPT4ge1xuICAgICAgICAgICAgICAgIHRoaXMubG9nKGxldmVsLCAuLi5hcmdzKTtcbiAgICAgICAgICAgICAgICBvcmlnaW5hbEZuKC4uLmFyZ3MpO1xuICAgICAgICAgICAgfTtcbiAgICAgICAgfSk7XG4gICAgfVxuXG4gICAgcHVibGljIGJ5cGFzc1JhZ2VzaGFrZShmbk5hbWU6IExvZ0Z1bmN0aW9uTmFtZSwgLi4uYXJnczogKEVycm9yIHwgRE9NRXhjZXB0aW9uIHwgb2JqZWN0IHwgc3RyaW5nKVtdKTogdm9pZCB7XG4gICAgICAgIHRoaXMub3JpZ2luYWxGdW5jdGlvbnNbZm5OYW1lXT8uKC4uLmFyZ3MpO1xuICAgIH1cblxuICAgIHB1YmxpYyBsb2cobGV2ZWw6IHN0cmluZywgLi4uYXJnczogKEVycm9yIHwgRE9NRXhjZXB0aW9uIHwgb2JqZWN0IHwgc3RyaW5nKVtdKTogdm9pZCB7XG4gICAgICAgIC8vIFdlIGRvbid0IGtub3cgd2hhdCBsb2NhbGUgdGhlIHVzZXIgbWF5IGJlIHJ1bm5pbmcgc28gdXNlIElTTyBzdHJpbmdzXG4gICAgICAgIGNvbnN0IHRzID0gbmV3IERhdGUoKS50b0lTT1N0cmluZygpO1xuXG4gICAgICAgIC8vIENvbnZlcnQgb2JqZWN0cyBhbmQgZXJyb3JzIHRvIGhlbHBmdWwgdGhpbmdzXG4gICAgICAgIGFyZ3MgPSBhcmdzLm1hcCgoYXJnKSA9PiB7XG4gICAgICAgICAgICBpZiAoYXJnIGluc3RhbmNlb2YgRE9NRXhjZXB0aW9uKSB7XG4gICAgICAgICAgICAgICAgcmV0dXJuIGFyZy5tZXNzYWdlICsgYCAoJHthcmcubmFtZX0gfCAke2FyZy5jb2RlfSlgO1xuICAgICAgICAgICAgfSBlbHNlIGlmIChhcmcgaW5zdGFuY2VvZiBFcnJvcikge1xuICAgICAgICAgICAgICAgIHJldHVybiBhcmcubWVzc2FnZSArIChhcmcuc3RhY2sgPyBgXFxuJHthcmcuc3RhY2t9YCA6IFwiXCIpO1xuICAgICAgICAgICAgfSBlbHNlIGlmICh0eXBlb2YgYXJnID09PSBcIm9iamVjdFwiKSB7XG4gICAgICAgICAgICAgICAgcmV0dXJuIEpTT04uc3RyaW5naWZ5KGFyZywgZ2V0Q2lyY3VsYXJSZXBsYWNlcigpKTtcbiAgICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICAgICAgcmV0dXJuIGFyZztcbiAgICAgICAgICAgIH1cbiAgICAgICAgfSk7XG5cbiAgICAgICAgLy8gU29tZSBicm93c2VycyBzdXBwb3J0IHN0cmluZyBmb3JtYXR0aW5nIHdoaWNoIHdlJ3JlIG5vdCBkb2luZyBoZXJlXG4gICAgICAgIC8vIHNvIHRoZSBsaW5lcyBhcmUgYSBsaXR0bGUgbW9yZSB1Z2x5IGJ1dCBlYXN5IHRvIGltcGxlbWVudCAvIHF1aWNrIHRvXG4gICAgICAgIC8vIHJ1bi5cbiAgICAgICAgLy8gRXhhbXBsZSBsaW5lOlxuICAgICAgICAvLyAyMDE3LTAxLTE4VDExOjIzOjUzLjIxNFogVyBGYWlsZWQgdG8gc2V0IGJhZGdlIGNvdW50XG4gICAgICAgIGxldCBsaW5lID0gYCR7dHN9ICR7bGV2ZWx9ICR7YXJncy5qb2luKFwiIFwiKX1cXG5gO1xuICAgICAgICAvLyBEbyBzb21lIGNsZWFudXBcbiAgICAgICAgbGluZSA9IGxpbmUucmVwbGFjZSgvdG9rZW49W2EtekEtWjAtOS1dKy9nbSwgXCJ0b2tlbj14eHh4eFwiKTtcbiAgICAgICAgLy8gVXNpbmcgKyByZWFsbHkgaXMgdGhlIHF1aWNrZXN0IHdheSBpbiBKU1xuICAgICAgICAvLyBodHRwOi8vanNwZXJmLmNvbS9jb25jYXQtdnMtcGx1cy12cy1qb2luXG4gICAgICAgIHRoaXMubG9ncyArPSBsaW5lO1xuICAgIH1cblxuICAgIC8qKlxuICAgICAqIFJldHJpZXZlIGxvZyBsaW5lcyB0byBmbHVzaCB0byBkaXNrLlxuICAgICAqIEBwYXJhbSB7Ym9vbGVhbn0ga2VlcExvZ3MgVHJ1ZSB0byBub3QgZGVsZXRlIGxvZ3MgYWZ0ZXIgZmx1c2hpbmcuXG4gICAgICogQHJldHVybiB7c3RyaW5nfSBcXG4gZGVsaW1pdGVkIGxvZyBsaW5lcyB0byBmbHVzaC5cbiAgICAgKi9cbiAgICBwdWJsaWMgZmx1c2goa2VlcExvZ3M/OiBib29sZWFuKTogc3RyaW5nIHtcbiAgICAgICAgLy8gVGhlIENvbnNvbGVMb2dnZXIgZG9lc24ndCBjYXJlIGhvdyB0aGVzZSBlbmQgdXAgb24gZGlzaywgaXQganVzdFxuICAgICAgICAvLyBmbHVzaGVzIHRoZW0gdG8gdGhlIGNhbGxlci5cbiAgICAgICAgaWYgKGtlZXBMb2dzKSB7XG4gICAgICAgICAgICByZXR1cm4gdGhpcy5sb2dzO1xuICAgICAgICB9XG4gICAgICAgIGNvbnN0IGxvZ3NUb0ZsdXNoID0gdGhpcy5sb2dzO1xuICAgICAgICB0aGlzLmxvZ3MgPSBcIlwiO1xuICAgICAgICByZXR1cm4gbG9nc1RvRmx1c2g7XG4gICAgfVxufVxuXG4vLyBBIGNsYXNzIHdoaWNoIHN0b3JlcyBsb2cgbGluZXMgaW4gYW4gSW5kZXhlZERCIGluc3RhbmNlLlxuZXhwb3J0IGNsYXNzIEluZGV4ZWREQkxvZ1N0b3JlIHtcbiAgICBwcml2YXRlIGlkOiBzdHJpbmc7XG4gICAgcHJpdmF0ZSBpbmRleCA9IDA7XG4gICAgcHJpdmF0ZSBkYjogSURCRGF0YWJhc2UgfCBudWxsID0gbnVsbDtcbiAgICBwcml2YXRlIGZsdXNoUHJvbWlzZTogUHJvbWlzZTx2b2lkPiB8IG51bGwgPSBudWxsO1xuICAgIHByaXZhdGUgZmx1c2hBZ2FpblByb21pc2U6IFByb21pc2U8dm9pZD4gfCBudWxsID0gbnVsbDtcblxuICAgIHB1YmxpYyBjb25zdHJ1Y3RvcihcbiAgICAgICAgcHJpdmF0ZSBpbmRleGVkREI6IElEQkZhY3RvcnksXG4gICAgICAgIHByaXZhdGUgbG9nZ2VyOiBDb25zb2xlTG9nZ2VyLFxuICAgICkge1xuICAgICAgICB0aGlzLmlkID0gXCJpbnN0YW5jZS1cIiArIHJhbmRvbVN0cmluZygxNik7XG4gICAgfVxuXG4gICAgLyoqXG4gICAgICogQHJldHVybiB7UHJvbWlzZX0gUmVzb2x2ZXMgd2hlbiB0aGUgc3RvcmUgaXMgcmVhZHkuXG4gICAgICovXG4gICAgcHVibGljIGNvbm5lY3QoKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgICAgIGNvbnN0IHJlcSA9IHRoaXMuaW5kZXhlZERCLm9wZW4oXCJsb2dzXCIpO1xuICAgICAgICByZXR1cm4gbmV3IFByb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuICAgICAgICAgICAgcmVxLm9uc3VjY2VzcyA9ICgpID0+IHtcbiAgICAgICAgICAgICAgICB0aGlzLmRiID0gcmVxLnJlc3VsdDtcbiAgICAgICAgICAgICAgICAvLyBQZXJpb2RpY2FsbHkgZmx1c2ggbG9ncyB0byBsb2NhbCBzdG9yYWdlIC8gaW5kZXhlZGRiXG4gICAgICAgICAgICAgICAgd2luZG93LnNldEludGVydmFsKHRoaXMuZmx1c2guYmluZCh0aGlzKSwgRkxVU0hfUkFURV9NUyk7XG4gICAgICAgICAgICAgICAgcmVzb2x2ZSgpO1xuICAgICAgICAgICAgfTtcblxuICAgICAgICAgICAgcmVxLm9uZXJyb3IgPSAoKSA9PiB7XG4gICAgICAgICAgICAgICAgY29uc3QgZXJyID0gXCJGYWlsZWQgdG8gb3BlbiBsb2cgZGF0YWJhc2U6IFwiICsgcmVxLmVycm9yPy5uYW1lO1xuICAgICAgICAgICAgICAgIGxvZ2dlci5lcnJvcihlcnIpO1xuICAgICAgICAgICAgICAgIHJlamVjdChuZXcgRXJyb3IoZXJyKSk7XG4gICAgICAgICAgICB9O1xuXG4gICAgICAgICAgICAvLyBGaXJzdCB0aW1lOiBTZXR1cCB0aGUgb2JqZWN0IHN0b3JlXG4gICAgICAgICAgICByZXEub251cGdyYWRlbmVlZGVkID0gKCkgPT4ge1xuICAgICAgICAgICAgICAgIGNvbnN0IGRiID0gcmVxLnJlc3VsdDtcbiAgICAgICAgICAgICAgICBjb25zdCBsb2dPYmpTdG9yZSA9IGRiLmNyZWF0ZU9iamVjdFN0b3JlKFwibG9nc1wiLCB7XG4gICAgICAgICAgICAgICAgICAgIGtleVBhdGg6IFtcImlkXCIsIFwiaW5kZXhcIl0sXG4gICAgICAgICAgICAgICAgfSk7XG4gICAgICAgICAgICAgICAgLy8gS2V5cyBpbiB0aGUgZGF0YWJhc2UgbG9vayBsaWtlOiBbIFwiaW5zdGFuY2UtMTQ4OTM4NDkwXCIsIDAgXVxuICAgICAgICAgICAgICAgIC8vIExhdGVyIG9uIHdlIG5lZWQgdG8gcXVlcnkgZXZlcnl0aGluZyBiYXNlZCBvbiBhbiBpbnN0YW5jZSBpZC5cbiAgICAgICAgICAgICAgICAvLyBJbiBvcmRlciB0byBkbyB0aGlzLCB3ZSBuZWVkIHRvIHNldCB1cCBpbmRleGVzIFwiaWRcIi5cbiAgICAgICAgICAgICAgICBsb2dPYmpTdG9yZS5jcmVhdGVJbmRleChcImlkXCIsIFwiaWRcIiwgeyB1bmlxdWU6IGZhbHNlIH0pO1xuXG4gICAgICAgICAgICAgICAgbG9nT2JqU3RvcmUuYWRkKHRoaXMuZ2VuZXJhdGVMb2dFbnRyeShuZXcgRGF0ZSgpICsgXCIgOjo6IExvZyBkYXRhYmFzZSB3YXMgY3JlYXRlZC5cIikpO1xuXG4gICAgICAgICAgICAgICAgY29uc3QgbGFzdE1vZGlmaWVkU3RvcmUgPSBkYi5jcmVhdGVPYmplY3RTdG9yZShcImxvZ3NsYXN0bW9kXCIsIHtcbiAgICAgICAgICAgICAgICAgICAga2V5UGF0aDogXCJpZFwiLFxuICAgICAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgICAgIGxhc3RNb2RpZmllZFN0b3JlLmFkZCh0aGlzLmdlbmVyYXRlTGFzdE1vZGlmaWVkVGltZSgpKTtcbiAgICAgICAgICAgIH07XG4gICAgICAgIH0pO1xuICAgIH1cblxuICAgIC8qKlxuICAgICAqIEZsdXNoIGxvZ3MgdG8gZGlzay5cbiAgICAgKlxuICAgICAqIFRoZXJlIGFyZSBndWFyZHMgdG8gcHJvdGVjdCBhZ2FpbnN0IHJhY2UgY29uZGl0aW9ucyBpbiBvcmRlciB0byBlbnN1cmVcbiAgICAgKiB0aGF0IGFsbCBwcmV2aW91cyBmbHVzaGVzIGhhdmUgY29tcGxldGVkIGJlZm9yZSB0aGUgbW9zdCByZWNlbnQgZmx1c2guXG4gICAgICogQ29uc2lkZXIgd2l0aG91dCBndWFyZHM6XG4gICAgICogIC0gQSBjYWxscyBmbHVzaCgpIHBlcmlvZGljYWxseS5cbiAgICAgKiAgLSBCIGNhbGxzIGZsdXNoKCkgYW5kIHdhbnRzIHRvIHNlbmQgbG9ncyBpbW1lZGlhdGVseSBhZnRlcndhcmRzLlxuICAgICAqICAtIElmIEIgZG9lc24ndCB3YWl0IGZvciBBJ3MgZmx1c2ggdG8gY29tcGxldGUsIEIgd2lsbCBiZSBtaXNzaW5nIHRoZVxuICAgICAqICAgIGNvbnRlbnRzIG9mIEEncyBmbHVzaC5cbiAgICAgKiBUbyBwcm90ZWN0IGFnYWluc3QgdGhpcywgd2Ugc2V0ICdmbHVzaFByb21pc2UnIHdoZW4gYSBmbHVzaCBpcyBvbmdvaW5nLlxuICAgICAqIFN1YnNlcXVlbnQgY2FsbHMgdG8gZmx1c2goKSBkdXJpbmcgdGhpcyBwZXJpb2Qgd2lsbCBjaGFpbiBhbm90aGVyIGZsdXNoLFxuICAgICAqIHRoZW4ga2VlcCByZXR1cm5pbmcgdGhhdCBzYW1lIGNoYWluZWQgZmx1c2guXG4gICAgICpcbiAgICAgKiBUaGlzIGd1YXJhbnRlZXMgdGhhdCB3ZSB3aWxsIGFsd2F5cyBldmVudHVhbGx5IGRvIGEgZmx1c2ggd2hlbiBmbHVzaCgpIGlzXG4gICAgICogY2FsbGVkLlxuICAgICAqXG4gICAgICogQHJldHVybiB7UHJvbWlzZX0gUmVzb2x2ZWQgd2hlbiB0aGUgbG9ncyBoYXZlIGJlZW4gZmx1c2hlZC5cbiAgICAgKi9cbiAgICBwdWJsaWMgZmx1c2goKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgICAgIC8vIGNoZWNrIGlmIGEgZmx1c2goKSBvcGVyYXRpb24gaXMgb25nb2luZ1xuICAgICAgICBpZiAodGhpcy5mbHVzaFByb21pc2UpIHtcbiAgICAgICAgICAgIGlmICh0aGlzLmZsdXNoQWdhaW5Qcm9taXNlKSB7XG4gICAgICAgICAgICAgICAgLy8gdGhpcyBpcyB0aGUgM3JkKyB0aW1lIHdlJ3ZlIGNhbGxlZCBmbHVzaCgpIDogcmV0dXJuIHRoZSBzYW1lIHByb21pc2UuXG4gICAgICAgICAgICAgICAgcmV0dXJuIHRoaXMuZmx1c2hBZ2FpblByb21pc2U7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICAvLyBxdWV1ZSB1cCBhIGZsdXNoIHRvIG9jY3VyIGltbWVkaWF0ZWx5IGFmdGVyIHRoZSBwZW5kaW5nIG9uZSBjb21wbGV0ZXMuXG4gICAgICAgICAgICB0aGlzLmZsdXNoQWdhaW5Qcm9taXNlID0gdGhpcy5mbHVzaFByb21pc2VcbiAgICAgICAgICAgICAgICAudGhlbigoKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgIHJldHVybiB0aGlzLmZsdXNoKCk7XG4gICAgICAgICAgICAgICAgfSlcbiAgICAgICAgICAgICAgICAudGhlbigoKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgIHRoaXMuZmx1c2hBZ2FpblByb21pc2UgPSBudWxsO1xuICAgICAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgcmV0dXJuIHRoaXMuZmx1c2hBZ2FpblByb21pc2U7XG4gICAgICAgIH1cbiAgICAgICAgLy8gdGhlcmUgaXMgbm8gZmx1c2ggcHJvbWlzZSBvciB0aGVyZSB3YXMgYnV0IGl0IGhhcyBmaW5pc2hlZCwgc28gZG9cbiAgICAgICAgLy8gYSBicmFuZCBuZXcgb25lLCBkZXN0cm95aW5nIHRoZSBjaGFpbiB3aGljaCBtYXkgaGF2ZSBiZWVuIGJ1aWx0IHVwLlxuICAgICAgICB0aGlzLmZsdXNoUHJvbWlzZSA9IG5ldyBQcm9taXNlPHZvaWQ+KChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgICAgIGlmICghdGhpcy5kYikge1xuICAgICAgICAgICAgICAgIC8vIG5vdCBjb25uZWN0ZWQgeWV0IG9yIHVzZXIgcmVqZWN0ZWQgYWNjZXNzIGZvciB1cyB0byByL3cgdG8gdGhlIGRiLlxuICAgICAgICAgICAgICAgIHJlamVjdChuZXcgRXJyb3IoXCJObyBjb25uZWN0ZWQgZGF0YWJhc2VcIikpO1xuICAgICAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGNvbnN0IGxpbmVzID0gdGhpcy5sb2dnZXIuZmx1c2goKTtcbiAgICAgICAgICAgIGlmIChsaW5lcy5sZW5ndGggPT09IDApIHtcbiAgICAgICAgICAgICAgICByZXNvbHZlKCk7XG4gICAgICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgY29uc3QgdHhuID0gdGhpcy5kYi50cmFuc2FjdGlvbihbXCJsb2dzXCIsIFwibG9nc2xhc3Rtb2RcIl0sIFwicmVhZHdyaXRlXCIpO1xuICAgICAgICAgICAgY29uc3Qgb2JqU3RvcmUgPSB0eG4ub2JqZWN0U3RvcmUoXCJsb2dzXCIpO1xuICAgICAgICAgICAgdHhuLm9uY29tcGxldGUgPSAoZXZlbnQpID0+IHtcbiAgICAgICAgICAgICAgICByZXNvbHZlKCk7XG4gICAgICAgICAgICB9O1xuICAgICAgICAgICAgdHhuLm9uZXJyb3IgPSAoKSA9PiB7XG4gICAgICAgICAgICAgICAgbG9nZ2VyLmVycm9yKFwiRmFpbGVkIHRvIGZsdXNoIGxvZ3MgOiBcIiwgdHhuLmVycm9yKTtcbiAgICAgICAgICAgICAgICByZWplY3QobmV3IEVycm9yKFwiRmFpbGVkIHRvIHdyaXRlIGxvZ3M6IFwiICsgdHhuLmVycm9yPy5tZXNzYWdlKSk7XG4gICAgICAgICAgICB9O1xuICAgICAgICAgICAgb2JqU3RvcmUuYWRkKHRoaXMuZ2VuZXJhdGVMb2dFbnRyeShsaW5lcykpO1xuICAgICAgICAgICAgY29uc3QgbGFzdE1vZFN0b3JlID0gdHhuLm9iamVjdFN0b3JlKFwibG9nc2xhc3Rtb2RcIik7XG4gICAgICAgICAgICBsYXN0TW9kU3RvcmUucHV0KHRoaXMuZ2VuZXJhdGVMYXN0TW9kaWZpZWRUaW1lKCkpO1xuICAgICAgICB9KS50aGVuKCgpID0+IHtcbiAgICAgICAgICAgIHRoaXMuZmx1c2hQcm9taXNlID0gbnVsbDtcbiAgICAgICAgfSk7XG4gICAgICAgIHJldHVybiB0aGlzLmZsdXNoUHJvbWlzZTtcbiAgICB9XG5cbiAgICAvKipcbiAgICAgKiBDb25zdW1lIHRoZSBtb3N0IHJlY2VudCBsb2dzIGFuZCByZXR1cm4gdGhlbS4gT2xkZXIgbG9ncyB3aGljaCBhcmUgbm90XG4gICAgICogcmV0dXJuZWQgYXJlIGRlbGV0ZWQgYXQgdGhlIHNhbWUgdGltZSwgc28gdGhpcyBjYW4gYmUgY2FsbGVkIGF0IHN0YXJ0dXBcbiAgICAgKiB0byBkbyBob3VzZS1rZWVwaW5nIHRvIGtlZXAgdGhlIGxvZ3MgZnJvbSBncm93aW5nIHRvbyBsYXJnZS5cbiAgICAgKlxuICAgICAqIEByZXR1cm4ge1Byb21pc2U8T2JqZWN0W10+fSBSZXNvbHZlcyB0byBhbiBhcnJheSBvZiBvYmplY3RzLiBUaGUgYXJyYXkgaXNcbiAgICAgKiBzb3J0ZWQgaW4gdGltZSAob2xkZXN0IGZpcnN0KSBiYXNlZCBvbiB3aGVuIHRoZSBsb2cgZmlsZSB3YXMgY3JlYXRlZCAodGhlXG4gICAgICogbG9nIElEKS4gVGhlIG9iamVjdHMgaGF2ZSBzYWlkIGxvZyBJRCBpbiBhbiBcImlkXCIgZmllbGQgYW5kIFwibGluZXNcIiB3aGljaFxuICAgICAqIGlzIGEgYmlnIHN0cmluZyB3aXRoIGFsbCB0aGUgbmV3LWxpbmUgZGVsaW1pdGVkIGxvZ3MuXG4gICAgICovXG4gICAgcHVibGljIGFzeW5jIGNvbnN1bWUoKTogUHJvbWlzZTx7IGxpbmVzOiBzdHJpbmc7IGlkOiBzdHJpbmcgfVtdPiB7XG4gICAgICAgIGNvbnN0IGFsbExvZ0lkcyA9IGF3YWl0IHRoaXMuZmV0Y2hMb2dJZHMoKTtcbiAgICAgICAgbGV0IHJlbW92ZUxvZ0lkczogc3RyaW5nW10gPSBbXTtcbiAgICAgICAgY29uc3QgbG9nczogeyBsaW5lczogc3RyaW5nOyBpZDogc3RyaW5nIH1bXSA9IFtdO1xuICAgICAgICBsZXQgc2l6ZSA9IDA7XG4gICAgICAgIGZvciAobGV0IGkgPSAwOyBpIDwgYWxsTG9nSWRzLmxlbmd0aDsgaSsrKSB7XG4gICAgICAgICAgICBjb25zdCBpbnN0YW5jZUlkID0gYWxsTG9nSWRzW2ldO1xuICAgICAgICAgICAgY29uc3QgeyBsaW5lcywgdHJ1bmNhdGVkIH0gPSBhd2FpdCB0aGlzLmZldGNoTG9ncyhpbnN0YW5jZUlkLCBzaXplKTtcblxuICAgICAgICAgICAgLy8gYWx3YXlzIGFkZCB0aGUgcmV0dXJuZWQgbG9nczogZmV0Y2hMb2dzIHdpbGwgdHJ1bmNhdGUgb25jZSBpdCBoaXRzIHRoZSBzaXplIGxpbWl0LFxuICAgICAgICAgICAgLy8gc28gd2UnbGwgZ28gb3ZlciB0aGUgbWF4IGJ1dCBvbmx5IGJ5IG9uZSBmcmFnbWVudCdzIHdvcnRoLlxuICAgICAgICAgICAgbG9ncy5wdXNoKHsgbGluZXMsIGlkOiBpbnN0YW5jZUlkIH0pO1xuICAgICAgICAgICAgc2l6ZSArPSBsaW5lcy5sZW5ndGg7XG5cbiAgICAgICAgICAgIC8vIElmIGZldGNoTG9ncyB0cnVuY2F0ZWQgd2UnbGwgbm93IGJlIGF0IG9yIG92ZXIgdGhlIHNpemUgbGltaXQsXG4gICAgICAgICAgICAvLyBpbiB3aGljaCBjYXNlIHdlIHNob3VsZCBzdG9wIGFuZCByZW1vdmUgdGhlIHJlc3Qgb2YgdGhlIGxvZyBmaWxlcy5cbiAgICAgICAgICAgIGlmICh0cnVuY2F0ZWQpIHtcbiAgICAgICAgICAgICAgICBsb2dnZXIubG9nKFxuICAgICAgICAgICAgICAgICAgICBgcmFnZXNoYWtlOiByZWFjaGVkIHNpemUgbGltaXQgd2hpbGUgcHJvY2Vzc2luZyBpbnN0YW5jZSAke2kgKyAxfS8ke1xuICAgICAgICAgICAgICAgICAgICAgICAgYWxsTG9nSWRzLmxlbmd0aFxuICAgICAgICAgICAgICAgICAgICB9ICgke2luc3RhbmNlSWR9KSwgd2l0aCAke3NpemV9IGJ5dGVzIG9mIGxvZ3M6IHdpbGwgZHJvcCBmdXJ0aGVyIGluc3RhbmNlc2AsXG4gICAgICAgICAgICAgICAgKTtcbiAgICAgICAgICAgICAgICAvLyB0aGUgcmVtYWluaW5nIGxvZyBJRHMgc2hvdWxkIGJlIHJlbW92ZWQuIElmIHdlIGdvIG91dCBvZlxuICAgICAgICAgICAgICAgIC8vIGJvdW5kcyB0aGlzIGlzIGp1c3QgW11cbiAgICAgICAgICAgICAgICByZW1vdmVMb2dJZHMgPSBhbGxMb2dJZHMuc2xpY2UoaSArIDEpO1xuICAgICAgICAgICAgICAgIGJyZWFrO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgIGlmIChyZW1vdmVMb2dJZHMubGVuZ3RoID4gMCkge1xuICAgICAgICAgICAgbG9nZ2VyLmxvZyhgcmFnZXNoYWtlOiByZW1vdmluZyBsb2dzOiAke3JlbW92ZUxvZ0lkc31gKTtcbiAgICAgICAgICAgIC8vIERvbid0IGF3YWl0IHRoaXMgYmVjYXVzZSBpdCdzIG5vbi1mYXRhbCBpZiB3ZSBjYW4ndCBjbGVhbiB1cFxuICAgICAgICAgICAgLy8gbG9ncy5cbiAgICAgICAgICAgIFByb21pc2UuYWxsKHJlbW92ZUxvZ0lkcy5tYXAoKGlkKSA9PiB0aGlzLmRlbGV0ZUxvZ3MoaWQpKSkudGhlbihcbiAgICAgICAgICAgICAgICAoKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgIGxvZ2dlci5sb2coYFJlbW92ZWQgJHtyZW1vdmVMb2dJZHMubGVuZ3RofSBvbGQgbG9ncy5gKTtcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIChlcnIpID0+IHtcbiAgICAgICAgICAgICAgICAgICAgbG9nZ2VyLmVycm9yKGVycik7XG4gICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIGxvZ3M7XG4gICAgfVxuXG4gICAgLyoqXG4gICAgICogRmV0Y2ggYWxsIHRoZSBhcHBsaWNhdGlvbiBpbnN0YW5jZSBuYW1lcyBmcm9tIHRoZSBkYXRhYmFzZS5cbiAgICAgKi9cbiAgICBwcml2YXRlIGZldGNoTG9nSWRzKCk6IFByb21pc2U8c3RyaW5nW10+IHtcbiAgICAgICAgY29uc3QgZGIgPSB0aGlzLmRiO1xuICAgICAgICBpZiAoIWRiKSByZXR1cm4gUHJvbWlzZS5yZWplY3QoXCJEQiB1bmF2YWlsYWJsZVwiKTtcblxuICAgICAgICAvLyBUbyBnYXRoZXIgYWxsIHRoZSBsb2cgSURzLCBxdWVyeSBmb3IgYWxsIHJlY29yZHMgaW4gbG9nc2xhc3Rtb2QuXG4gICAgICAgIGNvbnN0IG8gPSBkYi50cmFuc2FjdGlvbihcImxvZ3NsYXN0bW9kXCIsIFwicmVhZG9ubHlcIikub2JqZWN0U3RvcmUoXCJsb2dzbGFzdG1vZFwiKTtcbiAgICAgICAgcmV0dXJuIHNlbGVjdFF1ZXJ5KG8sIHVuZGVmaW5lZCwgKGN1cnNvcikgPT4ge1xuICAgICAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICAgICAgICBpZDogY3Vyc29yLnZhbHVlLmlkLFxuICAgICAgICAgICAgICAgIHRzOiBjdXJzb3IudmFsdWUudHMsXG4gICAgICAgICAgICB9O1xuICAgICAgICB9KS50aGVuKChyZXMpID0+IHtcbiAgICAgICAgICAgIC8vIFNvcnQgSURzIGJ5IHRpbWVzdGFtcCAobmV3ZXN0IGZpcnN0KVxuICAgICAgICAgICAgcmV0dXJuIHJlc1xuICAgICAgICAgICAgICAgIC5zb3J0KChhLCBiKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgIHJldHVybiBiLnRzIC0gYS50cztcbiAgICAgICAgICAgICAgICB9KVxuICAgICAgICAgICAgICAgIC5tYXAoKGEpID0+IGEuaWQpO1xuICAgICAgICB9KTtcbiAgICB9XG5cbiAgICAvKipcbiAgICAgKiBGZXRjaCBsb2dzIGZvciBhIGdpdmVuIGFwcGxpY2F0aW9uIGluc3RhbmNlIGZyb20gdGhlIGRhdGFiYXNlLCBzdG9wcGluZyBvbmNlIHdlIGhpdCB0aGUgc2l6ZSBsaW1pdC5cbiAgICAgKlxuICAgICAqIEBwYXJhbSBpZCAtIEFwcGxpY2F0aW9uIGluc3RhbmNlIHRvIGZldGNoIGxvZ3MgZm9yLlxuICAgICAqIEBwYXJhbSBzaXplU29GYXIgLSBBbW91bnQgb2YgbG9ncyB3ZSBoYXZlIGFscmVhZHkgcmV0cmlldmVkIGZyb20gb3RoZXIgaW5zdGFuY2VzLlxuICAgICAqXG4gICAgICogQHJldHVybnMgQW4gb2JqZWN0IHdpdGggdGhlIHByb3BlcnRpZXM6XG4gICAgICogICogYGxpbmVzYDogdGhlIGNvbmNhdGVuYXRlZCBsb2dzIGZvciB0aGlzIElEXG4gICAgICogICogYHRydW5jYXRlZGA6IHdoZXRoZXIgdGhlIG91dHB1dCB3YXMgdHJ1bmNhdGVkIGR1ZSB0byBoaXR0aW5nIHRoZSBzaXplIGxpbWl0LlxuICAgICAqL1xuICAgIHByaXZhdGUgZmV0Y2hMb2dzKFxuICAgICAgICBpZDogc3RyaW5nLFxuICAgICAgICBzaXplU29GYXI6IG51bWJlcixcbiAgICApOiBQcm9taXNlPHtcbiAgICAgICAgbGluZXM6IHN0cmluZztcbiAgICAgICAgdHJ1bmNhdGVkOiBib29sZWFuO1xuICAgIH0+IHtcbiAgICAgICAgY29uc3QgZGIgPSB0aGlzLmRiO1xuICAgICAgICBpZiAoIWRiKSByZXR1cm4gUHJvbWlzZS5yZWplY3QoXCJEQiB1bmF2YWlsYWJsZVwiKTtcblxuICAgICAgICBjb25zdCBvYmplY3RTdG9yZSA9IGRiLnRyYW5zYWN0aW9uKFwibG9nc1wiLCBcInJlYWRvbmx5XCIpLm9iamVjdFN0b3JlKFwibG9nc1wiKTtcblxuICAgICAgICAvKiogRGV0ZXJtaW5lIHdoZXRoZXIgd2Ugc2hvdWxkIHN0b3AgY29sbGVjdGluZyBsb2dzIGFmdGVyIHRoaXMgYmF0Y2guXG4gICAgICAgICAqXG4gICAgICAgICAqIEBwYXJhbSBzaXplU29GYXIgLSBUaGUgdG90YWwgYW1vdW50IG9mIGxvZ3MgY29sbGVjdGVkIHNvIGZhci5cbiAgICAgICAgICogQHBhcmFtIGxvZ0JhdGNoVGltZXN0YW1wIC0gVGhlIHRpbWVzdGFtcCBvZiB0aGUgbW9zdCByZWNlbnQgYmF0Y2ggb2YgbG9ncyBjb2xsZWN0ZWQuXG4gICAgICAgICAqXG4gICAgICAgICAqIEByZXR1cm5zIGB0cnVlYCBpZiB3ZSBzaG91bGQgc3RvcCBhZnRlciB0aGlzIGJhdGNoLlxuICAgICAgICAgKi9cbiAgICAgICAgZnVuY3Rpb24gc2hvdWxkVHJ1bmNhdGVBZnRlckxvZ0JhdGNoKHNpemVTb0ZhcjogbnVtYmVyLCBsb2dCYXRjaFRpbWVzdGFtcDogbnVtYmVyKTogYm9vbGVhbiB7XG4gICAgICAgICAgICAvLyBGaXJzdCBjaGVjayBpZiB3ZSBoYXZlIGV4Y2VlZGVkIHRoZSBhYnNvbHV0ZSBsaW1pdFxuICAgICAgICAgICAgaWYgKHNpemVTb0ZhciA+PSBNQVhfTE9HX1NJWkVfMjRIKSB7XG4gICAgICAgICAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgICAgICAgICB9XG5cbiAgICAgICAgICAgIC8vIE90aGVyd2lzZSwgY2hlY2sgaWYgdGhlIG1vc3QgcmVjZW50IGJhdGNoIGlzIG1vcmUgdGhhbiAyNEggb2xkLCBhbmQgd2UgaGF2ZSBleGNlZWRlZCB0aGUgbGltaXQgZm9yIGxvZ3Mgb3ZlciAyNEhcbiAgICAgICAgICAgIGlmIChEYXRlLm5vdygpIC0gbG9nQmF0Y2hUaW1lc3RhbXAgPj0gMjQgKiAzNjAwICogMTAwMCAmJiBzaXplU29GYXIgPj0gTUFYX0xPR19TSVpFKSB7XG4gICAgICAgICAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgICAgICAgICB9XG5cbiAgICAgICAgICAgIC8vIE90aGVyd2lzZSwgd2UncmUgZ29vZC5cbiAgICAgICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgICAgfVxuXG4gICAgICAgIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgICAgICAgICBjb25zdCBxdWVyeSA9IG9iamVjdFN0b3JlLmluZGV4KFwiaWRcIikub3BlbkN1cnNvcihJREJLZXlSYW5nZS5vbmx5KGlkKSwgXCJwcmV2XCIpO1xuICAgICAgICAgICAgbGV0IGxpbmVzID0gXCJcIjtcbiAgICAgICAgICAgIHF1ZXJ5Lm9uZXJyb3IgPSAoKSA9PiB7XG4gICAgICAgICAgICAgICAgcmVqZWN0KG5ldyBFcnJvcihcIlF1ZXJ5IGZhaWxlZDogXCIgKyBxdWVyeS5lcnJvcj8ubWVzc2FnZSkpO1xuICAgICAgICAgICAgfTtcbiAgICAgICAgICAgIHF1ZXJ5Lm9uc3VjY2VzcyA9ICgpID0+IHtcbiAgICAgICAgICAgICAgICBjb25zdCBjdXJzb3IgPSBxdWVyeS5yZXN1bHQ7XG4gICAgICAgICAgICAgICAgaWYgKCFjdXJzb3IpIHtcbiAgICAgICAgICAgICAgICAgICAgLy8gZW5kIG9mIHJlc3VsdHNcbiAgICAgICAgICAgICAgICAgICAgcmVzb2x2ZSh7IGxpbmVzLCB0cnVuY2F0ZWQ6IGZhbHNlIH0pO1xuICAgICAgICAgICAgICAgICAgICByZXR1cm47XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIGNvbnN0IG5ld0xpbmVzID0gY3Vyc29yLnZhbHVlLmxpbmVzO1xuICAgICAgICAgICAgICAgIC8vIFRoZSBxdWVyeSByZXR1cm5zIGxvZyBjaHVua3MgaW4gcmV2ZXJzZSB0aW1lIG9yZGVyLCBzbyBwcmVwZW5kIHRoaXMgbmV3IGNodW5rIHRvIHRoZSBidWZmZXIuXG4gICAgICAgICAgICAgICAgbGluZXMgPSBuZXdMaW5lcyArIGxpbmVzO1xuICAgICAgICAgICAgICAgIHNpemVTb0ZhciArPSBuZXdMaW5lcy5sZW5ndGg7XG5cbiAgICAgICAgICAgICAgICAvLyBJZiB3ZSBoYXZlIG5vdyBleGNlZWRlZCB0aGUgc2l6ZSBsaW1pdCwgc3RvcC5cbiAgICAgICAgICAgICAgICBpZiAoc2hvdWxkVHJ1bmNhdGVBZnRlckxvZ0JhdGNoKHNpemVTb0ZhciwgY3Vyc29yLnZhbHVlLnRzID8/IDApKSB7XG4gICAgICAgICAgICAgICAgICAgIHJlc29sdmUoeyBsaW5lcywgdHJ1bmNhdGVkOiB0cnVlIH0pO1xuICAgICAgICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICAgICAgICAgIGN1cnNvci5jb250aW51ZSgpO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH07XG4gICAgICAgIH0pO1xuICAgIH1cblxuICAgIC8qKlxuICAgICAqIERlbGV0ZSBsb2dzIGZvciBhIGdpdmVuIGFwcGxpY2F0aW9uIGluc3RhbmNlLlxuICAgICAqXG4gICAgICogQHBhcmFtIGlkIC0gQXBwbGljYXRpb24gaW5zdGFuY2UgdG8gZGVsZXRlIGxvZ3MgZm9yLlxuICAgICAqL1xuICAgIHByaXZhdGUgZGVsZXRlTG9ncyhpZDogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgICAgIGNvbnN0IGRiID0gdGhpcy5kYjtcbiAgICAgICAgaWYgKCFkYikgcmV0dXJuIFByb21pc2UucmVqZWN0KFwiREIgdW5hdmFpbGFibGVcIik7XG5cbiAgICAgICAgcmV0dXJuIG5ldyBQcm9taXNlPHZvaWQ+KChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgICAgIGNvbnN0IHR4biA9IGRiLnRyYW5zYWN0aW9uKFtcImxvZ3NcIiwgXCJsb2dzbGFzdG1vZFwiXSwgXCJyZWFkd3JpdGVcIik7XG4gICAgICAgICAgICBjb25zdCBvID0gdHhuLm9iamVjdFN0b3JlKFwibG9nc1wiKTtcbiAgICAgICAgICAgIC8vIG9ubHkgbG9hZCB0aGUga2V5IHBhdGgsIG5vdCB0aGUgZGF0YSB3aGljaCBtYXkgYmUgaHVnZVxuICAgICAgICAgICAgY29uc3QgcXVlcnkgPSBvLmluZGV4KFwiaWRcIikub3BlbktleUN1cnNvcihJREJLZXlSYW5nZS5vbmx5KGlkKSk7XG4gICAgICAgICAgICBxdWVyeS5vbnN1Y2Nlc3MgPSAoKSA9PiB7XG4gICAgICAgICAgICAgICAgY29uc3QgY3Vyc29yID0gcXVlcnkucmVzdWx0O1xuICAgICAgICAgICAgICAgIGlmICghY3Vyc29yKSB7XG4gICAgICAgICAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgby5kZWxldGUoY3Vyc29yLnByaW1hcnlLZXkpO1xuICAgICAgICAgICAgICAgIGN1cnNvci5jb250aW51ZSgpO1xuICAgICAgICAgICAgfTtcbiAgICAgICAgICAgIHR4bi5vbmNvbXBsZXRlID0gKCkgPT4ge1xuICAgICAgICAgICAgICAgIHJlc29sdmUoKTtcbiAgICAgICAgICAgIH07XG4gICAgICAgICAgICB0eG4ub25lcnJvciA9ICgpID0+IHtcbiAgICAgICAgICAgICAgICByZWplY3QobmV3IEVycm9yKFwiRmFpbGVkIHRvIGRlbGV0ZSBsb2dzIGZvciBcIiArIGAnJHtpZH0nIDogJHtxdWVyeS5lcnJvcj8ubWVzc2FnZX1gKSk7XG4gICAgICAgICAgICB9O1xuICAgICAgICAgICAgLy8gZGVsZXRlIGxhc3QgbW9kaWZpZWQgZW50cmllc1xuICAgICAgICAgICAgY29uc3QgbGFzdE1vZFN0b3JlID0gdHhuLm9iamVjdFN0b3JlKFwibG9nc2xhc3Rtb2RcIik7XG4gICAgICAgICAgICBsYXN0TW9kU3RvcmUuZGVsZXRlKGlkKTtcbiAgICAgICAgfSk7XG4gICAgfVxuXG4gICAgLyoqIEdlbmVyYXRlIHRoZSBvYmplY3QgdG8gYmUgc3RvcmVkIGluIHRoZSBgbG9nc2Agc3RvcmUgKi9cbiAgICBwcml2YXRlIGdlbmVyYXRlTG9nRW50cnkobGluZXM6IHN0cmluZyk6IHsgaWQ6IHN0cmluZzsgbGluZXM6IHN0cmluZzsgaW5kZXg6IG51bWJlcjsgdHM6IG51bWJlciB9IHtcbiAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICAgIGlkOiB0aGlzLmlkLFxuICAgICAgICAgICAgbGluZXM6IGxpbmVzLFxuICAgICAgICAgICAgaW5kZXg6IHRoaXMuaW5kZXgrKyxcbiAgICAgICAgICAgIC8qKiBUaGUgdGltZXN0YW1wIGF0IHdoaWNoIHRoZSBsaW5lIHdhcyAqZmx1c2hlZCogKG5vdCBuZWNlc3NhcmlseSB3aGVuIGl0IHdhcyB3cml0dGVuKS4gKi9cbiAgICAgICAgICAgIHRzOiBEYXRlLm5vdygpLFxuICAgICAgICB9O1xuICAgIH1cblxuICAgIHByaXZhdGUgZ2VuZXJhdGVMYXN0TW9kaWZpZWRUaW1lKCk6IHsgaWQ6IHN0cmluZzsgdHM6IG51bWJlciB9IHtcbiAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICAgIGlkOiB0aGlzLmlkLFxuICAgICAgICAgICAgdHM6IERhdGUubm93KCksXG4gICAgICAgIH07XG4gICAgfVxufVxuXG4vKipcbiAqIEhlbHBlciBtZXRob2QgdG8gY29sbGVjdCByZXN1bHRzIGZyb20gYSBDdXJzb3IgYW5kIHByb21pc2VpZnkgaXQuXG4gKiBAcGFyYW0ge09iamVjdFN0b3JlfEluZGV4fSBzdG9yZSBUaGUgc3RvcmUgdG8gcGVyZm9ybSBvcGVuQ3Vyc29yIG9uLlxuICogQHBhcmFtIHtJREJLZXlSYW5nZT19IGtleVJhbmdlIE9wdGlvbmFsIGtleSByYW5nZSB0byBhcHBseSBvbiB0aGUgY3Vyc29yLlxuICogQHBhcmFtIHtGdW5jdGlvbn0gcmVzdWx0TWFwcGVyIEEgZnVuY3Rpb24gd2hpY2ggaXMgcmVwZWF0ZWRseSBjYWxsZWQgd2l0aCBhXG4gKiBDdXJzb3IuXG4gKiBSZXR1cm4gdGhlIGRhdGEgeW91IHdhbnQgdG8ga2VlcC5cbiAqIEByZXR1cm4ge1Byb21pc2U8VFtdPn0gUmVzb2x2ZXMgdG8gYW4gYXJyYXkgb2Ygd2hhdGV2ZXIgeW91IHJldHVybmVkIGZyb21cbiAqIHJlc3VsdE1hcHBlci5cbiAqL1xuZnVuY3Rpb24gc2VsZWN0UXVlcnk8VD4oXG4gICAgc3RvcmU6IElEQkluZGV4IHwgSURCT2JqZWN0U3RvcmUsXG4gICAga2V5UmFuZ2U6IElEQktleVJhbmdlIHwgdW5kZWZpbmVkLFxuICAgIHJlc3VsdE1hcHBlcjogKGN1cnNvcjogSURCQ3Vyc29yV2l0aFZhbHVlKSA9PiBULFxuKTogUHJvbWlzZTxUW10+IHtcbiAgICBjb25zdCBxdWVyeSA9IHN0b3JlLm9wZW5DdXJzb3Ioa2V5UmFuZ2UpO1xuICAgIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgICAgIGNvbnN0IHJlc3VsdHM6IFRbXSA9IFtdO1xuICAgICAgICBxdWVyeS5vbmVycm9yID0gKCkgPT4ge1xuICAgICAgICAgICAgcmVqZWN0KG5ldyBFcnJvcihcIlF1ZXJ5IGZhaWxlZDogXCIgKyBxdWVyeS5lcnJvcj8ubWVzc2FnZSkpO1xuICAgICAgICB9O1xuICAgICAgICAvLyBjb2xsZWN0IHJlc3VsdHNcbiAgICAgICAgcXVlcnkub25zdWNjZXNzID0gKCkgPT4ge1xuICAgICAgICAgICAgY29uc3QgY3Vyc29yID0gcXVlcnkucmVzdWx0O1xuICAgICAgICAgICAgaWYgKCFjdXJzb3IpIHtcbiAgICAgICAgICAgICAgICByZXNvbHZlKHJlc3VsdHMpO1xuICAgICAgICAgICAgICAgIHJldHVybjsgLy8gZW5kIG9mIHJlc3VsdHNcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIHJlc3VsdHMucHVzaChyZXN1bHRNYXBwZXIoY3Vyc29yKSk7XG4gICAgICAgICAgICBjdXJzb3IuY29udGludWUoKTtcbiAgICAgICAgfTtcbiAgICB9KTtcbn1cblxuLyoqXG4gKiBDb25maWd1cmUgcmFnZSBzaGFraW5nIHN1cHBvcnQgZm9yIHNlbmRpbmcgYnVnIHJlcG9ydHMuXG4gKiBNb2RpZmllcyBnbG9iYWxzLlxuICogQHBhcmFtIHtib29sZWFufSBzZXRVcFBlcnNpc3RlbmNlIFdoZW4gdHJ1ZSAoZGVmYXVsdCksIHRoZSBwZXJzaXN0ZW5jZSB3aWxsXG4gKiBiZSBzZXQgdXAgaW1tZWRpYXRlbHkgZm9yIHRoZSBsb2dzLlxuICogQHJldHVybiB7UHJvbWlzZX0gUmVzb2x2ZXMgd2hlbiBzZXQgdXAuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBpbml0KHNldFVwUGVyc2lzdGVuY2UgPSB0cnVlKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgaWYgKGdsb2JhbC5teF9yYWdlX2luaXRQcm9taXNlKSB7XG4gICAgICAgIHJldHVybiBnbG9iYWwubXhfcmFnZV9pbml0UHJvbWlzZTtcbiAgICB9XG4gICAgZ2xvYmFsLm14X3JhZ2VfbG9nZ2VyID0gbmV3IENvbnNvbG