420 lines
66 KiB
JavaScript
420 lines
66 KiB
JavaScript
|
"use strict";
|
||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||
|
};
|
||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
const debug_1 = __importDefault(require("debug"));
|
||
|
const time_limit_promise_1 = __importDefault(require("time-limit-promise"));
|
||
|
const events_1 = require("events");
|
||
|
const mustache_1 = __importDefault(require("mustache"));
|
||
|
const lodash_1 = require("lodash");
|
||
|
const parse_user_agent_1 = require("../../utils/parse-user-agent");
|
||
|
const read_file_relative_1 = require("read-file-relative");
|
||
|
const promisify_event_1 = __importDefault(require("promisify-event"));
|
||
|
const nanoid_1 = require("nanoid");
|
||
|
const command_1 = __importDefault(require("./command"));
|
||
|
const status_1 = __importDefault(require("./status"));
|
||
|
const heartbeat_status_1 = __importDefault(require("./heartbeat-status"));
|
||
|
const runtime_1 = require("../../errors/runtime");
|
||
|
const types_1 = require("../../errors/types");
|
||
|
const warning_log_1 = __importDefault(require("../../notifications/warning-log"));
|
||
|
const service_routes_1 = __importDefault(require("./service-routes"));
|
||
|
const browser_connection_timeouts_1 = require("../../utils/browser-connection-timeouts");
|
||
|
const tracker_1 = __importDefault(require("./tracker"));
|
||
|
const getBrowserConnectionDebugScope = (id) => `testcafe:browser:connection:${id}`;
|
||
|
const IDLE_PAGE_TEMPLATE = (0, read_file_relative_1.readSync)('../../client/browser/idle-page/index.html.mustache');
|
||
|
class BrowserConnection extends events_1.EventEmitter {
|
||
|
constructor(gateway, browserInfo, permanent, disableMultipleWindows = false, proxyless = false, messageBus) {
|
||
|
super();
|
||
|
this._currentTestRun = null;
|
||
|
this.url = '';
|
||
|
this.idleUrl = '';
|
||
|
this.forcedIdleUrl = '';
|
||
|
this.initScriptUrl = '';
|
||
|
this.heartbeatUrl = '';
|
||
|
this.statusUrl = '';
|
||
|
this.activeWindowIdUrl = '';
|
||
|
this.closeWindowUrl = '';
|
||
|
this.statusDoneUrl = '';
|
||
|
this.heartbeatRelativeUrl = '';
|
||
|
this.statusRelativeUrl = '';
|
||
|
this.statusDoneRelativeUrl = '';
|
||
|
this.idleRelativeUrl = '';
|
||
|
this.openFileProtocolRelativeUrl = '';
|
||
|
this.openFileProtocolUrl = '';
|
||
|
this.dispatchProxylessEventRelativeUrl = '';
|
||
|
this.osInfo = null;
|
||
|
this.HEARTBEAT_TIMEOUT = browser_connection_timeouts_1.HEARTBEAT_TIMEOUT;
|
||
|
this.BROWSER_CLOSE_TIMEOUT = browser_connection_timeouts_1.BROWSER_CLOSE_TIMEOUT;
|
||
|
this.BROWSER_RESTART_TIMEOUT = browser_connection_timeouts_1.BROWSER_RESTART_TIMEOUT;
|
||
|
this.id = BrowserConnection._generateId();
|
||
|
this.jobQueue = [];
|
||
|
this.initScriptsQueue = [];
|
||
|
this.browserConnectionGateway = gateway;
|
||
|
this.disconnectionPromise = null;
|
||
|
this.testRunAborted = false;
|
||
|
this.warningLog = new warning_log_1.default(null, warning_log_1.default.createAddWarningCallback(messageBus));
|
||
|
this.debugLogger = (0, debug_1.default)(getBrowserConnectionDebugScope(this.id));
|
||
|
if (messageBus)
|
||
|
this.messageBus = messageBus;
|
||
|
this.browserInfo = browserInfo;
|
||
|
this.browserInfo.userAgentProviderMetaInfo = '';
|
||
|
this.provider = browserInfo.provider;
|
||
|
this.permanent = permanent;
|
||
|
this.status = status_1.default.uninitialized;
|
||
|
this.idle = true;
|
||
|
this.heartbeatTimeout = null;
|
||
|
this.pendingTestRunInfo = null;
|
||
|
this.disableMultipleWindows = disableMultipleWindows;
|
||
|
this.proxyless = proxyless;
|
||
|
this._buildCommunicationUrls(gateway.proxy);
|
||
|
this._setEventHandlers();
|
||
|
tracker_1.default.add(this);
|
||
|
this.previousActiveWindowId = null;
|
||
|
this.browserConnectionGateway.startServingConnection(this);
|
||
|
// NOTE: Give a caller time to assign event listeners
|
||
|
process.nextTick(() => this._runBrowser());
|
||
|
}
|
||
|
_buildCommunicationUrls(proxy) {
|
||
|
this.url = proxy.resolveRelativeServiceUrl(`${service_routes_1.default.connect}/${this.id}`);
|
||
|
this.forcedIdleUrl = proxy.resolveRelativeServiceUrl(`${service_routes_1.default.idleForced}/${this.id}`);
|
||
|
this.initScriptUrl = proxy.resolveRelativeServiceUrl(`${service_routes_1.default.initScript}/${this.id}`);
|
||
|
this.heartbeatRelativeUrl = `${service_routes_1.default.heartbeat}/${this.id}`;
|
||
|
this.statusRelativeUrl = `${service_routes_1.default.status}/${this.id}`;
|
||
|
this.statusDoneRelativeUrl = `${service_routes_1.default.statusDone}/${this.id}`;
|
||
|
this.idleRelativeUrl = `${service_routes_1.default.idle}/${this.id}`;
|
||
|
this.activeWindowIdUrl = `${service_routes_1.default.activeWindowId}/${this.id}`;
|
||
|
this.closeWindowUrl = `${service_routes_1.default.closeWindow}/${this.id}`;
|
||
|
this.openFileProtocolRelativeUrl = `${service_routes_1.default.openFileProtocol}/${this.id}`;
|
||
|
this.dispatchProxylessEventRelativeUrl = `${service_routes_1.default.dispatchProxylessEvent}/${this.id}`;
|
||
|
this.idleUrl = proxy.resolveRelativeServiceUrl(this.idleRelativeUrl);
|
||
|
this.heartbeatUrl = proxy.resolveRelativeServiceUrl(this.heartbeatRelativeUrl);
|
||
|
this.statusUrl = proxy.resolveRelativeServiceUrl(this.statusRelativeUrl);
|
||
|
this.statusDoneUrl = proxy.resolveRelativeServiceUrl(this.statusDoneRelativeUrl);
|
||
|
this.openFileProtocolUrl = proxy.resolveRelativeServiceUrl(this.openFileProtocolRelativeUrl);
|
||
|
}
|
||
|
set messageBus(messageBus) {
|
||
|
this._messageBus = messageBus;
|
||
|
this.warningLog.callback = warning_log_1.default.createAddWarningCallback(this._messageBus);
|
||
|
if (messageBus) {
|
||
|
messageBus.on('test-run-start', testRun => {
|
||
|
if (testRun.browserConnection.id === this.id)
|
||
|
this._currentTestRun = testRun;
|
||
|
});
|
||
|
}
|
||
|
this.emit('message-bus-initialized', messageBus);
|
||
|
}
|
||
|
get messageBus() {
|
||
|
return this._messageBus;
|
||
|
}
|
||
|
_setEventHandlers() {
|
||
|
this.on('error', e => {
|
||
|
this.debugLogger(e);
|
||
|
this._forceIdle();
|
||
|
this.close();
|
||
|
});
|
||
|
for (const name in status_1.default) {
|
||
|
const status = status_1.default[name];
|
||
|
this.on(status, () => {
|
||
|
this.debugLogger(`status changed to '${status}'`);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
static _generateId() {
|
||
|
return (0, nanoid_1.nanoid)(7);
|
||
|
}
|
||
|
_getAdditionalBrowserOptions() {
|
||
|
const options = {
|
||
|
disableMultipleWindows: this.disableMultipleWindows,
|
||
|
};
|
||
|
if (this.proxyless) {
|
||
|
options.proxyless = {
|
||
|
serviceDomains: [
|
||
|
this.browserConnectionGateway.proxy.server1Info.domain,
|
||
|
this.browserConnectionGateway.proxy.server2Info.domain,
|
||
|
],
|
||
|
developmentMode: this.browserConnectionGateway.proxy.options.developmentMode,
|
||
|
};
|
||
|
}
|
||
|
return options;
|
||
|
}
|
||
|
async _runBrowser() {
|
||
|
try {
|
||
|
const additionalOptions = this._getAdditionalBrowserOptions();
|
||
|
await this.provider.openBrowser(this.id, this.url, this.browserInfo.browserOption, additionalOptions);
|
||
|
if (this.status !== status_1.default.ready)
|
||
|
await (0, promisify_event_1.default)(this, 'ready');
|
||
|
this.status = status_1.default.opened;
|
||
|
this.emit('opened');
|
||
|
}
|
||
|
catch (err) {
|
||
|
this.emit('error', new runtime_1.GeneralError(types_1.RUNTIME_ERRORS.unableToOpenBrowser, this.browserInfo.providerName + ':' + this.browserInfo.browserName, err.stack));
|
||
|
}
|
||
|
}
|
||
|
async _closeBrowser(data = {}) {
|
||
|
if (!this.idle)
|
||
|
await (0, promisify_event_1.default)(this, 'idle');
|
||
|
try {
|
||
|
await this.provider.closeBrowser(this.id, data);
|
||
|
}
|
||
|
catch (err) {
|
||
|
// NOTE: A warning would be really nice here, but it can't be done while log is stored in a task.
|
||
|
this.debugLogger(err);
|
||
|
}
|
||
|
}
|
||
|
_forceIdle() {
|
||
|
if (!this.idle) {
|
||
|
this.idle = true;
|
||
|
this.emit('idle');
|
||
|
}
|
||
|
}
|
||
|
_createBrowserDisconnectedError() {
|
||
|
return new runtime_1.GeneralError(types_1.RUNTIME_ERRORS.browserDisconnected, this.userAgent);
|
||
|
}
|
||
|
_waitForHeartbeat() {
|
||
|
this.heartbeatTimeout = setTimeout(() => {
|
||
|
const err = this._createBrowserDisconnectedError();
|
||
|
this.status = status_1.default.disconnected;
|
||
|
this.testRunAborted = true;
|
||
|
this.emit('disconnected', err);
|
||
|
this._restartBrowserOnDisconnect(err);
|
||
|
}, this.HEARTBEAT_TIMEOUT);
|
||
|
}
|
||
|
async _getTestRunInfo(needPopNext) {
|
||
|
if (needPopNext || !this.pendingTestRunInfo)
|
||
|
this.pendingTestRunInfo = await this._popNextTestRunInfo();
|
||
|
return this.pendingTestRunInfo;
|
||
|
}
|
||
|
async _popNextTestRunInfo() {
|
||
|
while (this.hasQueuedJobs && !this.currentJob.hasQueuedTestRuns)
|
||
|
this.jobQueue.shift();
|
||
|
return this.hasQueuedJobs ? await this.currentJob.popNextTestRunInfo(this) : null;
|
||
|
}
|
||
|
getCurrentTestRun() {
|
||
|
return this._currentTestRun;
|
||
|
}
|
||
|
static getById(id) {
|
||
|
return tracker_1.default.activeBrowserConnections[id] || null;
|
||
|
}
|
||
|
async _restartBrowser() {
|
||
|
this.status = status_1.default.uninitialized;
|
||
|
this._forceIdle();
|
||
|
let resolveTimeout = null;
|
||
|
let isTimeoutExpired = false;
|
||
|
let timeout = null;
|
||
|
const restartPromise = (0, time_limit_promise_1.default)(this._closeBrowser({ isRestarting: true }), this.BROWSER_CLOSE_TIMEOUT, { rejectWith: new runtime_1.TimeoutError() })
|
||
|
.catch(err => this.debugLogger(err))
|
||
|
.then(() => this._runBrowser());
|
||
|
const timeoutPromise = new Promise(resolve => {
|
||
|
resolveTimeout = resolve;
|
||
|
timeout = setTimeout(() => {
|
||
|
isTimeoutExpired = true;
|
||
|
resolve();
|
||
|
}, this.BROWSER_RESTART_TIMEOUT);
|
||
|
});
|
||
|
return Promise.race([restartPromise, timeoutPromise])
|
||
|
.then(() => {
|
||
|
clearTimeout(timeout);
|
||
|
if (isTimeoutExpired)
|
||
|
this.emit('error', this._createBrowserDisconnectedError());
|
||
|
else
|
||
|
resolveTimeout();
|
||
|
});
|
||
|
}
|
||
|
_restartBrowserOnDisconnect(err) {
|
||
|
let resolveFn = null;
|
||
|
let rejectFn = null;
|
||
|
this.disconnectionPromise = new Promise((resolve, reject) => {
|
||
|
resolveFn = resolve;
|
||
|
rejectFn = () => {
|
||
|
reject(err);
|
||
|
};
|
||
|
setTimeout(() => {
|
||
|
rejectFn();
|
||
|
});
|
||
|
})
|
||
|
.then(() => {
|
||
|
return this._restartBrowser();
|
||
|
})
|
||
|
.catch(e => {
|
||
|
this.emit('error', e);
|
||
|
});
|
||
|
this.disconnectionPromise.resolve = resolveFn;
|
||
|
this.disconnectionPromise.reject = rejectFn;
|
||
|
}
|
||
|
async getDefaultBrowserInitTimeout() {
|
||
|
const isLocalBrowser = await this.provider.isLocalBrowser(this.id, this.browserInfo.browserName);
|
||
|
return isLocalBrowser ? browser_connection_timeouts_1.LOCAL_BROWSER_INIT_TIMEOUT : browser_connection_timeouts_1.REMOTE_BROWSER_INIT_TIMEOUT;
|
||
|
}
|
||
|
async processDisconnection(disconnectionThresholdExceeded) {
|
||
|
const { resolve, reject } = this.disconnectionPromise;
|
||
|
if (disconnectionThresholdExceeded)
|
||
|
reject();
|
||
|
else
|
||
|
resolve();
|
||
|
}
|
||
|
addWarning(message, ...args) {
|
||
|
if (this.currentJob)
|
||
|
this.currentJob.warningLog.addWarning(message, ...args);
|
||
|
else
|
||
|
this.warningLog.addWarning(message, ...args);
|
||
|
}
|
||
|
_appendToPrettyUserAgent(str) {
|
||
|
this.browserInfo.parsedUserAgent.prettyUserAgent += ` (${str})`;
|
||
|
}
|
||
|
_moveWarningLogToJob(job) {
|
||
|
job.warningLog.copyFrom(this.warningLog);
|
||
|
this.warningLog.clear();
|
||
|
}
|
||
|
setProviderMetaInfo(str, options) {
|
||
|
const appendToUserAgent = options === null || options === void 0 ? void 0 : options.appendToUserAgent;
|
||
|
if (appendToUserAgent) {
|
||
|
// NOTE:
|
||
|
// change prettyUserAgent only when connection already was established
|
||
|
if (this.isReady())
|
||
|
this._appendToPrettyUserAgent(str);
|
||
|
else
|
||
|
this.on('ready', () => this._appendToPrettyUserAgent(str));
|
||
|
return;
|
||
|
}
|
||
|
this.browserInfo.userAgentProviderMetaInfo = str;
|
||
|
}
|
||
|
get userAgent() {
|
||
|
let userAgent = this.browserInfo.parsedUserAgent.prettyUserAgent;
|
||
|
if (this.browserInfo.userAgentProviderMetaInfo)
|
||
|
userAgent += ` (${this.browserInfo.userAgentProviderMetaInfo})`;
|
||
|
return userAgent;
|
||
|
}
|
||
|
get connectionInfo() {
|
||
|
if (!this.osInfo)
|
||
|
return this.userAgent;
|
||
|
const { name, version } = this.browserInfo.parsedUserAgent;
|
||
|
let connectionInfo = (0, parse_user_agent_1.calculatePrettyUserAgent)({ name, version }, this.osInfo);
|
||
|
const metaInfo = this.browserInfo.userAgentProviderMetaInfo || (0, parse_user_agent_1.extractMetaInfo)(this.browserInfo.parsedUserAgent.prettyUserAgent);
|
||
|
if (metaInfo)
|
||
|
connectionInfo += ` (${metaInfo})`;
|
||
|
return connectionInfo;
|
||
|
}
|
||
|
get retryTestPages() {
|
||
|
return this.browserConnectionGateway.retryTestPages;
|
||
|
}
|
||
|
get hasQueuedJobs() {
|
||
|
return !!this.jobQueue.length;
|
||
|
}
|
||
|
get currentJob() {
|
||
|
return this.jobQueue[0];
|
||
|
}
|
||
|
// API
|
||
|
runInitScript(code) {
|
||
|
return new Promise(resolve => this.initScriptsQueue.push({ code, resolve }));
|
||
|
}
|
||
|
addJob(job) {
|
||
|
this.jobQueue.push(job);
|
||
|
this._moveWarningLogToJob(job);
|
||
|
}
|
||
|
removeJob(job) {
|
||
|
(0, lodash_1.pull)(this.jobQueue, job);
|
||
|
}
|
||
|
async close() {
|
||
|
if (this.status === status_1.default.closing || this.status === status_1.default.closed)
|
||
|
return;
|
||
|
this.status = status_1.default.closing;
|
||
|
this.emit(status_1.default.closing);
|
||
|
await this._closeBrowser();
|
||
|
this.browserConnectionGateway.stopServingConnection(this);
|
||
|
if (this.heartbeatTimeout)
|
||
|
clearTimeout(this.heartbeatTimeout);
|
||
|
tracker_1.default.remove(this);
|
||
|
this.status = status_1.default.closed;
|
||
|
this.emit(status_1.default.closed);
|
||
|
}
|
||
|
async establish(userAgent) {
|
||
|
this.status = status_1.default.ready;
|
||
|
this.browserInfo.parsedUserAgent = (0, parse_user_agent_1.parseUserAgent)(userAgent);
|
||
|
this.osInfo = await this.provider.getOSInfo(this.id);
|
||
|
this._waitForHeartbeat();
|
||
|
this.emit('ready');
|
||
|
}
|
||
|
heartbeat() {
|
||
|
if (this.heartbeatTimeout)
|
||
|
clearTimeout(this.heartbeatTimeout);
|
||
|
this._waitForHeartbeat();
|
||
|
return {
|
||
|
code: this.status === status_1.default.closing ? heartbeat_status_1.default.closing : heartbeat_status_1.default.ok,
|
||
|
url: this.status === status_1.default.closing ? this.idleUrl : '',
|
||
|
};
|
||
|
}
|
||
|
renderIdlePage() {
|
||
|
return mustache_1.default.render(IDLE_PAGE_TEMPLATE, {
|
||
|
userAgent: this.connectionInfo,
|
||
|
statusUrl: this.statusUrl,
|
||
|
heartbeatUrl: this.heartbeatUrl,
|
||
|
initScriptUrl: this.initScriptUrl,
|
||
|
openFileProtocolUrl: this.openFileProtocolUrl,
|
||
|
retryTestPages: !!this.browserConnectionGateway.retryTestPages,
|
||
|
proxyless: this.proxyless,
|
||
|
});
|
||
|
}
|
||
|
getInitScript() {
|
||
|
const initScriptPromise = this.initScriptsQueue[0];
|
||
|
return { code: initScriptPromise ? initScriptPromise.code : null };
|
||
|
}
|
||
|
handleInitScriptResult(data) {
|
||
|
const initScriptPromise = this.initScriptsQueue.shift();
|
||
|
if (initScriptPromise)
|
||
|
initScriptPromise.resolve(JSON.parse(data));
|
||
|
}
|
||
|
isHeadlessBrowser() {
|
||
|
return this.provider.isHeadlessBrowser(this.id);
|
||
|
}
|
||
|
async reportJobResult(status, data) {
|
||
|
await this.provider.reportJobResult(this.id, status, data);
|
||
|
}
|
||
|
async getStatus(isTestDone) {
|
||
|
if (!this.idle && !isTestDone) {
|
||
|
this.idle = true;
|
||
|
this.emit('idle');
|
||
|
}
|
||
|
if (this.status === status_1.default.opened) {
|
||
|
const nextTestRunInfo = await this._getTestRunInfo(isTestDone || this.testRunAborted);
|
||
|
this.testRunAborted = false;
|
||
|
if (nextTestRunInfo) {
|
||
|
this.idle = false;
|
||
|
return {
|
||
|
cmd: command_1.default.run,
|
||
|
testRunId: nextTestRunInfo.testRunId,
|
||
|
url: nextTestRunInfo.url,
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
return {
|
||
|
cmd: command_1.default.idle,
|
||
|
url: this.idleUrl,
|
||
|
testRunId: null,
|
||
|
};
|
||
|
}
|
||
|
get activeWindowId() {
|
||
|
return this.provider.getActiveWindowId(this.id);
|
||
|
}
|
||
|
set activeWindowId(val) {
|
||
|
this.previousActiveWindowId = this.activeWindowId;
|
||
|
this.provider.setActiveWindowId(this.id, val);
|
||
|
}
|
||
|
async openFileProtocol(url) {
|
||
|
return this.provider.openFileProtocol(this.id, url);
|
||
|
}
|
||
|
async dispatchProxylessEvent(type, options) {
|
||
|
return this.provider.dispatchProxylessEvent(this.id, type, options);
|
||
|
}
|
||
|
async canUseDefaultWindowActions() {
|
||
|
return this.provider.canUseDefaultWindowActions(this.id);
|
||
|
}
|
||
|
isReady() {
|
||
|
return this.status === status_1.default.ready ||
|
||
|
this.status === status_1.default.opened ||
|
||
|
this.status === status_1.default.closing;
|
||
|
}
|
||
|
}
|
||
|
exports.default = BrowserConnection;
|
||
|
module.exports = exports.default;
|
||
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYnJvd3Nlci9jb25uZWN0aW9uL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBQUEsa0RBQTBCO0FBQzFCLDRFQUEyQztBQUMzQyxtQ0FBc0M7QUFDdEMsd0RBQWdDO0FBQ2hDLG1DQUF3QztBQUN4QyxtRUFLc0M7QUFDdEMsMkRBQXNEO0FBQ3RELHNFQUE2QztBQUM3QyxtQ0FBZ0M7QUFDaEMsd0RBQWdDO0FBQ2hDLHNEQUErQztBQUMvQywwRUFBaUQ7QUFDakQsa0RBQWtFO0FBQ2xFLDhDQUFvRDtBQUdwRCxrRkFBeUQ7QUFHekQsc0VBQThDO0FBQzlDLHlGQU1pRDtBQUVqRCx3REFBaUQ7QUFRakQsTUFBTSw4QkFBOEIsR0FBRyxDQUFDLEVBQVUsRUFBVSxFQUFFLENBQUMsK0JBQStCLEVBQUUsRUFBRSxDQUFDO0FBRW5HLE1BQU0sa0JBQWtCLEdBQUcsSUFBQSw2QkFBSSxFQUFDLG9EQUFvRCxDQUFDLENBQUM7QUE2Q3RGLE1BQXFCLGlCQUFrQixTQUFRLHFCQUFZO0lBNkN2RCxZQUNJLE9BQWlDLEVBQ2pDLFdBQXdCLEVBQ3hCLFNBQWtCLEVBQ2xCLHNCQUFzQixHQUFHLEtBQUssRUFDOUIsU0FBUyxHQUFHLEtBQUssRUFDakIsVUFBdUI7UUFDdkIsS0FBSyxFQUFFLENBQUM7UUEzQ0osb0JBQWUsR0FBbUIsSUFBSSxDQUFDO1FBU3hDLFFBQUcsR0FBRyxFQUFFLENBQUM7UUFDVCxZQUFPLEdBQUcsRUFBRSxDQUFDO1FBQ1osa0JBQWEsR0FBRyxFQUFFLENBQUM7UUFDbkIsa0JBQWEsR0FBRyxFQUFFLENBQUM7UUFDcEIsaUJBQVksR0FBRyxFQUFFLENBQUM7UUFDbEIsY0FBUyxHQUFHLEVBQUUsQ0FBQztRQUNmLHNCQUFpQixHQUFHLEVBQUUsQ0FBQztRQUN2QixtQkFBYyxHQUFHLEVBQUUsQ0FBQztRQUNwQixrQkFBYSxHQUFHLEVBQUUs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
|