UNPKG

matrix-react-sdk

Version:
262 lines (240 loc) 36.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SESSION_LOCK_CONSTANTS = void 0; exports.checkSessionLockFree = checkSessionLockFree; exports.getSessionLock = getSessionLock; var _logger = require("matrix-js-sdk/src/logger"); var _uuid = require("uuid"); /* Copyright 2024 New Vector Ltd. Copyright 2023 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ /* * Functionality for checking that only one instance is running at once * * The algorithm here is twofold. * * First, we "claim" a lock by periodically writing to `STORAGE_ITEM_PING`. On shutdown, we clear that item. So, * a new instance starting up can check if the lock is free by inspecting `STORAGE_ITEM_PING`. If it is unset, * or is stale, the new instance can assume the lock is free and claim it for itself. Otherwise, the new instance * has to wait for the ping to be stale, or the item to be cleared. * * Secondly, we need a mechanism for proactively telling existing instances to shut down. We do this by writing a * unique value to `STORAGE_ITEM_CLAIMANT`. Other instances of the app are supposed to monitor for writes to * `STORAGE_ITEM_CLAIMANT` and initiate shutdown when it happens. * * There is slight complexity in `STORAGE_ITEM_CLAIMANT` in that we need to watch out for yet another instance * starting up and staking a claim before we even get a chance to take the lock. When that happens we just bail out * and let the newer instance get the lock. * * `STORAGE_ITEM_OWNER` has no functional role in the lock mechanism; it exists solely as a diagnostic indicator * of which instance is writing to `STORAGE_ITEM_PING`. */ const SESSION_LOCK_CONSTANTS = exports.SESSION_LOCK_CONSTANTS = { /** * LocalStorage key for an item which indicates we have the lock. * * The instance which holds the lock writes the current time to this key every few seconds, to indicate it is still * alive and holds the lock. */ STORAGE_ITEM_PING: "react_sdk_session_lock_ping", /** * LocalStorage key for an item which holds the unique "session ID" of the instance which currently holds the lock. * * This property doesn't actually form a functional part of the locking algorithm; it is purely diagnostic. */ STORAGE_ITEM_OWNER: "react_sdk_session_lock_owner", /** * LocalStorage key for the session ID of the most recent claimant to the lock. * * Each instance writes to this key on startup, so existing instances can detect new ones starting up. */ STORAGE_ITEM_CLAIMANT: "react_sdk_session_lock_claimant", /** * The number of milliseconds after which we consider a lock claim stale */ LOCK_EXPIRY_TIME_MS: 30000 }; /** * See if any instances are currently running * * @returns true if any instance is currently active */ function checkSessionLockFree() { const prefixedLogger = _logger.logger.getChild(`checkSessionLockFree`); const lastPingTime = window.localStorage.getItem(SESSION_LOCK_CONSTANTS.STORAGE_ITEM_PING); if (lastPingTime === null) { // no other holder prefixedLogger.info("No other session has the lock"); return true; } const lockHolder = window.localStorage.getItem(SESSION_LOCK_CONSTANTS.STORAGE_ITEM_OWNER); // see if it has expired const timeAgo = Date.now() - parseInt(lastPingTime); const remaining = SESSION_LOCK_CONSTANTS.LOCK_EXPIRY_TIME_MS - timeAgo; if (remaining <= 0) { // another session claimed the lock, but it is stale. prefixedLogger.info(`Last ping (from ${lockHolder}) was ${timeAgo}ms ago: lock is free`); return true; } prefixedLogger.info(`Last ping (from ${lockHolder}) was ${timeAgo}ms ago: lock is taken`); return false; } /** * Ensure that only one instance of the application is running at once. * * If there are any other running instances, tells them to stop, and waits for them to do so. * * Once we are the sole instance, sets a background job going to service a lock. Then, if another instance starts up, * `onNewInstance` is called: it should shut the app down to make sure we aren't doing any more work. * * @param onNewInstance - callback to handle another instance starting up. NOTE: this may be called before * `getSessionLock` returns if the lock is stolen before we get a chance to start. * * @returns true if we successfully claimed the lock; false if another instance stole it from under our nose * (in which `onNewInstance` will have been called) */ async function getSessionLock(onNewInstance) { /** unique ID for this session */ const sessionIdentifier = (0, _uuid.v4)(); const prefixedLogger = _logger.logger.getChild(`getSessionLock[${sessionIdentifier}]`); /** The ID of our regular task to service the lock. * * Non-null while we hold the lock; null if we have not yet claimed it, or have released it. */ let lockServicer = null; /** * See if the lock is free. * * @returns * - `>0`: the number of milliseconds before the current claim on the lock can be considered stale. * - `0`: the lock is free for the taking * - `<0`: someone else has staked a claim for the lock, so we are no longer in line for it. */ function checkLock() { // first of all, check that we are still the active claimant (ie, another instance hasn't come along while we were waiting. const claimant = window.localStorage.getItem(SESSION_LOCK_CONSTANTS.STORAGE_ITEM_CLAIMANT); if (claimant !== sessionIdentifier) { prefixedLogger.warn(`Lock was claimed by ${claimant} while we were waiting for it: aborting startup`); return -1; } const lastPingTime = window.localStorage.getItem(SESSION_LOCK_CONSTANTS.STORAGE_ITEM_PING); const lockHolder = window.localStorage.getItem(SESSION_LOCK_CONSTANTS.STORAGE_ITEM_OWNER); if (lastPingTime === null) { prefixedLogger.info("No other session has the lock: proceeding with startup"); return 0; } const timeAgo = Date.now() - parseInt(lastPingTime); const remaining = SESSION_LOCK_CONSTANTS.LOCK_EXPIRY_TIME_MS - timeAgo; if (remaining <= 0) { // another session claimed the lock, but it is stale. prefixedLogger.info(`Last ping (from ${lockHolder}) was ${timeAgo}ms ago: proceeding with startup`); return 0; } prefixedLogger.info(`Last ping (from ${lockHolder}) was ${timeAgo}ms ago, waiting ${remaining}ms`); return remaining; } function serviceLock() { window.localStorage.setItem(SESSION_LOCK_CONSTANTS.STORAGE_ITEM_OWNER, sessionIdentifier); window.localStorage.setItem(SESSION_LOCK_CONSTANTS.STORAGE_ITEM_PING, Date.now().toString()); } // handler for storage events, used later function onStorageEvent(event) { if (event.key === SESSION_LOCK_CONSTANTS.STORAGE_ITEM_CLAIMANT) { // It's possible that the event was delayed, and this update actually predates our claim on the lock. // (In particular: suppose tab A and tab B start concurrently and both attempt to set STORAGE_ITEM_CLAIMANT. // Each write queues up a `storage` event for all other tabs. So both tabs see the `storage` event from the // other, even though by the time it arrives we may have overwritten it.) // // To resolve any doubt, we check the *actual* state of the storage. const claimingSession = window.localStorage.getItem(SESSION_LOCK_CONSTANTS.STORAGE_ITEM_CLAIMANT); if (claimingSession === sessionIdentifier) { return; } prefixedLogger.info(`Session ${claimingSession} is waiting for the lock`); window.removeEventListener("storage", onStorageEvent); releaseLock().catch(err => { prefixedLogger.error("Error releasing session lock", err); }); } } // handler for pagehide and unload events, used later function onPagehideEvent() { // only remove the ping if we still think we're the owner. Otherwise we could be removing someone else's claim! if (lockServicer !== null) { prefixedLogger.debug("page hide: clearing our claim"); window.clearInterval(lockServicer); window.localStorage.removeItem(SESSION_LOCK_CONSTANTS.STORAGE_ITEM_PING); window.localStorage.removeItem(SESSION_LOCK_CONSTANTS.STORAGE_ITEM_OWNER); lockServicer = null; } // It's worth noting that, according to the spec, the page might come back to life again after a pagehide. // // In practice that's unlikely because Element is unlikely to qualify for the bfcache, but if it does, // this is probably the best we can do: we certainly don't want to stop the user loading any new tabs because // Element happens to be in a bfcache somewhere. // // So, we just hope that we aren't in the middle of any crypto operations, and rely on `onStorageEvent` kicking // in soon enough after we resume to tell us if another tab woke up while we were asleep. } async function releaseLock() { // tell the app to shut down await onNewInstance(); // and, once it has done so, stop pinging the lock. if (lockServicer !== null) { window.clearInterval(lockServicer); } window.localStorage.removeItem(SESSION_LOCK_CONSTANTS.STORAGE_ITEM_PING); window.localStorage.removeItem(SESSION_LOCK_CONSTANTS.STORAGE_ITEM_OWNER); lockServicer = null; } // first of all, stake a claim for the lock. This tells anyone else holding the lock that we want it. window.localStorage.setItem(SESSION_LOCK_CONSTANTS.STORAGE_ITEM_CLAIMANT, sessionIdentifier); // now, wait for the lock to be free. // eslint-disable-next-line no-constant-condition while (true) { const remaining = checkLock(); if (remaining == 0) { // ok, the lock is free, and nobody else has staked a more recent claim. break; } else if (remaining < 0) { // someone else staked a claim for the lock; we bail out. await onNewInstance(); return false; } // someone else has the lock. // wait for either the ping to expire, or a storage event. let onStorageUpdate; const storageUpdatePromise = new Promise(resolve => { onStorageUpdate = event => { if (event.key === SESSION_LOCK_CONSTANTS.STORAGE_ITEM_PING || event.key === SESSION_LOCK_CONSTANTS.STORAGE_ITEM_CLAIMANT) resolve(event); }; }); const sleepPromise = new Promise(resolve => { setTimeout(resolve, remaining, undefined); }); window.addEventListener("storage", onStorageUpdate); await Promise.race([sleepPromise, storageUpdatePromise]); window.removeEventListener("storage", onStorageUpdate); } // If we get here, we know the lock is ours for the taking. // CRITICAL SECTION // // The following code, up to the end of the function, must all be synchronous (ie, no `await` calls), to ensure that // we get our listeners in place and all the writes to localStorage done before other tabs run again. // claim the lock, and kick off a background process to service it every 5 seconds serviceLock(); lockServicer = window.setInterval(serviceLock, 5000); // Now add a listener for other claimants to the lock. window.addEventListener("storage", onStorageEvent); // also add a listener to clear our claims when our tab closes or navigates away window.addEventListener("pagehide", onPagehideEvent); // The pagehide event is called unreliably on Firefox, so additionally add an unload handler. // https://bugzilla.mozilla.org/show_bug.cgi?id=1854492 window.addEventListener("unload", onPagehideEvent); return true; } //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJfbG9nZ2VyIiwicmVxdWlyZSIsIl91dWlkIiwiU0VTU0lPTl9MT0NLX0NPTlNUQU5UUyIsImV4cG9ydHMiLCJTVE9SQUdFX0lURU1fUElORyIsIlNUT1JBR0VfSVRFTV9PV05FUiIsIlNUT1JBR0VfSVRFTV9DTEFJTUFOVCIsIkxPQ0tfRVhQSVJZX1RJTUVfTVMiLCJjaGVja1Nlc3Npb25Mb2NrRnJlZSIsInByZWZpeGVkTG9nZ2VyIiwibG9nZ2VyIiwiZ2V0Q2hpbGQiLCJsYXN0UGluZ1RpbWUiLCJ3aW5kb3ciLCJsb2NhbFN0b3JhZ2UiLCJnZXRJdGVtIiwiaW5mbyIsImxvY2tIb2xkZXIiLCJ0aW1lQWdvIiwiRGF0ZSIsIm5vdyIsInBhcnNlSW50IiwicmVtYWluaW5nIiwiZ2V0U2Vzc2lvbkxvY2siLCJvbk5ld0luc3RhbmNlIiwic2Vzc2lvbklkZW50aWZpZXIiLCJ1dWlkdjQiLCJsb2NrU2VydmljZXIiLCJjaGVja0xvY2siLCJjbGFpbWFudCIsIndhcm4iLCJzZXJ2aWNlTG9jayIsInNldEl0ZW0iLCJ0b1N0cmluZyIsIm9uU3RvcmFnZUV2ZW50IiwiZXZlbnQiLCJrZXkiLCJjbGFpbWluZ1Nlc3Npb24iLCJyZW1vdmVFdmVudExpc3RlbmVyIiwicmVsZWFzZUxvY2siLCJjYXRjaCIsImVyciIsImVycm9yIiwib25QYWdlaGlkZUV2ZW50IiwiZGVidWciLCJjbGVhckludGVydmFsIiwicmVtb3ZlSXRlbSIsIm9uU3RvcmFnZVVwZGF0ZSIsInN0b3JhZ2VVcGRhdGVQcm9taXNlIiwiUHJvbWlzZSIsInJlc29sdmUiLCJzbGVlcFByb21pc2UiLCJzZXRUaW1lb3V0IiwidW5kZWZpbmVkIiwiYWRkRXZlbnRMaXN0ZW5lciIsInJhY2UiLCJzZXRJbnRlcnZhbCJdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9TZXNzaW9uTG9jay50cyJdLCJzb3VyY2VzQ29udGVudCI6WyIvKlxuQ29weXJpZ2h0IDIwMjQgTmV3IFZlY3RvciBMdGQuXG5Db3B5cmlnaHQgMjAyMyBUaGUgTWF0cml4Lm9yZyBGb3VuZGF0aW9uIEMuSS5DLlxuXG5TUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQUdQTC0zLjAtb25seSBPUiBHUEwtMy4wLW9ubHlcblBsZWFzZSBzZWUgTElDRU5TRSBmaWxlcyBpbiB0aGUgcmVwb3NpdG9yeSByb290IGZvciBmdWxsIGRldGFpbHMuXG4qL1xuXG5pbXBvcnQgeyBsb2dnZXIgfSBmcm9tIFwibWF0cml4LWpzLXNkay9zcmMvbG9nZ2VyXCI7XG5pbXBvcnQgeyB2NCBhcyB1dWlkdjQgfSBmcm9tIFwidXVpZFwiO1xuXG4vKlxuICogRnVuY3Rpb25hbGl0eSBmb3IgY2hlY2tpbmcgdGhhdCBvbmx5IG9uZSBpbnN0YW5jZSBpcyBydW5uaW5nIGF0IG9uY2VcbiAqXG4gKiBUaGUgYWxnb3JpdGhtIGhlcmUgaXMgdHdvZm9sZC5cbiAqXG4gKiBGaXJzdCwgd2UgXCJjbGFpbVwiIGEgbG9jayBieSBwZXJpb2RpY2FsbHkgd3JpdGluZyB0byBgU1RPUkFHRV9JVEVNX1BJTkdgLiBPbiBzaHV0ZG93biwgd2UgY2xlYXIgdGhhdCBpdGVtLiBTbyxcbiAqIGEgbmV3IGluc3RhbmNlIHN0YXJ0aW5nIHVwIGNhbiBjaGVjayBpZiB0aGUgbG9jayBpcyBmcmVlIGJ5IGluc3BlY3RpbmcgYFNUT1JBR0VfSVRFTV9QSU5HYC4gSWYgaXQgaXMgdW5zZXQsXG4gKiBvciBpcyBzdGFsZSwgdGhlIG5ldyBpbnN0YW5jZSBjYW4gYXNzdW1lIHRoZSBsb2NrIGlzIGZyZWUgYW5kIGNsYWltIGl0IGZvciBpdHNlbGYuIE90aGVyd2lzZSwgdGhlIG5ldyBpbnN0YW5jZVxuICogaGFzIHRvIHdhaXQgZm9yIHRoZSBwaW5nIHRvIGJlIHN0YWxlLCBvciB0aGUgaXRlbSB0byBiZSBjbGVhcmVkLlxuICpcbiAqIFNlY29uZGx5LCB3ZSBuZWVkIGEgbWVjaGFuaXNtIGZvciBwcm9hY3RpdmVseSB0ZWxsaW5nIGV4aXN0aW5nIGluc3RhbmNlcyB0byBzaHV0IGRvd24uIFdlIGRvIHRoaXMgYnkgd3JpdGluZyBhXG4gKiB1bmlxdWUgdmFsdWUgdG8gYFNUT1JBR0VfSVRFTV9DTEFJTUFOVGAuIE90aGVyIGluc3RhbmNlcyBvZiB0aGUgYXBwIGFyZSBzdXBwb3NlZCB0byBtb25pdG9yIGZvciB3cml0ZXMgdG9cbiAqIGBTVE9SQUdFX0lURU1fQ0xBSU1BTlRgIGFuZCBpbml0aWF0ZSBzaHV0ZG93biB3aGVuIGl0IGhhcHBlbnMuXG4gKlxuICogVGhlcmUgaXMgc2xpZ2h0IGNvbXBsZXhpdHkgaW4gYFNUT1JBR0VfSVRFTV9DTEFJTUFOVGAgaW4gdGhhdCB3ZSBuZWVkIHRvIHdhdGNoIG91dCBmb3IgeWV0IGFub3RoZXIgaW5zdGFuY2VcbiAqIHN0YXJ0aW5nIHVwIGFuZCBzdGFraW5nIGEgY2xhaW0gYmVmb3JlIHdlIGV2ZW4gZ2V0IGEgY2hhbmNlIHRvIHRha2UgdGhlIGxvY2suIFdoZW4gdGhhdCBoYXBwZW5zIHdlIGp1c3QgYmFpbCBvdXRcbiAqIGFuZCBsZXQgdGhlIG5ld2VyIGluc3RhbmNlIGdldCB0aGUgbG9jay5cbiAqXG4gKiBgU1RPUkFHRV9JVEVNX09XTkVSYCBoYXMgbm8gZnVuY3Rpb25hbCByb2xlIGluIHRoZSBsb2NrIG1lY2hhbmlzbTsgaXQgZXhpc3RzIHNvbGVseSBhcyBhIGRpYWdub3N0aWMgaW5kaWNhdG9yXG4gKiBvZiB3aGljaCBpbnN0YW5jZSBpcyB3cml0aW5nIHRvIGBTVE9SQUdFX0lURU1fUElOR2AuXG4gKi9cblxuZXhwb3J0IGNvbnN0IFNFU1NJT05fTE9DS19DT05TVEFOVFMgPSB7XG4gICAgLyoqXG4gICAgICogTG9jYWxTdG9yYWdlIGtleSBmb3IgYW4gaXRlbSB3aGljaCBpbmRpY2F0ZXMgd2UgaGF2ZSB0aGUgbG9jay5cbiAgICAgKlxuICAgICAqIFRoZSBpbnN0YW5jZSB3aGljaCBob2xkcyB0aGUgbG9jayB3cml0ZXMgdGhlIGN1cnJlbnQgdGltZSB0byB0aGlzIGtleSBldmVyeSBmZXcgc2Vjb25kcywgdG8gaW5kaWNhdGUgaXQgaXMgc3RpbGxcbiAgICAgKiBhbGl2ZSBhbmQgaG9sZHMgdGhlIGxvY2suXG4gICAgICovXG4gICAgU1RPUkFHRV9JVEVNX1BJTkc6IFwicmVhY3Rfc2RrX3Nlc3Npb25fbG9ja19waW5nXCIsXG5cbiAgICAvKipcbiAgICAgKiBMb2NhbFN0b3JhZ2Uga2V5IGZvciBhbiBpdGVtIHdoaWNoIGhvbGRzIHRoZSB1bmlxdWUgXCJzZXNzaW9uIElEXCIgb2YgdGhlIGluc3RhbmNlIHdoaWNoIGN1cnJlbnRseSBob2xkcyB0aGUgbG9jay5cbiAgICAgKlxuICAgICAqIFRoaXMgcHJvcGVydHkgZG9lc24ndCBhY3R1YWxseSBmb3JtIGEgZnVuY3Rpb25hbCBwYXJ0IG9mIHRoZSBsb2NraW5nIGFsZ29yaXRobTsgaXQgaXMgcHVyZWx5IGRpYWdub3N0aWMuXG4gICAgICovXG4gICAgU1RPUkFHRV9JVEVNX09XTkVSOiBcInJlYWN0X3Nka19zZXNzaW9uX2xvY2tfb3duZXJcIixcblxuICAgIC8qKlxuICAgICAqIExvY2FsU3RvcmFnZSBrZXkgZm9yIHRoZSBzZXNzaW9uIElEIG9mIHRoZSBtb3N0IHJlY2VudCBjbGFpbWFudCB0byB0aGUgbG9jay5cbiAgICAgKlxuICAgICAqIEVhY2ggaW5zdGFuY2Ugd3JpdGVzIHRvIHRoaXMga2V5IG9uIHN0YXJ0dXAsIHNvIGV4aXN0aW5nIGluc3RhbmNlcyBjYW4gZGV0ZWN0IG5ldyBvbmVzIHN0YXJ0aW5nIHVwLlxuICAgICAqL1xuICAgIFNUT1JBR0VfSVRFTV9DTEFJTUFOVDogXCJyZWFjdF9zZGtfc2Vzc2lvbl9sb2NrX2NsYWltYW50XCIsXG5cbiAgICAvKipcbiAgICAgKiBUaGUgbnVtYmVyIG9mIG1pbGxpc2Vjb25kcyBhZnRlciB3aGljaCB3ZSBjb25zaWRlciBhIGxvY2sgY2xhaW0gc3RhbGVcbiAgICAgKi9cbiAgICBMT0NLX0VYUElSWV9USU1FX01TOiAzMDAwMCxcbn07XG5cbi8qKlxuICogU2VlIGlmIGFueSBpbnN0YW5jZXMgYXJlIGN1cnJlbnRseSBydW5uaW5nXG4gKlxuICogQHJldHVybnMgdHJ1ZSBpZiBhbnkgaW5zdGFuY2UgaXMgY3VycmVudGx5IGFjdGl2ZVxuICovXG5leHBvcnQgZnVuY3Rpb24gY2hlY2tTZXNzaW9uTG9ja0ZyZWUoKTogYm9vbGVhbiB7XG4gICAgY29uc3QgcHJlZml4ZWRMb2dnZXIgPSBsb2dnZXIuZ2V0Q2hpbGQoYGNoZWNrU2Vzc2lvbkxvY2tGcmVlYCk7XG5cbiAgICBjb25zdCBsYXN0UGluZ1RpbWUgPSB3aW5kb3cubG9jYWxTdG9yYWdlLmdldEl0ZW0oU0VTU0lPTl9MT0NLX0NPTlNUQU5UUy5TVE9SQUdFX0lURU1fUElORyk7XG4gICAgaWYgKGxhc3RQaW5nVGltZSA9PT0gbnVsbCkge1xuICAgICAgICAvLyBubyBvdGhlciBob2xkZXJcbiAgICAgICAgcHJlZml4ZWRMb2dnZXIuaW5mbyhcIk5vIG90aGVyIHNlc3Npb24gaGFzIHRoZSBsb2NrXCIpO1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG5cbiAgICBjb25zdCBsb2NrSG9sZGVyID0gd2luZG93LmxvY2FsU3RvcmFnZS5nZXRJdGVtKFNFU1NJT05fTE9DS19DT05TVEFOVFMuU1RPUkFHRV9JVEVNX09XTkVSKTtcblxuICAgIC8vIHNlZSBpZiBpdCBoYXMgZXhwaXJlZFxuICAgIGNvbnN0IHRpbWVBZ28gPSBEYXRlLm5vdygpIC0gcGFyc2VJbnQobGFzdFBpbmdUaW1lKTtcblxuICAgIGNvbnN0IHJlbWFpbmluZyA9IFNFU1NJT05fTE9DS19DT05TVEFOVFMuTE9DS19FWFBJUllfVElNRV9NUyAtIHRpbWVBZ287XG4gICAgaWYgKHJlbWFpbmluZyA8PSAwKSB7XG4gICAgICAgIC8vIGFub3RoZXIgc2Vzc2lvbiBjbGFpbWVkIHRoZSBsb2NrLCBidXQgaXQgaXMgc3RhbGUuXG4gICAgICAgIHByZWZpeGVkTG9nZ2VyLmluZm8oYExhc3QgcGluZyAoZnJvbSAke2xvY2tIb2xkZXJ9KSB3YXMgJHt0aW1lQWdvfW1zIGFnbzogbG9jayBpcyBmcmVlYCk7XG4gICAgICAgIHJldHVybiB0cnVlO1xuICAgIH1cblxuICAgIHByZWZpeGVkTG9nZ2VyLmluZm8oYExhc3QgcGluZyAoZnJvbSAke2xvY2tIb2xkZXJ9KSB3YXMgJHt0aW1lQWdvfW1zIGFnbzogbG9jayBpcyB0YWtlbmApO1xuICAgIHJldHVybiBmYWxzZTtcbn1cblxuLyoqXG4gKiBFbnN1cmUgdGhhdCBvbmx5IG9uZSBpbnN0YW5jZSBvZiB0aGUgYXBwbGljYXRpb24gaXMgcnVubmluZyBhdCBvbmNlLlxuICpcbiAqIElmIHRoZXJlIGFyZSBhbnkgb3RoZXIgcnVubmluZyBpbnN0YW5jZXMsIHRlbGxzIHRoZW0gdG8gc3RvcCwgYW5kIHdhaXRzIGZvciB0aGVtIHRvIGRvIHNvLlxuICpcbiAqIE9uY2Ugd2UgYXJlIHRoZSBzb2xlIGluc3RhbmNlLCBzZXRzIGEgYmFja2dyb3VuZCBqb2IgZ29pbmcgdG8gc2VydmljZSBhIGxvY2suIFRoZW4sIGlmIGFub3RoZXIgaW5zdGFuY2Ugc3RhcnRzIHVwLFxuICogYG9uTmV3SW5zdGFuY2VgIGlzIGNhbGxlZDogaXQgc2hvdWxkIHNodXQgdGhlIGFwcCBkb3duIHRvIG1ha2Ugc3VyZSB3ZSBhcmVuJ3QgZG9pbmcgYW55IG1vcmUgd29yay5cbiAqXG4gKiBAcGFyYW0gb25OZXdJbnN0YW5jZSAtIGNhbGxiYWNrIHRvIGhhbmRsZSBhbm90aGVyIGluc3RhbmNlIHN0YXJ0aW5nIHVwLiBOT1RFOiB0aGlzIG1heSBiZSBjYWxsZWQgYmVmb3JlXG4gKiAgICAgYGdldFNlc3Npb25Mb2NrYCByZXR1cm5zIGlmIHRoZSBsb2NrIGlzIHN0b2xlbiBiZWZvcmUgd2UgZ2V0IGEgY2hhbmNlIHRvIHN0YXJ0LlxuICpcbiAqIEByZXR1cm5zIHRydWUgaWYgd2Ugc3VjY2Vzc2Z1bGx5IGNsYWltZWQgdGhlIGxvY2s7IGZhbHNlIGlmIGFub3RoZXIgaW5zdGFuY2Ugc3RvbGUgaXQgZnJvbSB1bmRlciBvdXIgbm9zZVxuICogICAgIChpbiB3aGljaCBgb25OZXdJbnN0YW5jZWAgd2lsbCBoYXZlIGJlZW4gY2FsbGVkKVxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gZ2V0U2Vzc2lvbkxvY2sob25OZXdJbnN0YW5jZTogKCkgPT4gUHJvbWlzZTx2b2lkPik6IFByb21pc2U8Ym9vbGVhbj4ge1xuICAgIC8qKiB1bmlxdWUgSUQgZm9yIHRoaXMgc2Vzc2lvbiAqL1xuICAgIGNvbnN0IHNlc3Npb25JZGVudGlmaWVyID0gdXVpZHY0KCk7XG5cbiAgICBjb25zdCBwcmVmaXhlZExvZ2dlciA9IGxvZ2dlci5nZXRDaGlsZChgZ2V0U2Vzc2lvbkxvY2tbJHtzZXNzaW9uSWRlbnRpZmllcn1dYCk7XG5cbiAgICAvKiogVGhlIElEIG9mIG91ciByZWd1bGFyIHRhc2sgdG8gc2VydmljZSB0aGUgbG9jay5cbiAgICAgKlxuICAgICAqIE5vbi1udWxsIHdoaWxlIHdlIGhvbGQgdGhlIGxvY2s7IG51bGwgaWYgd2UgaGF2ZSBub3QgeWV0IGNsYWltZWQgaXQsIG9yIGhhdmUgcmVsZWFzZWQgaXQuICovXG4gICAgbGV0IGxvY2tTZXJ2aWNlcjogbnVtYmVyIHwgbnVsbCA9IG51bGw7XG5cbiAgICAvKipcbiAgICAgKiBTZWUgaWYgdGhlIGxvY2sgaXMgZnJlZS5cbiAgICAgKlxuICAgICAqIEByZXR1cm5zXG4gICAgICogIC0gYD4wYDogdGhlIG51bWJlciBvZiBtaWxsaXNlY29uZHMgYmVmb3JlIHRoZSBjdXJyZW50IGNsYWltIG9uIHRoZSBsb2NrIGNhbiBiZSBjb25zaWRlcmVkIHN0YWxlLlxuICAgICAqICAtIGAwYDogdGhlIGxvY2sgaXMgZnJlZSBmb3IgdGhlIHRha2luZ1xuICAgICAqICAtIGA8MGA6IHNvbWVvbmUgZWxzZSBoYXMgc3Rha2VkIGEgY2xhaW0gZm9yIHRoZSBsb2NrLCBzbyB3ZSBhcmUgbm8gbG9uZ2VyIGluIGxpbmUgZm9yIGl0LlxuICAgICAqL1xuICAgIGZ1bmN0aW9uIGNoZWNrTG9jaygpOiBudW1iZXIge1xuICAgICAgICAvLyBmaXJzdCBvZiBhbGwsIGNoZWNrIHRoYXQgd2UgYXJlIHN0aWxsIHRoZSBhY3RpdmUgY2xhaW1hbnQgKGllLCBhbm90aGVyIGluc3RhbmNlIGhhc24ndCBjb21lIGFsb25nIHdoaWxlIHdlIHdlcmUgd2FpdGluZy5cbiAgICAgICAgY29uc3QgY2xhaW1hbnQgPSB3aW5kb3cubG9jYWxTdG9yYWdlLmdldEl0ZW0oU0VTU0lPTl9MT0NLX0NPTlNUQU5UUy5TVE9SQUdFX0lURU1fQ0xBSU1BTlQpO1xuICAgICAgICBpZiAoY2xhaW1hbnQgIT09IHNlc3Npb25JZGVudGlmaWVyKSB7XG4gICAgICAgICAgICBwcmVmaXhlZExvZ2dlci53YXJuKGBMb2NrIHdhcyBjbGFpbWVkIGJ5ICR7Y2xhaW1hbnR9IHdoaWxlIHdlIHdlcmUgd2FpdGluZyBmb3IgaXQ6IGFib3J0aW5nIHN0YXJ0dXBgKTtcbiAgICAgICAgICAgIHJldHVybiAtMTtcbiAgICAgICAgfVxuXG4gICAgICAgIGNvbnN0IGxhc3RQaW5nVGltZSA9IHdpbmRvdy5sb2NhbFN0b3JhZ2UuZ2V0SXRlbShTRVNTSU9OX0xPQ0tfQ09OU1RBTlRTLlNUT1JBR0VfSVRFTV9QSU5HKTtcbiAgICAgICAgY29uc3QgbG9ja0hvbGRlciA9IHdpbmRvdy5sb2NhbFN0b3JhZ2UuZ2V0SXRlbShTRVNTSU9OX0xPQ0tfQ09OU1RBTlRTLlNUT1JBR0VfSVRFTV9PV05FUik7XG4gICAgICAgIGlmIChsYXN0UGluZ1RpbWUgPT09IG51bGwpIHtcbiAgICAgICAgICAgIHByZWZpeGVkTG9nZ2VyLmluZm8oXCJObyBvdGhlciBzZXNzaW9uIGhhcyB0aGUgbG9jazogcHJvY2VlZGluZyB3aXRoIHN0YXJ0dXBcIik7XG4gICAgICAgICAgICByZXR1cm4gMDtcbiAgICAgICAgfVxuXG4gICAgICAgIGNvbnN0IHRpbWVBZ28gPSBEYXRlLm5vdygpIC0gcGFyc2VJbnQobGFzdFBpbmdUaW1lKTtcbiAgICAgICAgY29uc3QgcmVtYWluaW5nID0gU0VTU0lPTl9MT0NLX0NPTlNUQU5UUy5MT0NLX0VYUElSWV9USU1FX01TIC0gdGltZUFnbztcbiAgICAgICAgaWYgKHJlbWFpbmluZyA8PSAwKSB7XG4gICAgICAgICAgICAvLyBhbm90aGVyIHNlc3Npb24gY2xhaW1lZCB0aGUgbG9jaywgYnV0IGl0IGlzIHN0YWxlLlxuICAgICAgICAgICAgcHJlZml4ZWRMb2dnZXIuaW5mbyhgTGFzdCBwaW5nIChmcm9tICR7bG9ja0hvbGRlcn0pIHdhcyAke3RpbWVBZ299bXMgYWdvOiBwcm9jZWVkaW5nIHdpdGggc3RhcnR1cGApO1xuICAgICAgICAgICAgcmV0dXJuIDA7XG4gICAgICAgIH1cblxuICAgICAgICBwcmVmaXhlZExvZ2dlci5pbmZvKGBMYXN0IHBpbmcgKGZyb20gJHtsb2NrSG9sZGVyfSkgd2FzICR7dGltZUFnb31tcyBhZ28sIHdhaXRpbmcgJHtyZW1haW5pbmd9bXNgKTtcbiAgICAgICAgcmV0dXJuIHJlbWFpbmluZztcbiAgICB9XG5cbiAgICBmdW5jdGlvbiBzZXJ2aWNlTG9jaygpOiB2b2lkIHtcbiAgICAgICAgd2luZG93LmxvY2FsU3RvcmFnZS5zZXRJdGVtKFNFU1NJT05fTE9DS19DT05TVEFOVFMuU1RPUkFHRV9JVEVNX09XTkVSLCBzZXNzaW9uSWRlbnRpZmllcik7XG4gICAgICAgIHdpbmRvdy5sb2NhbFN0b3JhZ2Uuc2V0SXRlbShTRVNTSU9OX0xPQ0tfQ09OU1RBTlRTLlNUT1JBR0VfSVRFTV9QSU5HLCBEYXRlLm5vdygpLnRvU3RyaW5nKCkpO1xuICAgIH1cblxuICAgIC8vIGhhbmRsZXIgZm9yIHN0b3JhZ2UgZXZlbnRzLCB1c2VkIGxhdGVyXG4gICAgZnVuY3Rpb24gb25TdG9yYWdlRXZlbnQoZXZlbnQ6IFN0b3JhZ2VFdmVudCk6IHZvaWQge1xuICAgICAgICBpZiAoZXZlbnQua2V5ID09PSBTRVNTSU9OX0xPQ0tfQ09OU1RBTlRTLlNUT1JBR0VfSVRFTV9DTEFJTUFOVCkge1xuICAgICAgICAgICAgLy8gSXQncyBwb3NzaWJsZSB0aGF0IHRoZSBldmVudCB3YXMgZGVsYXllZCwgYW5kIHRoaXMgdXBkYXRlIGFjdHVhbGx5IHByZWRhdGVzIG91ciBjbGFpbSBvbiB0aGUgbG9jay5cbiAgICAgICAgICAgIC8vIChJbiBwYXJ0aWN1bGFyOiBzdXBwb3NlIHRhYiBBIGFuZCB0YWIgQiBzdGFydCBjb25jdXJyZW50bHkgYW5kIGJvdGggYXR0ZW1wdCB0byBzZXQgU1RPUkFHRV9JVEVNX0NMQUlNQU5ULlxuICAgICAgICAgICAgLy8gRWFjaCB3cml0ZSBxdWV1ZXMgdXAgYSBgc3RvcmFnZWAgZXZlbnQgZm9yIGFsbCBvdGhlciB0YWJzLiBTbyBib3RoIHRhYnMgc2VlIHRoZSBgc3RvcmFnZWAgZXZlbnQgZnJvbSB0aGVcbiAgICAgICAgICAgIC8vIG90aGVyLCBldmVuIHRob3VnaCBieSB0aGUgdGltZSBpdCBhcnJpdmVzIHdlIG1heSBoYXZlIG92ZXJ3cml0dGVuIGl0LilcbiAgICAgICAgICAgIC8vXG4gICAgICAgICAgICAvLyBUbyByZXNvbHZlIGFueSBkb3VidCwgd2UgY2hlY2sgdGhlICphY3R1YWwqIHN0YXRlIG9mIHRoZSBzdG9yYWdlLlxuICAgICAgICAgICAgY29uc3QgY2xhaW1pbmdTZXNzaW9uID0gd2luZG93LmxvY2FsU3RvcmFnZS5nZXRJdGVtKFNFU1NJT05fTE9DS19DT05TVEFOVFMuU1RPUkFHRV9JVEVNX0NMQUlNQU5UKTtcbiAgICAgICAgICAgIGlmIChjbGFpbWluZ1Nlc3Npb24gPT09IHNlc3Npb25JZGVudGlmaWVyKSB7XG4gICAgICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgcHJlZml4ZWRMb2dnZXIuaW5mbyhgU2Vzc2lvbiAke2NsYWltaW5nU2Vzc2lvbn0gaXMgd2FpdGluZyBmb3IgdGhlIGxvY2tgKTtcbiAgICAgICAgICAgIHdpbmRvdy5yZW1vdmVFdmVudExpc3RlbmVyKFwic3RvcmFnZVwiLCBvblN0b3JhZ2VFdmVudCk7XG4gICAgICAgICAgICByZWxlYXNlTG9jaygpLmNhdGNoKChlcnIpID0+IHtcbiAgICAgICAgICAgICAgICBwcmVmaXhlZExvZ2dlci5lcnJvcihcIkVycm9yIHJlbGVhc2luZyBzZXNzaW9uIGxvY2tcIiwgZXJyKTtcbiAgICAgICAgICAgIH0pO1xuICAgICAgICB9XG4gICAgfVxuXG4gICAgLy8gaGFuZGxlciBmb3IgcGFnZWhpZGUgYW5kIHVubG9hZCBldmVudHMsIHVzZWQgbGF0ZXJcbiAgICBmdW5jdGlvbiBvblBhZ2VoaWRlRXZlbnQoKTogdm9pZCB7XG4gICAgICAgIC8vIG9ubHkgcmVtb3ZlIHRoZSBwaW5nIGlmIHdlIHN0aWxsIHRoaW5rIHdlJ3JlIHRoZSBvd25lci4gT3RoZXJ3aXNlIHdlIGNvdWxkIGJlIHJlbW92aW5nIHNvbWVvbmUgZWxzZSdzIGNsYWltIVxuICAgICAgICBpZiAobG9ja1NlcnZpY2VyICE9PSBudWxsKSB7XG4gICAgICAgICAgICBwcmVmaXhlZExvZ2dlci5kZWJ1ZyhcInBhZ2UgaGlkZTogY2xlYXJpbmcgb3VyIGNsYWltXCIpO1xuICAgICAgICAgICAgd2luZG93LmNsZWFySW50ZXJ2YWwobG9ja1NlcnZpY2VyKTtcbiAgICAgICAgICAgIHdpbmRvdy5sb2NhbFN0b3JhZ2UucmVtb3ZlSXRlbShTRVNTSU9OX0xPQ0tfQ09OU1RBTlRTLlNUT1JBR0VfSVRFTV9QSU5HKTtcbiAgICAgICAgICAgIHdpbmRvdy5sb2NhbFN0b3JhZ2UucmVtb3ZlSXRlbShTRVNTSU9OX0xPQ0tfQ09OU1RBTlRTLlNUT1JBR0VfSVRFTV9PV05FUik7XG4gICAgICAgICAgICBsb2NrU2VydmljZXIgPSBudWxsO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gSXQncyB3b3J0aCBub3RpbmcgdGhhdCwgYWNjb3JkaW5nIHRvIHRoZSBzcGVjLCB0aGUgcGFnZSBtaWdodCBjb21lIGJhY2sgdG8gbGlmZSBhZ2FpbiBhZnRlciBhIHBhZ2VoaWRlLlxuICAgICAgICAvL1xuICAgICAgICAvLyBJbiBwcmFjdGljZSB0aGF0J3MgdW5saWtlbHkgYmVjYXVzZSBFbGVtZW50IGlzIHVubGlrZWx5IHRvIHF1YWxpZnkgZm9yIHRoZSBiZmNhY2hlLCBidXQgaWYgaXQgZG9lcyxcbiAgICAgICAgLy8gdGhpcyBpcyBwcm9iYWJseSB0aGUgYmVzdCB3ZSBjYW4gZG86IHdlIGNlcnRhaW5seSBkb24ndCB3YW50IHRvIHN0b3AgdGhlIHVzZXIgbG9hZGluZyBhbnkgbmV3IHRhYnMgYmVjYXVzZVxuICAgICAgICAvLyBFbGVtZW50IGhhcHBlbnMgdG8gYmUgaW4gYSBiZmNhY2hlIHNvbWV3aGVyZS5cbiAgICAgICAgLy9cbiAgICAgICAgLy8gU28sIHdlIGp1c3QgaG9wZSB0aGF0IHdlIGFyZW4ndCBpbiB0aGUgbWlkZGxlIG9mIGFueSBjcnlwdG8gb3BlcmF0aW9ucywgYW5kIHJlbHkgb24gYG9uU3RvcmFnZUV2ZW50YCBraWNraW5nXG4gICAgICAgIC8vIGluIHNvb24gZW5vdWdoIGFmdGVyIHdlIHJlc3VtZSB0byB0ZWxsIHVzIGlmIGFub3RoZXIgdGFiIHdva2UgdXAgd2hpbGUgd2Ugd2VyZSBhc2xlZXAuXG4gICAgfVxuXG4gICAgYXN5bmMgZnVuY3Rpb24gcmVsZWFzZUxvY2soKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgICAgIC8vIHRlbGwgdGhlIGFwcCB0byBzaHV0IGRvd25cbiAgICAgICAgYXdhaXQgb25OZXdJbnN0YW5jZSgpO1xuXG4gICAgICAgIC8vIGFuZCwgb25jZSBpdCBoYXMgZG9uZSBzbywgc3RvcCBwaW5naW5nIHRoZSBsb2NrLlxuICAgICAgICBpZiAobG9ja1NlcnZpY2VyICE9PSBudWxsKSB7XG4gICAgICAgICAgICB3aW5kb3cuY2xlYXJJbnRlcnZhbChsb2NrU2VydmljZXIpO1xuICAgICAgICB9XG4gICAgICAgIHdpbmRvdy5sb2NhbFN0b3JhZ2UucmVtb3ZlSXRlbShTRVNTSU9OX0xPQ0tfQ09OU1RBTlRTLlNUT1JBR0VfSVRFTV9QSU5HKTtcbiAgICAgICAgd2luZG93LmxvY2FsU3RvcmFnZS5yZW1vdmVJdGVtKFNFU1NJT05fTE9DS19DT05TVEFOVFMuU1RPUkFHRV9JVEVNX09XTkVSKTtcbiAgICAgICAgbG9ja1NlcnZpY2VyID0gbnVsbDtcbiAgICB9XG5cbiAgICAvLyBmaXJzdCBvZiBhbGwsIHN0YWtlIGEgY2xhaW0gZm9yIHRoZSBsb2NrLiBUaGlzIHRlbGxzIGFueW9uZSBlbHNlIGhvbGRpbmcgdGhlIGxvY2sgdGhhdCB3ZSB3YW50IGl0LlxuICAgIHdpbmRvdy5sb2NhbFN0b3JhZ2Uuc2V0SXRlbShTRVNTSU9OX0xPQ0tfQ09OU1RBTlRTLlNUT1JBR0VfSVRFTV9DTEFJTUFOVCwgc2Vzc2lvbklkZW50aWZpZXIpO1xuXG4gICAgLy8gbm93LCB3YWl0IGZvciB0aGUgbG9jayB0byBiZSBmcmVlLlxuICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1jb25zdGFudC1jb25kaXRpb25cbiAgICB3aGlsZSAodHJ1ZSkge1xuICAgICAgICBjb25zdCByZW1haW5pbmcgPSBjaGVja0xvY2soKTtcblxuICAgICAgICBpZiAocmVtYWluaW5nID09IDApIHtcbiAgICAgICAgICAgIC8vIG9rLCB0aGUgbG9jayBpcyBmcmVlLCBhbmQgbm9ib2R5IGVsc2UgaGFzIHN0YWtlZCBhIG1vcmUgcmVjZW50IGNsYWltLlxuICAgICAgICAgICAgYnJlYWs7XG4gICAgICAgIH0gZWxzZSBpZiAocmVtYWluaW5nIDwgMCkge1xuICAgICAgICAgICAgLy8gc29tZW9uZSBlbHNlIHN0YWtlZCBhIGNsYWltIGZvciB0aGUgbG9jazsgd2UgYmFpbCBvdXQuXG4gICAgICAgICAgICBhd2FpdCBvbk5ld0luc3RhbmNlKCk7XG4gICAgICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICAgIH1cblxuICAgICAgICAvLyBzb21lb25lIGVsc2UgaGFzIHRoZSBsb2NrLlxuICAgICAgICAvLyB3YWl0IGZvciBlaXRoZXIgdGhlIHBpbmcgdG8gZXhwaXJlLCBvciBhIHN0b3JhZ2UgZXZlbnQuXG4gICAgICAgIGxldCBvblN0b3JhZ2VVcGRhdGU6IChldmVudDogU3RvcmFnZUV2ZW50KSA9PiB2b2lkO1xuXG4gICAgICAgIGNvbnN0IHN0b3JhZ2VVcGRhdGVQcm9taXNlID0gbmV3IFByb21pc2UoKHJlc29sdmUpID0+IHtcbiAgICAgICAgICAgIG9uU3RvcmFnZVVwZGF0ZSA9IChldmVudDogU3RvcmFnZUV2ZW50KSA9PiB7XG4gICAgICAgICAgICAgICAgaWYgKFxuICAgICAgICAgICAgICAgICAgICBldmVudC5rZXkgPT09IFNFU1NJT05fTE9DS19DT05TVEFOVFMuU1RPUkFHRV9JVEVNX1BJTkcgfHxcbiAgICAgICAgICAgICAgICAgICAgZXZlbnQua2V5ID09PSBTRVNTSU9OX0xPQ0tfQ09OU1RBTlRTLlNUT1JBR0VfSVRFTV9DTEFJTUFOVFxuICAgICAgICAgICAgICAgIClcbiAgICAgICAgICAgICAgICAgICAgcmVzb2x2ZShldmVudCk7XG4gICAgICAgICAgICB9O1xuICAgICAgICB9KTtcblxuICAgICAgICBjb25zdCBzbGVlcFByb21pc2UgPSBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4ge1xuICAgICAgICAgICAgc2V0VGltZW91dChyZXNvbHZlLCByZW1haW5pbmcsIHVuZGVmaW5lZCk7XG4gICAgICAgIH0pO1xuXG4gICAgICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKFwic3RvcmFnZVwiLCBvblN0b3JhZ2VVcGRhdGUhKTtcbiAgICAgICAgYXdhaXQgUHJvbWlzZS5yYWNlKFtzbGVlcFByb21pc2UsIHN0b3JhZ2VVcGRhdGVQcm9taXNlXSk7XG4gICAgICAgIHdpbmRvdy5yZW1vdmVFdmVudExpc3RlbmVyKFwic3RvcmFnZVwiLCBvblN0b3JhZ2VVcGRhdGUhKTtcbiAgICB9XG5cbiAgICAvLyBJZiB3ZSBnZXQgaGVyZSwgd2Uga25vdyB0aGUgbG9jayBpcyBvdXJzIGZvciB0aGUgdGFraW5nLlxuXG4gICAgLy8gQ1JJVElDQUwgU0VDVElPTlxuICAgIC8vXG4gICAgLy8gVGhlIGZvbGxvd2luZyBjb2RlLCB1cCB0byB0aGUgZW5kIG9mIHRoZSBmdW5jdGlvbiwgbXVzdCBhbGwgYmUgc3luY2hyb25vdXMgKGllLCBubyBgYXdhaXRgIGNhbGxzKSwgdG8gZW5zdXJlIHRoYXRcbiAgICAvLyB3ZSBnZXQgb3VyIGxpc3RlbmVycyBpbiBwbGFjZSBhbmQgYWxsIHRoZSB3cml0ZXMgdG8gbG9jYWxTdG9yYWdlIGRvbmUgYmVmb3JlIG90aGVyIHRhYnMgcnVuIGFnYWluLlxuXG4gICAgLy8gY2xhaW0gdGhlIGxvY2ssIGFuZCBraWNrIG9mZiBhIGJhY2tncm91bmQgcHJvY2VzcyB0byBzZXJ2aWNlIGl0IGV2ZXJ5IDUgc2Vjb25kc1xuICAgIHNlcnZpY2VMb2NrKCk7XG4gICAgbG9ja1NlcnZpY2VyID0gd2luZG93LnNldEludGVydmFsKHNlcnZpY2VMb2NrLCA1MDAwKTtcblxuICAgIC8vIE5vdyBhZGQgYSBsaXN0ZW5lciBmb3Igb3RoZXIgY2xhaW1hbnRzIHRvIHRoZSBsb2NrLlxuICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKFwic3RvcmFnZVwiLCBvblN0b3JhZ2VFdmVudCk7XG5cbiAgICAvLyBhbHNvIGFkZCBhIGxpc3RlbmVyIHRvIGNsZWFyIG91ciBjbGFpbXMgd2hlbiBvdXIgdGFiIGNsb3NlcyBvciBuYXZpZ2F0ZXMgYXdheVxuICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKFwicGFnZWhpZGVcIiwgb25QYWdlaGlkZUV2ZW50KTtcblxuICAgIC8vIFRoZSBwYWdlaGlkZSBldmVudCBpcyBjYWxsZWQgdW5yZWxpYWJseSBvbiBGaXJlZm94LCBzbyBhZGRpdGlvbmFsbHkgYWRkIGFuIHVubG9hZCBoYW5kbGVyLlxuICAgIC8vIGh0dHBzOi8vYnVnemlsbGEubW96aWxsYS5vcmcvc2hvd19idWcuY2dpP2lkPTE4NTQ0OTJcbiAgICB3aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcihcInVubG9hZFwiLCBvblBhZ2VoaWRlRXZlbnQpO1xuXG4gICAgcmV0dXJuIHRydWU7XG59XG4iXSwibWFwcGluZ3MiOiI7Ozs7Ozs7O0FBUUEsSUFBQUEsT0FBQSxHQUFBQyxPQUFB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