UNPKG

matrix-react-sdk

Version:
595 lines (577 loc) 91.9 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.HiddenImagePlaceholder = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireWildcard(require("react")); var _reactBlurhash = require("react-blurhash"); var _classnames = _interopRequireDefault(require("classnames")); var _reactTransitionGroup = require("react-transition-group"); var _logger = require("matrix-js-sdk/src/logger"); var _matrix = require("matrix-js-sdk/src/matrix"); var _compoundWeb = require("@vector-im/compound-web"); var _MFileBody = _interopRequireDefault(require("./MFileBody")); var _Modal = _interopRequireDefault(require("../../../Modal")); var _languageHandler = require("../../../languageHandler"); var _SettingsStore = _interopRequireDefault(require("../../../settings/SettingsStore")); var _Spinner = _interopRequireDefault(require("../elements/Spinner")); var _Media = require("../../../customisations/Media"); var _imageMedia = require("../../../utils/image-media"); var _ImageView = _interopRequireDefault(require("../elements/ImageView")); var _ImageSize = require("../../../settings/enums/ImageSize"); var _MatrixClientPeg = require("../../../MatrixClientPeg"); var _RoomContext = _interopRequireWildcard(require("../../../contexts/RoomContext")); var _Image = require("../../../utils/Image"); var _FileUtils = require("../../../utils/FileUtils"); var _connection = require("../../../utils/connection"); var _MediaProcessingError = _interopRequireDefault(require("./shared/MediaProcessingError")); var _DecryptFile = require("../../../utils/DecryptFile"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } /* Copyright 2024 New Vector Ltd. Copyright 2015-2021 The Matrix.org Foundation C.I.C. Copyright 2018, 2019 Michael Telatynski <7t3chguy@gmail.com> SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ var Placeholder = /*#__PURE__*/function (Placeholder) { Placeholder[Placeholder["NoImage"] = 0] = "NoImage"; Placeholder[Placeholder["Blurhash"] = 1] = "Blurhash"; return Placeholder; }(Placeholder || {}); class MImageBody extends _react.default.Component { constructor(...args) { super(...args); (0, _defineProperty2.default)(this, "unmounted", true); (0, _defineProperty2.default)(this, "image", /*#__PURE__*/(0, _react.createRef)()); (0, _defineProperty2.default)(this, "timeout", void 0); (0, _defineProperty2.default)(this, "sizeWatcher", void 0); (0, _defineProperty2.default)(this, "state", { contentUrl: null, thumbUrl: null, imgError: false, imgLoaded: false, hover: false, showImage: _SettingsStore.default.getValue("showImages"), placeholder: Placeholder.NoImage }); (0, _defineProperty2.default)(this, "onClick", ev => { if (ev.button === 0 && !ev.metaKey) { ev.preventDefault(); if (!this.state.showImage) { this.showImage(); return; } const content = this.props.mxEvent.getContent(); const httpUrl = this.state.contentUrl; if (!httpUrl) return; const params = { src: httpUrl, name: content.body && content.body.length > 0 ? content.body : (0, _languageHandler._t)("common|attachment"), mxEvent: this.props.mxEvent, permalinkCreator: this.props.permalinkCreator }; if (content.info) { params.width = content.info.w; params.height = content.info.h; params.fileSize = content.info.size; } if (this.image.current) { const clientRect = this.image.current.getBoundingClientRect(); params.thumbnailInfo = { width: clientRect.width, height: clientRect.height, positionX: clientRect.x, positionY: clientRect.y }; } _Modal.default.createDialog(_ImageView.default, params, "mx_Dialog_lightbox", undefined, true); } }); (0, _defineProperty2.default)(this, "onImageEnter", e => { this.setState({ hover: true }); if (!this.state.contentUrl || !this.state.showImage || !this.state.isAnimated || _SettingsStore.default.getValue("autoplayGifs")) { return; } const imgElement = e.currentTarget; imgElement.src = this.state.contentUrl; }); (0, _defineProperty2.default)(this, "onImageLeave", e => { this.setState({ hover: false }); const url = this.state.thumbUrl ?? this.state.contentUrl; if (!url || !this.state.showImage || !this.state.isAnimated || _SettingsStore.default.getValue("autoplayGifs")) { return; } const imgElement = e.currentTarget; imgElement.src = url; }); (0, _defineProperty2.default)(this, "reconnectedListener", (0, _connection.createReconnectedListener)(() => { _MatrixClientPeg.MatrixClientPeg.get()?.off(_matrix.ClientEvent.Sync, this.reconnectedListener); this.setState({ imgError: false }); })); (0, _defineProperty2.default)(this, "onImageError", () => { // If the thumbnail failed to load then try again using the contentUrl if (this.state.thumbUrl) { this.setState({ thumbUrl: null }); return; } this.clearBlurhashTimeout(); this.setState({ imgError: true }); _MatrixClientPeg.MatrixClientPeg.safeGet().on(_matrix.ClientEvent.Sync, this.reconnectedListener); }); (0, _defineProperty2.default)(this, "onImageLoad", () => { this.clearBlurhashTimeout(); this.props.onHeightChanged?.(); let loadedImageDimensions; if (this.image.current) { const { naturalWidth, naturalHeight } = this.image.current; // this is only used as a fallback in case content.info.w/h is missing loadedImageDimensions = { naturalWidth, naturalHeight }; } this.setState({ imgLoaded: true, loadedImageDimensions }); }); } showImage() { localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true"); this.setState({ showImage: true }); this.downloadImage(); } getContentUrl() { // During export, the content url will point to the MSC, which will later point to a local url if (this.props.forExport) return this.media.srcMxc; return this.media.srcHttp; } get media() { return (0, _Media.mediaFromContent)(this.props.mxEvent.getContent()); } getThumbUrl() { // FIXME: we let images grow as wide as you like, rather than capped to 800x600. // So either we need to support custom timeline widths here, or reimpose the cap, otherwise the // thumbnail resolution will be unnecessarily reduced. // custom timeline widths seems preferable. const thumbWidth = 800; const thumbHeight = 600; const content = this.props.mxEvent.getContent(); const media = (0, _Media.mediaFromContent)(content); const info = content.info; if (info?.mimetype === "image/svg+xml" && media.hasThumbnail) { // Special-case to return clientside sender-generated thumbnails for SVGs, if any, // given we deliberately don't thumbnail them serverside to prevent billion lol attacks and similar. return media.getThumbnailHttp(thumbWidth, thumbHeight, "scale"); } // we try to download the correct resolution for hi-res images (like retina screenshots). // Synapse only supports 800x600 thumbnails for now though, // so we'll need to download the original image for this to work well for now. // First, let's try a few cases that let us avoid downloading the original, including: // - When displaying a GIF, we always want to thumbnail so that we can // properly respect the user's GIF autoplay setting (which relies on // thumbnailing to produce the static preview image) // - On a low DPI device, always thumbnail to save bandwidth // - If there's no sizing info in the event, default to thumbnail if (this.state.isAnimated || window.devicePixelRatio === 1.0 || !info || !info.w || !info.h || !info.size) { return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight); } // We should only request thumbnails if the image is bigger than 800x600 (or 1600x1200 on retina) otherwise // the image in the timeline will just end up resampled and de-retina'd for no good reason. // Ideally the server would pre-gen 1600x1200 thumbnails in order to provide retina thumbnails, // but we don't do this currently in synapse for fear of disk space. // As a compromise, let's switch to non-retina thumbnails only if the original image is both // physically too large and going to be massive to load in the timeline (e.g. >1MB). const isLargerThanThumbnail = info.w > thumbWidth || info.h > thumbHeight; const isLargeFileSize = info.size > 1 * 1024 * 1024; // 1mb if (isLargeFileSize && isLargerThanThumbnail) { // image is too large physically and byte-wise to clutter our timeline so, // we ask for a thumbnail, despite knowing that it will be max 800x600 // despite us being retina (as synapse doesn't do 1600x1200 thumbs yet). return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight); } // download the original image otherwise, so we can scale it client side to take pixelRatio into account. return media.srcHttp; } async downloadImage() { if (this.state.contentUrl) return; // already downloaded let thumbUrl; let contentUrl; if (this.props.mediaEventHelper?.media.isEncrypted) { try { [contentUrl, thumbUrl] = await Promise.all([this.props.mediaEventHelper.sourceUrl.value, this.props.mediaEventHelper.thumbnailUrl.value]); } catch (error) { if (this.unmounted) return; if (error instanceof _DecryptFile.DecryptError) { _logger.logger.error("Unable to decrypt attachment: ", error); } else if (error instanceof _DecryptFile.DownloadError) { _logger.logger.error("Unable to download attachment to decrypt it: ", error); } else { _logger.logger.error("Error encountered when downloading encrypted attachment: ", error); } // Set a placeholder image when we can't decrypt the image. this.setState({ error }); return; } } else { thumbUrl = this.getThumbUrl(); contentUrl = this.getContentUrl(); } const content = this.props.mxEvent.getContent(); let isAnimated = (0, _Image.mayBeAnimated)(content.info?.mimetype); // If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server // because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail. if (isAnimated && !_SettingsStore.default.getValue("autoplayGifs")) { if (!thumbUrl || !content?.info?.thumbnail_info || (0, _Image.mayBeAnimated)(content.info.thumbnail_info.mimetype)) { const img = document.createElement("img"); const loadPromise = new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; }); img.crossOrigin = "Anonymous"; // CORS allow canvas access img.src = contentUrl ?? ""; try { await loadPromise; } catch (error) { _logger.logger.error("Unable to download attachment: ", error); this.setState({ error: error }); return; } try { const blob = await this.props.mediaEventHelper.sourceBlob.value; if (!(await (0, _Image.blobIsAnimated)(content.info?.mimetype, blob))) { isAnimated = false; } if (isAnimated) { const thumb = await (0, _imageMedia.createThumbnail)(img, img.width, img.height, content.info?.mimetype ?? "image/jpeg", false); thumbUrl = URL.createObjectURL(thumb.thumbnail); } } catch (error) { // This is a non-critical failure, do not surface the error or bail the method here _logger.logger.warn("Unable to generate thumbnail for animated image: ", error); } } } if (this.unmounted) return; this.setState({ contentUrl, thumbUrl, isAnimated }); } clearBlurhashTimeout() { if (this.timeout) { clearTimeout(this.timeout); this.timeout = undefined; } } componentDidMount() { this.unmounted = false; const showImage = this.state.showImage || localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true"; if (showImage) { // noinspection JSIgnoredPromiseFromCall this.downloadImage(); this.setState({ showImage: true }); } // else don't download anything because we don't want to display anything. // Add a 150ms timer for blurhash to first appear. if (this.props.mxEvent.getContent().info?.[_imageMedia.BLURHASH_FIELD]) { this.clearBlurhashTimeout(); this.timeout = window.setTimeout(() => { if (!this.state.imgLoaded || !this.state.imgError) { this.setState({ placeholder: Placeholder.Blurhash }); } }, 150); } this.sizeWatcher = _SettingsStore.default.watchSetting("Images.size", null, () => { this.forceUpdate(); // we don't really have a reliable thing to update, so just update the whole thing }); } componentWillUnmount() { this.unmounted = true; _MatrixClientPeg.MatrixClientPeg.get()?.off(_matrix.ClientEvent.Sync, this.reconnectedListener); this.clearBlurhashTimeout(); if (this.sizeWatcher) _SettingsStore.default.unwatchSetting(this.sizeWatcher); if (this.state.isAnimated && this.state.thumbUrl) { URL.revokeObjectURL(this.state.thumbUrl); } } getBanner(content) { // Hide it for the threads list & the file panel where we show it as text anyway. if ([_RoomContext.TimelineRenderingType.ThreadsList, _RoomContext.TimelineRenderingType.File].includes(this.context.timelineRenderingType)) { return null; } return /*#__PURE__*/_react.default.createElement("span", { className: "mx_MImageBody_banner" }, (0, _FileUtils.presentableTextForFile)(content, (0, _languageHandler._t)("common|image"), true, true)); } messageContent(contentUrl, thumbUrl, content, forcedHeight) { if (!thumbUrl) thumbUrl = contentUrl; // fallback // magic number // edge case for this not to be set by conditions below let infoWidth = 500; let infoHeight = 500; let infoSvg = false; if (content.info?.w && content.info?.h) { infoWidth = content.info.w; infoHeight = content.info.h; infoSvg = content.info.mimetype === "image/svg+xml"; } else if (thumbUrl && contentUrl) { // Whilst the image loads, display nothing. We also don't display a blurhash image // because we don't really know what size of image we'll end up with. // // Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`. // // By doing this, the image "pops" into the timeline, but is still restricted // by the same width and height logic below. if (!this.state.loadedImageDimensions) { let imageElement; if (!this.state.showImage) { imageElement = /*#__PURE__*/_react.default.createElement(HiddenImagePlaceholder, null); } else { imageElement = /*#__PURE__*/_react.default.createElement("img", { style: { display: "none" }, src: thumbUrl, ref: this.image, alt: content.body, onError: this.onImageError, onLoad: this.onImageLoad }); } return this.wrapImage(contentUrl, imageElement); } infoWidth = this.state.loadedImageDimensions.naturalWidth; infoHeight = this.state.loadedImageDimensions.naturalHeight; } // The maximum size of the thumbnail as it is rendered as an <img>, // accounting for any height constraints const { w: maxWidth, h: maxHeight } = (0, _ImageSize.suggestedSize)(_SettingsStore.default.getValue("Images.size"), { w: infoWidth, h: infoHeight }, forcedHeight ?? this.props.maxImageHeight); let img; let placeholder; let gifLabel; if (!this.props.forExport && !this.state.imgLoaded) { const classes = (0, _classnames.default)("mx_MImageBody_placeholder", { "mx_MImageBody_placeholder--blurhash": this.props.mxEvent.getContent().info?.[_imageMedia.BLURHASH_FIELD] }); placeholder = /*#__PURE__*/_react.default.createElement("div", { className: classes }, this.getPlaceholder(maxWidth, maxHeight)); } let showPlaceholder = Boolean(placeholder); if (thumbUrl && !this.state.imgError) { // Restrict the width of the thumbnail here, otherwise it will fill the container // which has the same width as the timeline // mx_MImageBody_thumbnail resizes img to exactly container size img = /*#__PURE__*/_react.default.createElement("img", { className: "mx_MImageBody_thumbnail", src: thumbUrl, ref: this.image, alt: content.body, onError: this.onImageError, onLoad: this.onImageLoad, onMouseEnter: this.onImageEnter, onMouseLeave: this.onImageLeave }); } if (!this.state.showImage) { img = /*#__PURE__*/_react.default.createElement(HiddenImagePlaceholder, { maxWidth: maxWidth }); showPlaceholder = false; // because we're hiding the image, so don't show the placeholder. } if (this.state.isAnimated && !_SettingsStore.default.getValue("autoplayGifs") && !this.state.hover) { // XXX: Arguably we may want a different label when the animated image is WEBP and not GIF gifLabel = /*#__PURE__*/_react.default.createElement("p", { className: "mx_MImageBody_gifLabel" }, "GIF"); } let banner; if (this.state.showImage && this.state.hover) { banner = this.getBanner(content); } // many SVGs don't have an intrinsic size if used in <img> elements. // due to this we have to set our desired width directly. // this way if the image is forced to shrink, the height adapts appropriately. const sizing = infoSvg ? { maxHeight, maxWidth, width: maxWidth } : { maxHeight, maxWidth }; if (!this.props.forExport) { placeholder = /*#__PURE__*/_react.default.createElement(_reactTransitionGroup.SwitchTransition, { mode: "out-in" }, /*#__PURE__*/_react.default.createElement(_reactTransitionGroup.CSSTransition, { classNames: "mx_rtg--fade", key: `img-${showPlaceholder}`, timeout: 300 }, showPlaceholder ? placeholder : /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null) /* Transition always expects a child */)); } const tooltipProps = this.getTooltipProps(); let thumbnail = /*#__PURE__*/_react.default.createElement("div", { className: "mx_MImageBody_thumbnail_container", style: { maxHeight, maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }, tabIndex: tooltipProps ? 0 : undefined }, placeholder, /*#__PURE__*/_react.default.createElement("div", { style: sizing }, img, gifLabel, banner), !this.props.forExport && !this.state.imgLoaded && /*#__PURE__*/_react.default.createElement("div", { style: { height: maxHeight, width: maxWidth } })); if (tooltipProps) { // We specify isTriggerInteractive=true and make the div interactive manually as a workaround for // https://github.com/element-hq/compound/issues/294 thumbnail = /*#__PURE__*/_react.default.createElement(_compoundWeb.Tooltip, (0, _extends2.default)({}, tooltipProps, { isTriggerInteractive: true }), thumbnail); } return this.wrapImage(contentUrl, thumbnail); } // Overridden by MStickerBody wrapImage(contentUrl, children) { if (contentUrl) { return /*#__PURE__*/_react.default.createElement("a", { href: contentUrl, target: this.props.forExport ? "_blank" : undefined, onClick: this.onClick }, children); } else if (!this.state.showImage) { return /*#__PURE__*/_react.default.createElement("div", { role: "button", onClick: this.onClick }, children); } return children; } // Overridden by MStickerBody getPlaceholder(width, height) { const blurhash = this.props.mxEvent.getContent().info?.[_imageMedia.BLURHASH_FIELD]; if (blurhash) { if (this.state.placeholder === Placeholder.NoImage) { return null; } else if (this.state.placeholder === Placeholder.Blurhash) { return /*#__PURE__*/_react.default.createElement(_reactBlurhash.Blurhash, { className: "mx_Blurhash", hash: blurhash, width: width, height: height }); } } return /*#__PURE__*/_react.default.createElement(_Spinner.default, { w: 32, h: 32 }); } // Overridden by MStickerBody getTooltipProps() { return null; } // Overridden by MStickerBody getFileBody() { if (this.props.forExport) return null; /* * In the room timeline or the thread context we don't need the download * link as the message action bar will fulfill that */ const hasMessageActionBar = this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Room || this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Pinned || this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Search || this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Thread || this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.ThreadsList; if (!hasMessageActionBar) { return /*#__PURE__*/_react.default.createElement(_MFileBody.default, (0, _extends2.default)({}, this.props, { showGenericPlaceholder: false })); } } render() { const content = this.props.mxEvent.getContent(); if (this.state.error) { let errorText = (0, _languageHandler._t)("timeline|m.image|error"); if (this.state.error instanceof _DecryptFile.DecryptError) { errorText = (0, _languageHandler._t)("timeline|m.image|error_decrypting"); } else if (this.state.error instanceof _DecryptFile.DownloadError) { errorText = (0, _languageHandler._t)("timeline|m.image|error_downloading"); } return /*#__PURE__*/_react.default.createElement(_MediaProcessingError.default, { className: "mx_MImageBody" }, errorText); } let contentUrl = this.state.contentUrl; let thumbUrl; if (this.props.forExport) { contentUrl = this.props.mxEvent.getContent().url ?? this.props.mxEvent.getContent().file?.url; thumbUrl = contentUrl; } else if (this.state.isAnimated && _SettingsStore.default.getValue("autoplayGifs")) { thumbUrl = contentUrl; } else { thumbUrl = this.state.thumbUrl ?? this.state.contentUrl; } const thumbnail = this.messageContent(contentUrl, thumbUrl, content); const fileBody = this.getFileBody(); return /*#__PURE__*/_react.default.createElement("div", { className: "mx_MImageBody" }, thumbnail, fileBody); } } exports.default = MImageBody; (0, _defineProperty2.default)(MImageBody, "contextType", _RoomContext.default); class HiddenImagePlaceholder extends _react.default.PureComponent { render() { const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null; let className = "mx_HiddenImagePlaceholder"; if (this.props.hover) className += " mx_HiddenImagePlaceholder_hover"; return /*#__PURE__*/_react.default.createElement("div", { className: className, style: { maxWidth: `min(100%, ${maxWidth}px)` } }, /*#__PURE__*/_react.default.createElement("div", { className: "mx_HiddenImagePlaceholder_button" }, /*#__PURE__*/_react.default.createElement("span", { className: "mx_HiddenImagePlaceholder_eye" }), /*#__PURE__*/_react.default.createElement("span", null, (0, _languageHandler._t)("timeline|m.image|show_image")))); } } exports.HiddenImagePlaceholder = HiddenImagePlaceholder; //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJfcmVhY3QiLCJfaW50ZXJvcFJlcXVpcmVXaWxkY2FyZCIsInJlcXVpcmUiLCJfcmVhY3RCbHVyaGFzaCIsIl9jbGFzc25hbWVzIiwiX2ludGVyb3BSZXF1aXJlRGVmYXVsdCIsIl9yZWFjdFRyYW5zaXRpb25Hcm91cCIsIl9sb2dnZXIiLCJfbWF0cml4IiwiX2NvbXBvdW5kV2ViIiwiX01GaWxlQm9keSIsIl9Nb2RhbCIsIl9sYW5ndWFnZUhhbmRsZXIiLCJfU2V0dGluZ3NTdG9yZSIsIl9TcGlubmVyIiwiX01lZGlhIiwiX2ltYWdlTWVkaWEiLCJfSW1hZ2VWaWV3IiwiX0ltYWdlU2l6ZSIsIl9NYXRyaXhDbGllbnRQZWciLCJfUm9vbUNvbnRleHQiLCJfSW1hZ2UiLCJfRmlsZVV0aWxzIiwiX2Nvbm5lY3Rpb24iLCJfTWVkaWFQcm9jZXNzaW5nRXJyb3IiLCJfRGVjcnlwdEZpbGUiLCJfZ2V0UmVxdWlyZVdpbGRjYXJkQ2FjaGUiLCJlIiwiV2Vha01hcCIsInIiLCJ0IiwiX19lc01vZHVsZSIsImRlZmF1bHQiLCJoYXMiLCJnZXQiLCJuIiwiX19wcm90b19fIiwiYSIsIk9iamVjdCIsImRlZmluZVByb3BlcnR5IiwiZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yIiwidSIsImhhc093blByb3BlcnR5IiwiY2FsbCIsImkiLCJzZXQiLCJQbGFjZWhvbGRlciIsIk1JbWFnZUJvZHkiLCJSZWFjdCIsIkNvbXBvbmVudCIsImNvbnN0cnVjdG9yIiwiYXJncyIsIl9kZWZpbmVQcm9wZXJ0eTIiLCJjcmVhdGVSZWYiLCJjb250ZW50VXJsIiwidGh1bWJVcmwiLCJpbWdFcnJvciIsImltZ0xvYWRlZCIsImhvdmVyIiwic2hvd0ltYWdlIiwiU2V0dGluZ3NTdG9yZSIsImdldFZhbHVlIiwicGxhY2Vob2xkZXIiLCJOb0ltYWdlIiwiZXYiLCJidXR0b24iLCJtZXRhS2V5IiwicHJldmVudERlZmF1bHQiLCJzdGF0ZSIsImNvbnRlbnQiLCJwcm9wcyIsIm14RXZlbnQiLCJnZXRDb250ZW50IiwiaHR0cFVybCIsInBhcmFtcyIsInNyYyIsIm5hbWUiLCJib2R5IiwibGVuZ3RoIiwiX3QiLCJwZXJtYWxpbmtDcmVhdG9yIiwiaW5mbyIsIndpZHRoIiwidyIsImhlaWdodCIsImgiLCJmaWxlU2l6ZSIsInNpemUiLCJpbWFnZSIsImN1cnJlbnQiLCJjbGllbnRSZWN0IiwiZ2V0Qm91bmRpbmdDbGllbnRSZWN0IiwidGh1bWJuYWlsSW5mbyIsInBvc2l0aW9uWCIsIngiLCJwb3NpdGlvblkiLCJ5IiwiTW9kYWwiLCJjcmVhdGVEaWFsb2ciLCJJbWFnZVZpZXciLCJ1bmRlZmluZWQiLCJzZXRTdGF0ZSIsImlzQW5pbWF0ZWQiLCJpbWdFbGVtZW50IiwiY3VycmVudFRhcmdldCIsInVybCIsImNyZWF0ZVJlY29ubmVjdGVkTGlzdGVuZXIiLCJNYXRyaXhDbGllbnRQZWciLCJvZmYiLCJDbGllbnRFdmVudCIsIlN5bmMiLCJyZWNvbm5lY3RlZExpc3RlbmVyIiwiY2xlYXJCbHVyaGFzaFRpbWVvdXQiLCJzYWZlR2V0Iiwib24iLCJvbkhlaWdodENoYW5nZWQiLCJsb2FkZWRJbWFnZURpbWVuc2lvbnMiLCJuYXR1cmFsV2lkdGgiLCJuYXR1cmFsSGVpZ2h0IiwibG9jYWxTdG9yYWdlIiwic2V0SXRlbSIsImdldElkIiwiZG93bmxvYWRJbWFnZSIsImdldENvbnRlbnRVcmwiLCJmb3JFeHBvcnQiLCJtZWRpYSIsInNyY014YyIsInNyY0h0dHAiLCJtZWRpYUZyb21Db250ZW50IiwiZ2V0VGh1bWJVcmwiLCJ0aHVtYldpZHRoIiwidGh1bWJIZWlnaHQiLCJtaW1ldHlwZSIsImhhc1RodW1ibmFpbCIsImdldFRodW1ibmFpbEh0dHAiLCJ3aW5kb3ciLCJkZXZpY2VQaXhlbFJhdGlvIiwiZ2V0VGh1bWJuYWlsT2ZTb3VyY2VIdHRwIiwiaXNMYXJnZXJUaGFuVGh1bWJuYWlsIiwiaXNMYXJnZUZpbGVTaXplIiwibWVkaWFFdmVudEhlbHBlciIsImlzRW5jcnlwdGVkIiwiUHJvbWlzZSIsImFsbCIsInNvdXJjZVVybCIsInZhbHVlIiwidGh1bWJuYWlsVXJsIiwiZXJyb3IiLCJ1bm1vdW50ZWQiLCJEZWNyeXB0RXJyb3IiLCJsb2dnZXIiLCJEb3dubG9hZEVycm9yIiwibWF5QmVBbmltYXRlZCIsInRodW1ibmFpbF9pbmZvIiwiaW1nIiwiZG9jdW1lbnQiLCJjcmVhdGVFbGVtZW50IiwibG9hZFByb21pc2UiLCJyZXNvbHZlIiwicmVqZWN0Iiwib25sb2FkIiwib25lcnJvciIsImNyb3NzT3JpZ2luIiwiYmxvYiIsInNvdXJjZUJsb2IiLCJibG9iSXNBbmltYXRlZCIsInRodW1iIiwiY3JlYXRlVGh1bWJuYWlsIiwiVVJMIiwiY3JlYXRlT2JqZWN0VVJMIiwidGh1bWJuYWlsIiwid2FybiIsInRpbWVvdXQiLCJjbGVhclRpbWVvdXQiLCJjb21wb25lbnREaWRNb3VudCIsImdldEl0ZW0iLCJCTFVSSEFTSF9GSUVMRCIsInNldFRpbWVvdXQiLCJCbHVyaGFzaCIsInNpemVXYXRjaGVyIiwid2F0Y2hTZXR0aW5nIiwiZm9yY2VVcGRhdGUiLCJjb21wb25lbnRXaWxsVW5tb3VudCIsInVud2F0Y2hTZXR0aW5nIiwicmV2b2tlT2JqZWN0VVJMIiwiZ2V0QmFubmVyIiwiVGltZWxpbmVSZW5kZXJpbmdUeXBlIiwiVGhyZWFkc0xpc3QiLCJGaWxlIiwiaW5jbHVkZXMiLCJjb250ZXh0IiwidGltZWxpbmVSZW5kZXJpbmdUeXBlIiwiY2xhc3NOYW1lIiwicHJlc2VudGFibGVUZXh0Rm9yRmlsZSIsIm1lc3NhZ2VDb250ZW50IiwiZm9yY2VkSGVpZ2h0IiwiaW5mb1dpZHRoIiwiaW5mb0hlaWdodCIsImluZm9TdmciLCJpbWFnZUVsZW1lbnQiLCJIaWRkZW5JbWFnZVBsYWNlaG9sZGVyIiwic3R5bGUiLCJkaXNwbGF5IiwicmVmIiwiYWx0Iiwib25FcnJvciIsIm9uSW1hZ2VFcnJvciIsIm9uTG9hZCIsIm9uSW1hZ2VMb2FkIiwid3JhcEltYWdlIiwibWF4V2lkdGgiLCJtYXhIZWlnaHQiLCJzdWdnZXN0ZWRJbWFnZVNpemUiLCJtYXhJbWFnZUhlaWdodCIsImdpZkxhYmVsIiwiY2xhc3NlcyIsImNsYXNzTmFtZXMiLCJnZXRQbGFjZWhvbGRlciIsInNob3dQbGFjZWhvbGRlciIsIkJvb2xlYW4iLCJvbk1vdXNlRW50ZXIiLCJvbkltYWdlRW50ZXIiLCJvbk1vdXNlTGVhdmUiLCJvbkltYWdlTGVhdmUiLCJiYW5uZXIiLCJzaXppbmciLCJTd2l0Y2hUcmFuc2l0aW9uIiwibW9kZSIsIkNTU1RyYW5zaXRpb24iLCJrZXkiLCJGcmFnbWVudCIsInRvb2x0aXBQcm9wcyIsImdldFRvb2x0aXBQcm9wcyIsImFzcGVjdFJhdGlvIiwidGFiSW5kZXgiLCJUb29sdGlwIiwiX2V4dGVuZHMyIiwiaXNUcmlnZ2VySW50ZXJhY3RpdmUiLCJjaGlsZHJlbiIsImhyZWYiLCJ0YXJnZXQiLCJvbkNsaWNrIiwicm9sZSIsImJsdXJoYXNoIiwiaGFzaCIsImdldEZpbGVCb2R5IiwiaGFzTWVzc2FnZUFjdGlvbkJhciIsIlJvb20iLCJQaW5uZWQiLCJTZWFyY2giLCJUaHJlYWQiLCJzaG93R2VuZXJpY1BsYWNlaG9sZGVyIiwicmVuZGVyIiwiZXJyb3JUZXh0IiwiZmlsZSIsImZpbGVCb2R5IiwiZXhwb3J0cyIsIlJvb21Db250ZXh0IiwiUHVyZUNvbXBvbmVudCJdLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9jb21wb25lbnRzL3ZpZXdzL21lc3NhZ2VzL01JbWFnZUJvZHkudHN4Il0sInNvdXJjZXNDb250ZW50IjpbIi8qXG5Db3B5cmlnaHQgMjAyNCBOZXcgVmVjdG9yIEx0ZC5cbkNvcHlyaWdodCAyMDE1LTIwMjEgVGhlIE1hdHJpeC5vcmcgRm91bmRhdGlvbiBDLkkuQy5cbkNvcHlyaWdodCAyMDE4LCAyMDE5IE1pY2hhZWwgVGVsYXR5bnNraSA8N3QzY2hndXlAZ21haWwuY29tPlxuXG5TUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQUdQTC0zLjAtb25seSBPUiBHUEwtMy4wLW9ubHlcblBsZWFzZSBzZWUgTElDRU5TRSBmaWxlcyBpbiB0aGUgcmVwb3NpdG9yeSByb290IGZvciBmdWxsIGRldGFpbHMuXG4qL1xuXG5pbXBvcnQgUmVhY3QsIHsgQ29tcG9uZW50UHJvcHMsIGNyZWF0ZVJlZiwgUmVhY3ROb2RlIH0gZnJvbSBcInJlYWN0XCI7XG5pbXBvcnQgeyBCbHVyaGFzaCB9IGZyb20gXCJyZWFjdC1ibHVyaGFzaFwiO1xuaW1wb3J0IGNsYXNzTmFtZXMgZnJvbSBcImNsYXNzbmFtZXNcIjtcbmltcG9ydCB7IENTU1RyYW5zaXRpb24sIFN3aXRjaFRyYW5zaXRpb24gfSBmcm9tIFwicmVhY3QtdHJhbnNpdGlvbi1ncm91cFwiO1xuaW1wb3J0IHsgbG9nZ2VyIH0gZnJvbSBcIm1hdHJpeC1qcy1zZGsvc3JjL2xvZ2dlclwiO1xuaW1wb3J0IHsgQ2xpZW50RXZlbnQgfSBmcm9tIFwibWF0cml4LWpzLXNkay9zcmMvbWF0cml4XCI7XG5pbXBvcnQgeyBJbWFnZUNvbnRlbnQgfSBmcm9tIFwibWF0cml4LWpzLXNkay9zcmMvdHlwZXNcIjtcbmltcG9ydCB7IFRvb2x0aXAgfSBmcm9tIFwiQHZlY3Rvci1pbS9jb21wb3VuZC13ZWJcIjtcblxuaW1wb3J0IE1GaWxlQm9keSBmcm9tIFwiLi9NRmlsZUJvZHlcIjtcbmltcG9ydCBNb2RhbCBmcm9tIFwiLi4vLi4vLi4vTW9kYWxcIjtcbmltcG9ydCB7IF90IH0gZnJvbSBcIi4uLy4uLy4uL2xhbmd1YWdlSGFuZGxlclwiO1xuaW1wb3J0IFNldHRpbmdzU3RvcmUgZnJvbSBcIi4uLy4uLy4uL3NldHRpbmdzL1NldHRpbmdzU3RvcmVcIjtcbmltcG9ydCBTcGlubmVyIGZyb20gXCIuLi9lbGVtZW50cy9TcGlubmVyXCI7XG5pbXBvcnQgeyBNZWRpYSwgbWVkaWFGcm9tQ29udGVudCB9IGZyb20gXCIuLi8uLi8uLi9jdXN0b21pc2F0aW9ucy9NZWRpYVwiO1xuaW1wb3J0IHsgQkxVUkhBU0hfRklFTEQsIGNyZWF0ZVRodW1ibmFpbCB9IGZyb20gXCIuLi8uLi8uLi91dGlscy9pbWFnZS1tZWRpYVwiO1xuaW1wb3J0IEltYWdlVmlldyBmcm9tIFwiLi4vZWxlbWVudHMvSW1hZ2VWaWV3XCI7XG5pbXBvcnQgeyBJQm9keVByb3BzIH0gZnJvbSBcIi4vSUJvZHlQcm9wc1wiO1xuaW1wb3J0IHsgSW1hZ2VTaXplLCBzdWdnZXN0ZWRTaXplIGFzIHN1Z2dlc3RlZEltYWdlU2l6ZSB9IGZyb20gXCIuLi8uLi8uLi9zZXR0aW5ncy9lbnVtcy9JbWFnZVNpemVcIjtcbmltcG9ydCB7IE1hdHJpeENsaWVudFBlZyB9IGZyb20gXCIuLi8uLi8uLi9NYXRyaXhDbGllbnRQZWdcIjtcbmltcG9ydCBSb29tQ29udGV4dCwgeyBUaW1lbGluZVJlbmRlcmluZ1R5cGUgfSBmcm9tIFwiLi4vLi4vLi4vY29udGV4dHMvUm9vbUNvbnRleHRcIjtcbmltcG9ydCB7IGJsb2JJc0FuaW1hdGVkLCBtYXlCZUFuaW1hdGVkIH0gZnJvbSBcIi4uLy4uLy4uL3V0aWxzL0ltYWdlXCI7XG5pbXBvcnQgeyBwcmVzZW50YWJsZVRleHRGb3JGaWxlIH0gZnJvbSBcIi4uLy4uLy4uL3V0aWxzL0ZpbGVVdGlsc1wiO1xuaW1wb3J0IHsgY3JlYXRlUmVjb25uZWN0ZWRMaXN0ZW5lciB9IGZyb20gXCIuLi8uLi8uLi91dGlscy9jb25uZWN0aW9uXCI7XG5pbXBvcnQgTWVkaWFQcm9jZXNzaW5nRXJyb3IgZnJvbSBcIi4vc2hhcmVkL01lZGlhUHJvY2Vzc2luZ0Vycm9yXCI7XG5pbXBvcnQgeyBEZWNyeXB0RXJyb3IsIERvd25sb2FkRXJyb3IgfSBmcm9tIFwiLi4vLi4vLi4vdXRpbHMvRGVjcnlwdEZpbGVcIjtcblxuZW51bSBQbGFjZWhvbGRlciB7XG4gICAgTm9JbWFnZSxcbiAgICBCbHVyaGFzaCxcbn1cblxuaW50ZXJmYWNlIElTdGF0ZSB7XG4gICAgY29udGVudFVybDogc3RyaW5nIHwgbnVsbDtcbiAgICB0aHVtYlVybDogc3RyaW5nIHwgbnVsbDtcbiAgICBpc0FuaW1hdGVkPzogYm9vbGVhbjtcbiAgICBlcnJvcj86IHVua25vd247XG4gICAgaW1nRXJyb3I6IGJvb2xlYW47XG4gICAgaW1nTG9hZGVkOiBib29sZWFuO1xuICAgIGxvYWRlZEltYWdlRGltZW5zaW9ucz86IHtcbiAgICAgICAgbmF0dXJhbFdpZHRoOiBudW1iZXI7XG4gICAgICAgIG5hdHVyYWxIZWlnaHQ6IG51bWJlcjtcbiAgICB9O1xuICAgIGhvdmVyOiBib29sZWFuO1xuICAgIHNob3dJbWFnZTogYm9vbGVhbjtcbiAgICBwbGFjZWhvbGRlcjogUGxhY2Vob2xkZXI7XG59XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIE1JbWFnZUJvZHkgZXh0ZW5kcyBSZWFjdC5Db21wb25lbnQ8SUJvZHlQcm9wcywgSVN0YXRlPiB7XG4gICAgcHVibGljIHN0YXRpYyBjb250ZXh0VHlwZSA9IFJvb21Db250ZXh0O1xuICAgIHB1YmxpYyBkZWNsYXJlIGNvbnRleHQ6IFJlYWN0LkNvbnRleHRUeXBlPHR5cGVvZiBSb29tQ29udGV4dD47XG5cbiAgICBwcml2YXRlIHVubW91bnRlZCA9IHRydWU7XG4gICAgcHJpdmF0ZSBpbWFnZSA9IGNyZWF0ZVJlZjxIVE1MSW1hZ2VFbGVtZW50PigpO1xuICAgIHByaXZhdGUgdGltZW91dD86IG51bWJlcjtcbiAgICBwcml2YXRlIHNpemVXYXRjaGVyPzogc3RyaW5nO1xuXG4gICAgcHVibGljIHN0YXRlOiBJU3RhdGUgPSB7XG4gICAgICAgIGNvbnRlbnRVcmw6IG51bGwsXG4gICAgICAgIHRodW1iVXJsOiBudWxsLFxuICAgICAgICBpbWdFcnJvcjogZmFsc2UsXG4gICAgICAgIGltZ0xvYWRlZDogZmFsc2UsXG4gICAgICAgIGhvdmVyOiBmYWxzZSxcbiAgICAgICAgc2hvd0ltYWdlOiBTZXR0aW5nc1N0b3JlLmdldFZhbHVlKFwic2hvd0ltYWdlc1wiKSxcbiAgICAgICAgcGxhY2Vob2xkZXI6IFBsYWNlaG9sZGVyLk5vSW1hZ2UsXG4gICAgfTtcblxuICAgIHByb3RlY3RlZCBzaG93SW1hZ2UoKTogdm9pZCB7XG4gICAgICAgIGxvY2FsU3RvcmFnZS5zZXRJdGVtKFwibXhfU2hvd0ltYWdlX1wiICsgdGhpcy5wcm9wcy5teEV2ZW50LmdldElkKCksIFwidHJ1ZVwiKTtcbiAgICAgICAgdGhpcy5zZXRTdGF0ZSh7IHNob3dJbWFnZTogdHJ1ZSB9KTtcbiAgICAgICAgdGhpcy5kb3dubG9hZEltYWdlKCk7XG4gICAgfVxuXG4gICAgcHJvdGVjdGVkIG9uQ2xpY2sgPSAoZXY6IFJlYWN0Lk1vdXNlRXZlbnQpOiB2b2lkID0+IHtcbiAgICAgICAgaWYgKGV2LmJ1dHRvbiA9PT0gMCAmJiAhZXYubWV0YUtleSkge1xuICAgICAgICAgICAgZXYucHJldmVudERlZmF1bHQoKTtcbiAgICAgICAgICAgIGlmICghdGhpcy5zdGF0ZS5zaG93SW1hZ2UpIHtcbiAgICAgICAgICAgICAgICB0aGlzLnNob3dJbWFnZSgpO1xuICAgICAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgY29uc3QgY29udGVudCA9IHRoaXMucHJvcHMubXhFdmVudC5nZXRDb250ZW50PEltYWdlQ29udGVudD4oKTtcbiAgICAgICAgICAgIGNvbnN0IGh0dHBVcmwgPSB0aGlzLnN0YXRlLmNvbnRlbnRVcmw7XG4gICAgICAgICAgICBpZiAoIWh0dHBVcmwpIHJldHVybjtcbiAgICAgICAgICAgIGNvbnN0IHBhcmFtczogT21pdDxDb21wb25lbnRQcm9wczx0eXBlb2YgSW1hZ2VWaWV3PiwgXCJvbkZpbmlzaGVkXCI+ID0ge1xuICAgICAgICAgICAgICAgIHNyYzogaHR0cFVybCxcbiAgICAgICAgICAgICAgICBuYW1lOiBjb250ZW50LmJvZHkgJiYgY29udGVudC5ib2R5Lmxlbmd0aCA+IDAgPyBjb250ZW50LmJvZHkgOiBfdChcImNvbW1vbnxhdHRhY2htZW50XCIpLFxuICAgICAgICAgICAgICAgIG14RXZlbnQ6IHRoaXMucHJvcHMubXhFdmVudCxcbiAgICAgICAgICAgICAgICBwZXJtYWxpbmtDcmVhdG9yOiB0aGlzLnByb3BzLnBlcm1hbGlua0NyZWF0b3IsXG4gICAgICAgICAgICB9O1xuXG4gICAgICAgICAgICBpZiAoY29udGVudC5pbmZvKSB7XG4gICAgICAgICAgICAgICAgcGFyYW1zLndpZHRoID0gY29udGVudC5pbmZvLnc7XG4gICAgICAgICAgICAgICAgcGFyYW1zLmhlaWdodCA9IGNvbnRlbnQuaW5mby5oO1xuICAgICAgICAgICAgICAgIHBhcmFtcy5maWxlU2l6ZSA9IGNvbnRlbnQuaW5mby5zaXplO1xuICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICBpZiAodGhpcy5pbWFnZS5jdXJyZW50KSB7XG4gICAgICAgICAgICAgICAgY29uc3QgY2xpZW50UmVjdCA9IHRoaXMuaW1hZ2UuY3VycmVudC5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKTtcblxuICAgICAgICAgICAgICAgIHBhcmFtcy50aHVtYm5haWxJbmZvID0ge1xuICAgICAgICAgICAgICAgICAgICB3aWR0aDogY2xpZW50UmVjdC53aWR0aCxcbiAgICAgICAgICAgICAgICAgICAgaGVpZ2h0OiBjbGllbnRSZWN0LmhlaWdodCxcbiAgICAgICAgICAgICAgICAgICAgcG9zaXRpb25YOiBjbGllbnRSZWN0LngsXG4gICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uWTogY2xpZW50UmVjdC55LFxuICAgICAgICAgICAgICAgIH07XG4gICAgICAgICAgICB9XG5cbiAgICAgICAgICAgIE1vZGFsLmNyZWF0ZURpYWxvZyhJbWFnZVZpZXcsIHBhcmFtcywgXCJteF9EaWFsb2dfbGlnaHRib3hcIiwgdW5kZWZpbmVkLCB0cnVlKTtcbiAgICAgICAgfVxuICAgIH07XG5cbiAgICBwcm90ZWN0ZWQgb25JbWFnZUVudGVyID0gKGU6IFJlYWN0Lk1vdXNlRXZlbnQ8SFRNTEltYWdlRWxlbWVudD4pOiB2b2lkID0+IHtcbiAgICAgICAgdGhpcy5zZXRTdGF0ZSh7IGhvdmVyOiB0cnVlIH0pO1xuXG4gICAgICAgIGlmIChcbiAgICAgICAgICAgICF0aGlzLnN0YXRlLmNvbnRlbnRVcmwgfHxcbiAgICAgICAgICAgICF0aGlzLnN0YXRlLnNob3dJbWFnZSB8fFxuICAgICAgICAgICAgIXRoaXMuc3RhdGUuaXNBbmltYXRlZCB8fFxuICAgICAgICAgICAgU2V0dGluZ3NTdG9yZS5nZXRWYWx1ZShcImF1dG9wbGF5R2lmc1wiKVxuICAgICAgICApIHtcbiAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgfVxuICAgICAgICBjb25zdCBpbWdFbGVtZW50ID0gZS5jdXJyZW50VGFyZ2V0O1xuICAgICAgICBpbWdFbGVtZW50LnNyYyA9IHRoaXMuc3RhdGUuY29udGVudFVybDtcbiAgICB9O1xuXG4gICAgcHJvdGVjdGVkIG9uSW1hZ2VMZWF2ZSA9IChlOiBSZWFjdC5Nb3VzZUV2ZW50PEhUTUxJbWFnZUVsZW1lbnQ+KTogdm9pZCA9PiB7XG4gICAgICAgIHRoaXMuc2V0U3RhdGUoeyBob3ZlcjogZmFsc2UgfSk7XG5cbiAgICAgICAgY29uc3QgdXJsID0gdGhpcy5zdGF0ZS50aHVtYlVybCA/PyB0aGlzLnN0YXRlLmNvbnRlbnRVcmw7XG4gICAgICAgIGlmICghdXJsIHx8ICF0aGlzLnN0YXRlLnNob3dJbWFnZSB8fCAhdGhpcy5zdGF0ZS5pc0FuaW1hdGVkIHx8IFNldHRpbmdzU3RvcmUuZ2V0VmFsdWUoXCJhdXRvcGxheUdpZnNcIikpIHtcbiAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgfVxuICAgICAgICBjb25zdCBpbWdFbGVtZW50ID0gZS5jdXJyZW50VGFyZ2V0O1xuICAgICAgICBpbWdFbGVtZW50LnNyYyA9IHVybDtcbiAgICB9O1xuXG4gICAgcHJpdmF0ZSByZWNvbm5lY3RlZExpc3RlbmVyID0gY3JlYXRlUmVjb25uZWN0ZWRMaXN0ZW5lcigoKTogdm9pZCA9PiB7XG4gICAgICAgIE1hdHJpeENsaWVudFBlZy5nZXQoKT8ub2ZmKENsaWVudEV2ZW50LlN5bmMsIHRoaXMucmVjb25uZWN0ZWRMaXN0ZW5lcik7XG4gICAgICAgIHRoaXMuc2V0U3RhdGUoeyBpbWdFcnJvcjogZmFsc2UgfSk7XG4gICAgfSk7XG5cbiAgICBwcml2YXRlIG9uSW1hZ2VFcnJvciA9ICgpOiB2b2lkID0+IHtcbiAgICAgICAgLy8gSWYgdGhlIHRodW1ibmFpbCBmYWlsZWQgdG8gbG9hZCB0aGVuIHRyeSBhZ2FpbiB1c2luZyB0aGUgY29udGVudFVybFxuICAgICAgICBpZiAodGhpcy5zdGF0ZS50aHVtYlVybCkge1xuICAgICAgICAgICAgdGhpcy5zZXRTdGF0ZSh7XG4gICAgICAgICAgICAgICAgdGh1bWJVcmw6IG51bGwsXG4gICAgICAgICAgICB9KTtcbiAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgfVxuXG4gICAgICAgIHRoaXMuY2xlYXJCbHVyaGFzaFRpbWVvdXQoKTtcbiAgICAgICAgdGhpcy5zZXRTdGF0ZSh7XG4gICAgICAgICAgICBpbWdFcnJvcjogdHJ1ZSxcbiAgICAgICAgfSk7XG4gICAgICAgIE1hdHJpeENsaWVudFBlZy5zYWZlR2V0KCkub24oQ2xpZW50RXZlbnQuU3luYywgdGhpcy5yZWNvbm5lY3RlZExpc3RlbmVyKTtcbiAgICB9O1xuXG4gICAgcHJpdmF0ZSBvbkltYWdlTG9hZCA9ICgpOiB2b2lkID0+IHtcbiAgICAgICAgdGhpcy5jbGVhckJsdXJoYXNoVGltZW91dCgpO1xuICAgICAgICB0aGlzLnByb3BzLm9uSGVpZ2h0Q2hhbmdlZD8uKCk7XG5cbiAgICAgICAgbGV0IGxvYWRlZEltYWdlRGltZW5zaW9uczogSVN0YXRlW1wibG9hZGVkSW1hZ2VEaW1lbnNpb25zXCJdO1xuXG4gICAgICAgIGlmICh0aGlzLmltYWdlLmN1cnJlbnQpIHtcbiAgICAgICAgICAgIGNvbnN0IHsgbmF0dXJhbFdpZHRoLCBuYXR1cmFsSGVpZ2h0IH0gPSB0aGlzLmltYWdlLmN1cnJlbnQ7XG4gICAgICAgICAgICAvLyB0aGlzIGlzIG9ubHkgdXNlZCBhcyBhIGZhbGxiYWNrIGluIGNhc2UgY29udGVudC5pbmZvLncvaCBpcyBtaXNzaW5nXG4gICAgICAgICAgICBsb2FkZWRJbWFnZURpbWVuc2lvbnMgPSB7IG5hdHVyYWxXaWR0aCwgbmF0dXJhbEhlaWdodCB9O1xuICAgICAgICB9XG4gICAgICAgIHRoaXMuc2V0U3RhdGUoeyBpbWdMb2FkZWQ6IHRydWUsIGxvYWRlZEltYWdlRGltZW5zaW9ucyB9KTtcbiAgICB9O1xuXG4gICAgcHJpdmF0ZSBnZXRDb250ZW50VXJsKCk6IHN0cmluZyB8IG51bGwge1xuICAgICAgICAvLyBEdXJpbmcgZXhwb3J0LCB0aGUgY29udGVudCB1cmwgd2lsbCBwb2ludCB0byB0aGUgTVNDLCB3aGljaCB3aWxsIGxhdGVyIHBvaW50IHRvIGEgbG9jYWwgdXJsXG4gICAgICAgIGlmICh0aGlzLnByb3BzLmZvckV4cG9ydCkgcmV0dXJuIHRoaXMubWVkaWEuc3JjTXhjO1xuICAgICAgICByZXR1cm4gdGhpcy5tZWRpYS5zcmNIdHRwO1xuICAgIH1cblxuICAgIHByaXZhdGUgZ2V0IG1lZGlhKCk6IE1lZGlhIHtcbiAgICAgICAgcmV0dXJuIG1lZGlhRnJvbUNvbnRlbnQodGhpcy5wcm9wcy5teEV2ZW50LmdldENvbnRlbnQoKSk7XG4gICAgfVxuXG4gICAgcHJpdmF0ZSBnZXRUaHVtYlVybCgpOiBzdHJpbmcgfCBudWxsIHtcbiAgICAgICAgLy8gRklYTUU6IHdlIGxldCBpbWFnZXMgZ3JvdyBhcyB3aWRlIGFzIHlvdSBsaWtlLCByYXRoZXIgdGhhbiBjYXBwZWQgdG8gODAweDYwMC5cbiAgICAgICAgLy8gU28gZWl0aGVyIHdlIG5lZWQgdG8gc3VwcG9ydCBjdXN0b20gdGltZWxpbmUgd2lkdGhzIGhlcmUsIG9yIHJlaW1wb3NlIHRoZSBjYXAsIG90aGVyd2lzZSB0aGVcbiAgICAgICAgLy8gdGh1bWJuYWlsIHJlc29sdXRpb24gd2lsbCBiZSB1bm5lY2Vzc2FyaWx5IHJlZHVjZWQuXG4gICAgICAgIC8vIGN1c3RvbSB0aW1lbGluZSB3aWR0aHMgc2VlbXMgcHJlZmVyYWJsZS5cbiAgICAgICAgY29uc3QgdGh1bWJXaWR0aCA9IDgwMDtcbiAgICAgICAgY29uc3QgdGh1bWJIZWlnaHQgPSA2MDA7XG5cbiAgICAgICAgY29uc3QgY29udGVudCA9IHRoaXMucHJvcHMubXhFdmVudC5nZXRDb250ZW50PEltYWdlQ29udGVudD4oKTtcbiAgICAgICAgY29uc3QgbWVkaWEgPSBtZWRpYUZyb21Db250ZW50KGNvbnRlbnQpO1xuICAgICAgICBjb25zdCBpbmZvID0gY29udGVudC5pbmZvO1xuXG4gICAgICAgIGlmIChpbmZvPy5taW1ldHlwZSA9PT0gXCJpbWFnZS9zdmcreG1sXCIgJiYgbWVkaWEuaGFzVGh1bWJuYWlsKSB7XG4gICAgICAgICAgICAvLyBTcGVjaWFsLWNhc2UgdG8gcmV0dXJuIGNsaWVudHNpZGUgc2VuZGVyLWdlbmVyYXRlZCB0aHVtYm5haWxzIGZvciBTVkdzLCBpZiBhbnksXG4gICAgICAgICAgICAvLyBnaXZlbiB3ZSBkZWxpYmVyYXRlbHkgZG9uJ3QgdGh1bWJuYWlsIHRoZW0gc2VydmVyc2lkZSB0byBwcmV2ZW50IGJpbGxpb24gbG9sIGF0dGFja3MgYW5kIHNpbWlsYXIuXG4gICAgICAgICAgICByZXR1cm4gbWVkaWEuZ2V0VGh1bWJuYWlsSHR0cCh0aHVtYldpZHRoLCB0aHVtYkhlaWdodCwgXCJzY2FsZVwiKTtcbiAgICAgICAgfVxuXG4gICAgICAgIC8vIHdlIHRyeSB0byBkb3dubG9hZCB0aGUgY29ycmVjdCByZXNvbHV0aW9uIGZvciBoaS1yZXMgaW1hZ2VzIChsaWtlIHJldGluYSBzY3JlZW5zaG90cykuXG4gICAgICAgIC8vIFN5bmFwc2Ugb25seSBzdXBwb3J0cyA4MDB4NjAwIHRodW1ibmFpbHMgZm9yIG5vdyB0aG91Z2gsXG4gICAgICAgIC8vIHNvIHdlJ2xsIG5lZWQgdG8gZG93bmxvYWQgdGhlIG9yaWdpbmFsIGltYWdlIGZvciB0aGlzIHRvIHdvcmsgIHdlbGwgZm9yIG5vdy5cbiAgICAgICAgLy8gRmlyc3QsIGxldCdzIHRyeSBhIGZldyBjYXNlcyB0aGF0IGxldCB1cyBhdm9pZCBkb3dubG9hZGluZyB0aGUgb3JpZ2luYWwsIGluY2x1ZGluZzpcbiAgICAgICAgLy8gICAtIFdoZW4gZGlzcGxheWluZyBhIEdJRiwgd2UgYWx3YXlzIHdhbnQgdG8gdGh1bWJuYWlsIHNvIHRoYXQgd2UgY2FuXG4gICAgICAgIC8vICAgICBwcm9wZXJseSByZXNwZWN0IHRoZSB1c2VyJ3MgR0lGIGF1dG9wbGF5IHNldHRpbmcgKHdoaWNoIHJlbGllcyBvblxuICAgICAgICAvLyAgICAgdGh1bWJuYWlsaW5nIHRvIHByb2R1Y2UgdGhlIHN0YXRpYyBwcmV2aWV3IGltYWdlKVxuICAgICAgICAvLyAgIC0gT24gYSBsb3cgRFBJIGRldmljZSwgYWx3YXlzIHRodW1ibmFpbCB0byBzYXZlIGJhbmR3aWR0aFxuICAgICAgICAvLyAgIC0gSWYgdGhlcmUncyBubyBzaXppbmcgaW5mbyBpbiB0aGUgZXZlbnQsIGRlZmF1bHQgdG8gdGh1bWJuYWlsXG4gICAgICAgIGlmICh0aGlzLnN0YXRlLmlzQW5pbWF0ZWQgfHwgd2luZG93LmRldmljZVBpeGVsUmF0aW8gPT09IDEuMCB8fCAhaW5mbyB8fCAhaW5mby53IHx8ICFpbmZvLmggfHwgIWluZm8uc2l6ZSkge1xuICAgICAgICAgICAgcmV0dXJuIG1lZGlhLmdldFRodW1ibmFpbE9mU291cmNlSHR0cCh0aHVtYldpZHRoLCB0aHVtYkhlaWdodCk7XG4gICAgICAgIH1cblxuICAgICAgICAvLyBXZSBzaG91bGQgb25seSByZXF1ZXN0IHRodW1ibmFpbHMgaWYgdGhlIGltYWdlIGlzIGJpZ2dlciB0aGFuIDgwMHg2MDAgKG9yIDE2MDB4MTIwMCBvbiByZXRpbmEpIG90aGVyd2lzZVxuICAgICAgICAvLyB0aGUgaW1hZ2UgaW4gdGhlIHRpbWVsaW5lIHdpbGwganVzdCBlbmQgdXAgcmVzYW1wbGVkIGFuZCBkZS1yZXRpbmEnZCBmb3Igbm8gZ29vZCByZWFzb24uXG4gICAgICAgIC8vIElkZWFsbHkgdGhlIHNlcnZlciB3b3VsZCBwcmUtZ2VuIDE2MDB4MTIwMCB0aHVtYm5haWxzIGluIG9yZGVyIHRvIHByb3ZpZGUgcmV0aW5hIHRodW1ibmFpbHMsXG4gICAgICAgIC8vIGJ1dCB3ZSBkb24ndCBkbyB0aGlzIGN1cnJlbnRseSBpbiBzeW5hcHNlIGZvciBmZWFyIG9mIGRpc2sgc3BhY2UuXG4gICAgICAgIC8vIEFzIGEgY29tcHJvbWlzZSwgbGV0J3Mgc3dpdGNoIHRvIG5vbi1yZXRpbmEgdGh1bWJuYWlscyBvbmx5IGlmIHRoZSBvcmlnaW5hbCBpbWFnZSBpcyBib3RoXG4gICAgICAgIC8vIHBoeXNpY2FsbHkgdG9vIGxhcmdlIGFuZCBnb2luZyB0byBiZSBtYXNzaXZlIHRvIGxvYWQgaW4gdGhlIHRpbWVsaW5lIChlLmcuID4xTUIpLlxuXG4gICAgICAgIGNvbnN0IGlzTGFyZ2VyVGhhblRodW1ibmFpbCA9IGluZm8udyA+IHRodW1iV2lkdGggfHwgaW5mby5oID4gdGh1bWJIZWlnaHQ7XG4gICAgICAgIGNvbnN0IGlzTGFyZ2VGaWxlU2l6ZSA9IGluZm8uc2l6ZSA+IDEgKiAxMDI0ICogMTAyNDsgLy8gMW1iXG5cbiAgICAgICAgaWYgKGlzTGFyZ2VGaWxlU2l6ZSAmJiBpc0xhcmdlclRoYW5UaHVtYm5haWwpIHtcbiAgICAgICAgICAgIC8vIGltYWdlIGlzIHRvbyBsYXJnZSBwaHlzaWNhbGx5IGFuZCBieXRlLXdpc2UgdG8gY2x1dHRlciBvdXIgdGltZWxpbmUgc28sXG4gICAgICAgICAgICAvLyB3ZSBhc2sgZm9yIGEgdGh1bWJuYWlsLCBkZXNwaXRlIGtub3dpbmcgdGhhdCBpdCB3aWxsIGJlIG1heCA4MDB4NjAwXG4gICAgICAgICAgICAvLyBkZXNwaXRlIHVzIGJlaW5nIHJldGluYSAoYXMgc3luYXBzZSBkb2Vzbid0IGRvIDE2MDB4MTIwMCB0aHVtYnMgeWV0KS5cbiAgICAgICAgICAgIHJldHVybiBtZWRpYS5nZXRUaHVtYm5haWxPZlNvdXJjZUh0dHAodGh1bWJXaWR0aCwgdGh1bWJIZWlnaHQpO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gZG93bmxvYWQgdGhlIG9yaWdpbmFsIGltYWdlIG90aGVyd2lzZSwgc28gd2UgY2FuIHNjYWxlIGl0IGNsaWVudCBzaWRlIHRvIHRha2UgcGl4ZWxSYXRpbyBpbnRvIGFjY291bnQuXG4gICAgICAgIHJldHVybiBtZWRpYS5zcmNIdHRwO1xuICAgIH1cblxuICAgIHByaXZhdGUgYXN5bmMgZG93bmxvYWRJbWFnZSgpOiBQcm9taXNlPHZvaWQ+IHtcbiAgICAgICAgaWYgKHRoaXMuc3RhdGUuY29udGVudFVybCkgcmV0dXJuOyAvLyBhbHJlYWR5IGRvd25sb2FkZWRcblxuICAgICAgICBsZXQgdGh1bWJVcmw6IHN0cmluZyB8IG51bGw7XG4gICAgICAgIGxldCBjb250ZW50VXJsOiBzdHJpbmcgfCBudWxsO1xuICAgICAgICBpZiAodGhpcy5wcm9wcy5tZWRpYUV2ZW50SGVscGVyPy5tZWRpYS5pc0VuY3J5cHRlZCkge1xuICAgICAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgICAgICBbY29udGVudFVybCwgdGh1bWJVcmxdID0gYXdhaXQgUHJvbWlzZS5hbGwoW1xuICAgICAgICAgICAgICAgICAgICB0aGlzLnByb3BzLm1lZGlhRXZlbnRIZWxwZXIuc291cmNlVXJsLnZhbHVlLFxuICAgICAgICAgICAgICAgICAgICB0aGlzLnByb3BzLm1lZGlhRXZlbnRIZWxwZXIudGh1bWJuYWlsVXJsLnZhbHVlLFxuICAgICAgICAgICAgICAgIF0pO1xuICAgICAgICAgICAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAgICAgICAgICAgICBpZiAodGhpcy51bm1vdW50ZWQpIHJldHVybjtcblxuICAgICAgICAgICAgICAgIGlmIChlcnJvciBpbnN0YW5jZW9mIERlY3J5cHRFcnJvcikge1xuICAgICAgICAgICAgICAgICAgICBsb2dnZXIuZXJyb3IoXCJVbmFibGUgdG8gZGVjcnlwdCBhdHRhY2htZW50OiBcIiwgZXJyb3IpO1xuICAgICAgICAgICAgICAgIH0gZWxzZSBpZiAoZXJyb3IgaW5zdGFuY2VvZiBEb3dubG9hZEVycm9yKSB7XG4gICAgICAgICAgICAgICAgICAgIGxvZ2dlci5lcnJvcihcIlVuYWJsZSB0byBkb3dubG9hZCBhdHRhY2htZW50IHRvIGRlY3J5cHQgaXQ6IFwiLCBlcnJvcik7XG4gICAgICAgICAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgICAgICAgICAgbG9nZ2VyLmVycm9yKFwiRXJyb3IgZW5jb3VudGVyZWQgd2hlbiBkb3dubG9hZGluZyBlbmNyeXB0ZWQgYXR0YWNobWVudDogXCIsIGVycm9yKTtcbiAgICAgICAgICAgICAgICB9XG5cbiAgICAgICAgICAgICAgICAvLyBTZXQgYSBwbGFjZWhvbGRlciBpbWFnZSB3aGVuIHdlIGNhbid0IGRlY3J5cHQgdGhlIGltYWdlLlxuICAgICAgICAgICAgICAgIHRoaXMuc2V0U3RhdGUoeyBlcnJvciB9KTtcbiAgICAgICAgICAgICAgICByZXR1cm47XG4gICAgICAgICAgICB9XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICB0aHVtYlVybCA9IHRoaXMuZ2V0VGh1bWJVcmwoKTtcbiAgICAgICAgICAgIGNvbnRlbnRVcmwgPSB0aGlzLmdldENvbnRlbnRVcmwoKTtcbiAgICAgICAgfVxuXG4gICAgICAgIGNvbnN0IGNvbnRlbnQgPSB0aGlzLnByb3BzLm14RXZlbnQuZ2V0Q29udGVudDxJbWFnZUNvbnRlbnQ+KCk7XG4gICAgICAgIGxldCBpc0FuaW1hdGVkID0gbWF5QmVBbmltYXRlZChjb250ZW50LmluZm8/Lm1pbWV0eXBlKTtcblxuICAgICAgICAvLyBJZiB0aGVyZSBpcyBubyBpbmNsdWRlZCBub24tYW5pbWF0ZWQgdGh1bWJuYWlsIHRoZW4gd2Ugd2lsbCBnZW5lcmF0ZSBvdXIgb3duLCB3ZSBjYW4ndCBkZXBlbmQgb24gdGhlIHNlcnZlclxuICAgICAgICAvLyBiZWNhdXNlIDEuIGVuY3J5cHRpb24gYW5kIDIuIHdlIGNhbid0IGFzayB0aGUgc2VydmVyIHNwZWNpZmljYWxseSBmb3IgYSBub24tYW5pbWF0ZWQgdGh1bWJuYWlsLlxuICAgICAgICBpZiAoaXNBbmltYXRlZCAmJiAhU2V0dGluZ3NTdG9yZS5nZXRWYWx1ZShcImF1dG9wbGF5R2lmc1wiKSkge1xuICAgICAgICAgICAgaWYgKCF0aHVtYlVybCB8fCAhY29udGVudD8uaW5mbz8udGh1bWJuYWlsX2luZm8gfHwgbWF5QmVBbmltYXRlZChjb250ZW50LmluZm8udGh1bWJuYWlsX2luZm8ubWltZXR5cGUpKSB7XG4gICAgICAgICAgICAgICAgY29uc3QgaW1nID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudChcImltZ1wiKTtcbiAgICAgICAgICAgICAgICBjb25zdCBsb2FkUHJvbWlzZSA9IG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgICAgICAgICAgICAgaW1nLm9ubG9hZCA9IHJlc29sdmU7XG4gICAgICAgICAgICAgICAgICAgIGltZy5vbmVycm9yID0gcmVqZWN0O1xuICAgICAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgICAgIGltZy5jcm9zc09yaWdpbiA9IFwiQW5vbnltb3VzXCI7IC8vIENPUlMgYWxsb3cgY2FudmFzIGFjY2Vzc1xuICAgICAgICAgICAgICAgIGltZy5zcmMgPSBjb250ZW50VXJsID8/IFwiXCI7XG5cbiAgICAgICAgICAgICAgICB0cnkge1xuICAgICAgICAgICAgICAgICAgICBhd2FpdCBsb2FkUHJvbWlzZTtcbiAgICAgICAgICAgICAgICB9IGNhdGNoIChlcnJvcikge1xuICAgICAgICAgICAgICAgICAgICBsb2dnZXIuZXJyb3IoXCJVbmFibGUgdG8gZG93bmxvYWQgYXR0YWNobWVudDogXCIsIGVycm9yKTtcbiAgICAgICAgICAgICAgICAgICAgdGhpcy5zZXRTdGF0ZSh7IGVycm9yOiBlcnJvciBhcyBFcnJvciB9KTtcbiAgICAgICAgICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICAgICAgICAgIGNvbnN0IGJsb2IgPSBhd2FpdCB0aGlzLnByb3BzLm1lZGlhRXZlbnRIZWxwZXIhLnNvdXJjZUJsb2IudmFsdWU7XG4gICAgICAgICAgICAgICAgICAgIGlmICghKGF3YWl0IGJsb2JJc0FuaW1hdGVkKGNvbnRlbnQuaW5mbz8ubWltZXR5cGUsIGJsb2IpKSkge1xuICAgICAgICAgICAgICAgICAgICAgICAgaXNBbmltYXRlZCA9IGZhbHNlO1xuICAgICAgICAgICAgICAgICAgICB9XG5cbiAgICAgICAgICAgICAgICAgICAgaWYgKGlzQW5pbWF0ZWQpIHtcbiAgICAgICAgICAgICAgICAgICAgICAgIGNvbnN0IHRodW1iID0gYXdhaXQgY3JlYXRlVGh1bWJuYWlsKFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIGltZyxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBpbWcud2lkdGgsXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgaW1nLmhlaWdodCxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBjb250ZW50LmluZm8/Lm1pbWV0eXBlID8/IFwiaW1hZ2UvanBlZ1wiLFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIGZhbHNlLFxuICAgICAgICAgICAgICAgICAgICAgICAgKTtcbiAgICAgICAgICAgICAgICAgICAgICAgIHRodW1iVXJsID0gVVJMLmNyZWF0ZU9iamVjdFVSTCh0aHVtYi50aHVtYm5haWwpO1xuICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAgICAgICAgICAgICAgICAgLy8gVGhpcyBpcyBhIG5vbi1jcml0aWNhbCBmYWlsdXJlLCBkbyBub3Qgc3VyZmFjZSB0aGUgZXJyb3Igb3IgYmFpbCB0aGUgbWV0aG9kIGhlcmVcbiAgICAgICAgICAgICAgICAgICAgbG9nZ2VyLndhcm4oXCJVbmFibGUgdG8gZ2VuZXJhdGUgdGh1bWJuYWlsIGZvciBhbmltYXRlZCBpbWFnZTogXCIsIGVycm9yKTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cblxuICAgICAgICBpZiAodGhpcy51bm1vdW50ZWQpIHJldHVybjtcbiAgICAgICAgdGhpcy5zZXRTdGF0ZSh7XG4gICAgICAgICAgICBjb250ZW50VXJsLFxuICAgICAgICAgICAgdGh1bWJVcmwsXG4gICAgICAgICAgICBpc0FuaW1hdGVkLFxuICAgICAgICB9KTtcbiAgICB9XG5cbiAgICBwcml2YXRlIGNsZWFyQmx1cmhhc2hUaW1lb3V0KCk6IHZvaWQge1xuICAgICAgICBpZiAodGhpcy50aW1lb3V0KSB7XG4gICAgICAgICAgICBjbGVhclRpbWVvdXQodGhpcy50aW1lb3V0KTtcbiAgICAgICAgICAgIHRoaXMudGltZW91dCA9IHVuZGVmaW5lZDtcbiAgICAgICAgfVxuICAgIH1cblxuICAgIHB1YmxpYyBjb21wb25lbnREaWRNb3VudCgpOiB2b2lkIHtcbiAgICAgICAgdGhpcy51bm1vdW50ZWQgPSBmYWxzZTtcblxuICAgICAgICBjb25zdCBzaG93SW1hZ2UgPVxuICAgICAgICAgICAgdGhpcy5zdGF0ZS5zaG93SW1hZ2UgfHwgbG9jYWxTdG9yYWdlLmdldEl0ZW0oXCJteF9TaG93SW1hZ2VfXCIgKyB0aGlzLnByb3BzLm14RXZlbnQuZ2V0SWQoKSkgPT09IFwidHJ1ZVwiO1xuXG4gICAgICAgIGlmIChzaG93SW1hZ2UpIHtcbiAgICAgICAgICAgIC8vIG5vaW5zcGVjdGlvbiBKU0lnbm9yZWRQcm9taXNlRnJvbUNhbGxcbiAgICAgICAgICAgIHRoaXMuZG93bmxvYWRJbWFnZSgpO1xuICAgICAgICAgICAgdGhpcy5zZXRTdGF0ZSh7IHNob3dJbWFnZTogdHJ1ZSB9KTtcbiAgICAgICAgfSAvLyBlbHNlIGRvbid0IGRvd25sb2FkIGFueXRoaW5nIGJlY2F1c2Ugd2UgZG9uJ3Qgd2FudCB0byBkaXNwbGF5IGFueXRoaW5nLlxuXG4gICAgICAgIC8vIEFkZCBhIDE1MG1zIHRpbWVyIGZvciBibHVyaGFzaCB0byBmaXJzdCBhcHBlYXIuXG4gICAgICAgIGlmICh0aGlzLnByb3BzLm14RXZlbnQuZ2V0Q29udGVudCgpLmluZm8/LltCTFVSSEFTSF9GSUVMRF0pIHtcbiAgICAgICAgICAgIHRoaXMuY2xlYXJCbHVyaGFzaFRpbWVvdXQoKTtcbiAgICAgICAgICAgIHRoaXMudGltZW91dCA9IHdpbmRvdy5zZXRUaW1lb3V0KCgpID0+IHtcbiAgICAgICAgICAgICAgICBpZiAoIXRoaXMuc3RhdGUuaW1nTG9hZGVkIHx8ICF0aGlz