matrix-react-sdk
Version:
SDK for matrix.org using React
262 lines (240 loc) • 36.2 kB
JavaScript
;
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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