1 Changelog

  • 20230317: Initial cellpose implementation.
  • 20230315: Separation of input image into timepoints.
  • 20230310: Used geopandas to trace cells over time by position.
  • 20230307: Setting up my environment to handle ipynb and python markdown.

2 Settings which may be necessary in emacs

I have been messing with my emacs setup to make this project easier.

(defvar elpy-rpc-virtualenv-path 'current)
(pyvenv-activate "/sw/local/fiji/202211")
(pyvenv-activate "venv")

3 Introduction

Jacques is seeking to segment and follow cells over time.

Steps performed so far:

  1. Python/venv installation of fiji
  2. Figured out some easy interactions between python and actual image data.
  3. Implemented a simple function to find minimum distances between putative cells using the extant methods.

4 Next steps

  1. Note that the czi images are immediately openable via fiji’s bioformat interface.
  2. Load dataset via fiji
  3. Invoke cellpose via the python interface (same as pyimagej)
  4. Save ROIs produced by cellpose, save them to zip output file
  1. The roi interface in fiji can address these
  1. Currently a macro is invoked which performs a set of measurements on every ROI and saves them to a csv. This creates the primary data structure used for all following processes.
  2. Need to create a datastructure which identifies each individual cell over time given these ROIs which x/y/time(frame) along with the measurements of interest (area/mean/stdev/intensities by color).
    1. Small postprocessing details: the intensity values produced must be normalized by cell area.

5 Demonstrate the first couple of steps using an actual dataset

Jacques sent me an image acquired from the microscope and I saved it as ‘test_data/raw.tif’, he also sent me a cellpose model which I saved to the ‘models/’ directory.

Given these as a starting point, let us try to open the image with a fiji instance and split it into a series of smaller files by timepoint.

5.1 Load necessary python modules

import os
import glob
import imagej
import pandas
from pandas import DataFrame
import numpy as np
import geopandas
import matplotlib.pyplot as plt
import seaborn
import scyjava
import multiprocessing as mp
from cellpose import models, io
from cellpose.io import *
from collections import defaultdict
from jpype import JArray, JInt

5.2 Set up some initial variables

base_dir = '/lab/scratch/atb/imaging/mtb_2023'
os.chdir(base_dir)
input_file = f"{base_dir}/test_data/raw.tif"
output_dir = f"{base_dir}/outputs"

5.3 Start imagej/fiji

Note that I am using fiji/imagej from within a virtual environment which I symbolically linked to the local directory ‘venv/’.

As a result, when I initialize fiji, I will call the base directory of the downloaded fiji tree within the virtual environment, which I somewhat erroneously put in bin/.

scyjava.config.add_option('-Xmx64g')
start_dir = os.getcwd()
ij = imagej.init('venv/bin/Fiji.app', mode = 'interactive')
## Something about this init() function changes the current working directory.
os.chdir(start_dir)
ij.getVersion()
## '2.9.0/1.53t'

5.4 Open the input file and split it by time

Open the file, figure out its dimensions, and write portions of it to an output directory.

I wrote this thinking I could parallelize the output writing to 8 cpus. But I think I do not yet understand how python scopes variables in this context and so it did not quite work. I turned that off for the moment and ran it and it finished in about a minute.

The following function may require a test to see if the output directory already exists because scijava will freak out.

def separate_slices(input_file, output_base = 'outputs', wanted_x = True, wanted_y = True,
                    wanted_z = 1, wanted_channel = 2, cpus = 8, overwrite = False):
    pool = mp.Pool(cpus)
    input_base = os.path.basename(input_file)
    input_dir = os.path.dirname(input_file)
    input_name = os.path.splitext(input_base)[0]
    output_directory = f"{input_dir}/outputs/{input_name}_substack"
    os.makedirs(output_directory, exist_ok = True)
    print("Starting to open the input file, this takes a moment.")
    dataset = ij.io().open(input_file)
    print(f"Opened input file, writing images to {output_directory}")

    data_info = {}
    for element in range(len(dataset.dims)):
        name = dataset.dims[element]
        data_info[name] = dataset.shape[element]
    print(f"This dataset has dimensions: X:{data_info['X']} Y:{data_info['Y']} Z:{data_info['Z']} Time:{data_info['Time']}")

    def save_slice(timepoint):
        wanted_slice = dataset[:, :, wanted_channel, wanted_z, timepoint]
        slice_image = ij.py.to_dataset(wanted_slice)
        output_filename = f"{output_directory}/{input_name}_{timepoint}.tif"
        if (os.path.exists(output_filename)):
            if overwrite:
                print(f"Rewriting {output_filename}")
                os.remove(output_filename)
                saved = ij.io().save(slice_image, output_filename)
            else:
                print(f"Skipping {output_filename}, it already exists.")
        else:
            saved = ij.io().save(slice_image, output_filename)
            print(f"Saving image {input_name}_{timepoint}.")
        return wanted_slice

    slices = []
    for timepoint in range(data_info['Time']):
        saved = save_slice(timepoint)
        slices.append(saved)
        ## I want to see if I can split the saves across 8 cpus.
        ## pool.apply(save_slice, args = (timepoint, ))
    ## pool.close()
    return dataset, slices, output_directory

raw_split, saved_slices, output_directory = separate_slices(input_file)

5.5 Cellpose

At this point we should have a directory containing files of individual timepoints. Jacques sent me an initial implementation of the usage of cellpose to call individual cells. Let us include that now. I think the previous function should probably also return the directory of the separated input files.

def invoke_cellpose(input_directory, model_file, channels = [[0, 0]], diameter = 160,
                    threshold = 0.4, do_3D = False, batch_size = 64):
    """ Invoke cellpose using the split files from the previous function.

    Jacques mentioned that this is getting slightly different results than the
    GUI-invoked version of this same function.  I figured that since I have a minute,
    I can poke at this and see if I can understand the requisites before he arrives.
    """

    ## Relevant options:
    ## model_type(cyto, nuclei, cyto2), net_avg(T/F if load built in networks and average them)
    model = models.CellposeModel(pretrained_model = model_file)
    files = get_image_files(input_directory, '_masks', look_one_level_down = False)
    imgs = [imread(f) for f in files]
    nimg = len(imgs)
    print(f"Read {nimg} images, starting cellpose.")
    ## Relevant options:
    ## batch_size(increase for more parallelization), channels(two element list of two element
    ## channels to segment; the first is the segment, second is optional nucleus;
    ## internal elements are color channels to query, so [[0,0],[2,3]] means do main cells in
    ## grayscale and a second with cells in blue, nuclei in green.
    ## channel_axis, z_axis ? invert (T/F flip pixels from b/w I assume),
    ## normalize(T/F percentile normalize the data), diameter, do_3d,
    ## anisotropy (rescaling factor for 3d segmentation), net_avg (average models),
    ## augment ?, tile ?, resample, interp, flow_threshold, cellprob_threshold (interesting),
    ## min_size (turned off with -1), stitch_threshold ?, rescale ?.
    output_files = defaultdict(dict)
    existing_files = 0
    for filename in files:
        f_name = os.path.splitext(filename)[0]
        output_mask = f"{start_dir}/{f_name}_cp_masks.png"
        output_txt = f"{start_dir}/{f_name}_cp_outlines.txt"
        output_files[filename]['input_file'] = f"{start_dir}/{filename}"
        output_files[filename]['output_mask'] = output_mask
        output_files[filename]['output_txt'] = output_txt
        output_files[filename]['exists'] = False
        if (os.path.exists(output_files[filename]['output_txt'])):
            existing_files = existing_files + 1
            output_files[filename]['exists'] = True

    if existing_files > 0:
        print(f"Out of {nimg} output files, {existing_files} already exist, not running cellpose.")
    else:
        masks, flows, styles = model.eval(
            imgs, diameter = diameter, channels = channels, flow_threshold = threshold,
            do_3D = do_3D, batch_size = batch_size)
        io.save_to_png(imgs, masks, flows, files)
    print("Finished cellpose, returning output filenames.")
    return output_files

os.chdir(start_dir)
output_directory = 'test_data/outputs/raw_substack'
cellpose_result = invoke_cellpose(output_directory, 'models/CP_20220523_104016')

6 Create Regions of interest from cellpose outputs

In Jacques notebook, it looks like he only extracts ROIs from one of the cellpose slices. I am assuming the goal is to extend this across all images?

There is an important caveat that I missed: imagej comes with a python2-based scripting language from which it appears some of his code is coming. As a result I should look carefully before using it, and pay close attention to the examples provided here for the most appropriate ways of interacting with the ROI manager etc:

https://github.com/imagej/pyimagej/blob/main/doc/examples/blob_detection_interactive.py

# get ImageJ resources
PolygonRoi = scyjava.jimport('ij.gui.PolygonRoi')
Overlay = scyjava.jimport('ij.gui.Overlay')
Regions = scyjava.jimport('net.imglib2.roi.Regions')
LabelRegions = scyjava.jimport('net.imglib2.roi.labeling.LabelRegions')
ov = Overlay()
from pandas import DataFrame
os.chdir(start_dir)

## The following is from a mix of a couple of implementations I found:
## https://pyimagej.readthedocs.io/en/latest/Classic-Segmentation.html
## an alternative method may be taken from:
## https://pyimagej.readthedocs.io/en/latest/Classic-Segmentation.html#segmentation-workflow-with-imagej2
## My goal is to pass the ROI regions to this function and create a similar df.

def slices_to_roi_measurements(cellpose_result, saved_slices):
    output_dict = cellpose_result
    cellpose_slices = list(cellpose_result.keys())
    slice_number = 0
    for slice_name in cellpose_slices:
        output_dict[slice_name]['slice_number'] = slice_number
        slice_data = saved_slices[slice_number]
        input_tif = cellpose_result[slice_name]['input_file']
        input_txt = cellpose_result[slice_name]['output_txt']
        input_mask = cellpose_result[slice_name]['output_mask']
        print(f"Processing cellpose outline: {input_txt}")
        # convert Dataset to ImagePlus
        imp = ij.py.to_imageplus(slice_data)
        rm = ij.RoiManager.getRoiManager()
        ## The logic for this was taken from:
        ## https://stackoverflow.com/questions/73849418/is-there-any-way-to-switch-imagej-macro-code-to-python3-code
        txt_fh = open(input_txt, 'r')
        ij.IJ.run("Set Measurements...", "area mean min centroid median skewness kurtosis redirect=None decimal=3")
        roi_stats = defaultdict(list)
        for line in txt_fh:
            xy = line.rstrip().split(",")
            xy_coords = [int(element) for element in xy]
            x_coords = [int(element) for element in xy[::2]]
            y_coords = [int(element) for element in xy[1::2]]
            xcoords_jint = JArray(JInt)(x_coords)
            ycoords_jint = JArray(JInt)(y_coords)
            polygon_roi_instance = scyjava.jimport('ij.gui.PolygonRoi')
            roi_instance = scyjava.jimport('ij.gui.Roi')
            imported_polygon = polygon_roi_instance(xcoords_jint, ycoords_jint,
                                                    len(x_coords), int(roi_instance.POLYGON))
            imp.setRoi(imported_polygon)
            ij.IJ.run(imp, 'Measure', '')
        slice_result = ij.ResultsTable.getResultsTable()
        slice_table = ij.convert().convert(slice_result, scyjava.jimport('org.scijava.table.Table'))
        slice_measurements = ij.py.from_java(slice_table)
        output_dict[slice_name]['measurements'] = slice_measurements
        ij.IJ.run('Clear Results')
        txt_fh.close()
        imp.setOverlay(ov)
        imp.getProcessor().resetMinAndMax()
        slice_number = slice_number + 1
    return output_dict

slice_measurements = slices_to_roi_measurements(cellpose_result, saved_slices)

7 Convert the slice measurements to pandas df

slices_to_roi_measurements() returns a dictionary with keys which are the filenames of each raw tif file. Each element of that dictionary is in turn a dictionary containing some information about the files along with a df of the measurements provided by imagej.

My little geopandas function assumes a single long df with some columns which tell it which timepoint. So lets make a quick function to give that here. OTOH it may be wiser/better to make some changes to slices_to_roi_measurements() so that it returns that format df; but since I am using this as a learning experience to get more comfortable with python data structures, I will not do it that way.

## def convert_slices_to_pandas(slices):

concatenated = pandas.DataFrame()
slice_keys = list(slice_measurements.keys())
slice_counter = 0
for k in slice_keys:
    slice_counter = slice_counter + 1
    current_slice = slice_measurements[k]
    print(f"The slice is {k}")
    slice_number = current_slice['slice_number']
    slice_data = current_slice['measurements']
    slice_data['slice_number'] = slice_number
    if (slice_counter == 1):
        concatenated = slice_data
    else:
        concatenated = pandas.concat([concatenated, slice_data])

8 Create cell groups

8.1 Load the TSV data

In the above R implementation, I dumped the data to a tsv file, and will just read that here for simplicity.

While I am testing, I will extract only the columns containing the ID, x, y, and time(Frame).

9 Load the coordinates into a geospatial distance method

geopandas looks like it handles coordinate data in a similar fashion to R’s spatstat.geom. I am not sure if it will be able to handle a z-stack; but I assume it will prove easy to extend the implementation to points_from_xyz().

gdf = geopandas.GeoDataFrame(
    concatenated,
    geometry = geopandas.points_from_xy(concatenated.X, concatenated.Y))

I think the easiest way to extract minimum distances in python is to split this df into pairs by Frame followed by using the sjoin_nearest() function to find nearest points from a->b.

9.1 subset the gdf data

In the following block, I will iterate over every time from 1 to end-1 and extract the elements of the gdf from those two time points. Then I will use the sjoin_nearest() to get me every time-pairwise closest distance as a new dataframe.

These pairwise distance dataframes will be appended to the eponymous pairwise_distances array; which upon completion will comprise n-1 elements where the first element includes the results for timepoints 1,2 and the last element includes n-1,n.

final_time = gdf.Frame.max()
pairwise_distances = []
for start_time in range(1, final_time):
    i = start_time
    j = i + 1
    ti_idx = gdf.Frame == i
    tj_idx = gdf.Frame == j
    print("Getting distances of dfs ", i, " and ", j, ".")
    ti = gdf[ti_idx]
    tj = gdf[tj_idx]
    ti_rows = ti.shape[0]
    tj_rows = tj.shape[0]
    titj = geopandas.sjoin_nearest(ti, tj, distance_col = "pairwise_dist")
    pairwise_distances.append(titj)

9.2 Filter the pairwise distances and find neighbors over time

Given the pairwise_distances array, I should be able to iterate over it and make a series of arrays containing the cell IDs which are composed of cells which are closest to each other over time.

The primary caveat here is that we know that sometimes the cells are merged or other shenanigans happen which cause the closest distance to get relatively large, thus I will filter out any pairwise distances which are greater than max_dist.

As a result, the number of cells observed over time will by definition not be the ~ 20 observed in the 121 timepoints, but will be some larger number comprised of the sum over time. A likely next step will be to merge these partial groups post-facto, but not right now, I just want to get this initial implementation working.

Here is the logic:

  1. Use id_counter to make a global cell ID across all timepoints.
  2. Use traced{} to associate the id_counter cell ID to an array of observed cells over time.
  3. Use ends{} to associate every known current endpoint with a id_counter cell ID so that we can keep track of who is who.
  4. Then iterate over every element of pairwise_distances and do the following:
  1. drop elements with distances > max_dist.
  2. check to see if the early-timepoint nearest neighbor (start_cell) has been seen in ends{}: * If so add end_cell to it and to the appropriate element of traced{}. * If not, initialize traced{} with the start/end and add it to ends{}.

Upon completion I should have a dictionary of arrays containing all of the contiugous nearest-neighbor cells that stayed within max_dist of each other.

## Start over if the distance between two cells is bigger than 10 units.
max_dist = 10.0
## Start over if the proportion between the bigger and smaller cell area has changed
## by more than a factor of 0.25.
max_prop = 0.7
id_counter = 0
## Cell IDs pointing to a list of cells
traced = {}
## Endpoints pointing to the cell IDs
ends = {}
for i in range(0, final_time - 1):
    query = pairwise_distances[i]

    passed_idx = query.pairwise_dist <= max_dist
    failed_idx = query.pairwise_dist > max_dist
    if (failed_idx.sum() > 0):
        print("Skipped ", failed_idx.sum(), " elements in segment ", i,
              " because distance is too large.")
        query = query[passed_idx]

    prop_change = query.Area_left / query.Area_right
    increased_idx = prop_change > 1.0
    prop_change[increased_idx] = 1.0 / prop_change[increased_idx]
    failed_idx = prop_change < max_prop
    passed_idx = prop_change >= max_prop
    if (failed_idx.sum() > 0):
        print("Skipped ", failed_idx.sum(), " elements in segment ", i,
              " because the size changed too much.")
        query = query[passed_idx]

    for row in query.itertuples():
        start_cell = row.ID_left
        end_cell = row.ID_right
        if start_cell in ends.keys():
            cell_id = ends[start_cell]
            current_value = traced[cell_id]
            current_value.append(end_cell)
            traced[cell_id] = current_value
            ends[end_cell] = cell_id
        else:
            id_counter = id_counter + 1
            traced[id_counter] = [start_cell, end_cell]
            ends[end_cell] = id_counter

traced

Ok, I looked at the result of traced and it seems to make sense to me; lets rewrite this as a single function that Jacques can add to his notebook. I will assume he has a pandas df as input.

9.3 Get information from a group of cells

As a final step, we should be able to extract and play with the information from one or more groups of cells.

cell_id = 6
cell_idx = input['ID'].isin(traced[cell_id])
cell_data = input[cell_idx].reset_index()

plt.scatter(cell_data['X'], cell_data['Y'])
final_row = cell_data.Frame.max()
for start_time in range(1, final_row - 1):
    ti_idx = cell_data.Frame == start_time
    tj_idx = cell_data.Frame == start_time + 1
    p1x = cell_data[ti_idx].X
    p2x = cell_data[tj_idx].X
    p1y = cell_data[ti_idx].Y
    p2y = cell_data[tj_idx].Y
    x_points = [p1x, p2x]
    y_points = [p1y, p2y]
    plt.plot(x_points, y_points)
plt.show()

seaborn.violinplot(data = cell_data.Area)
plt.show()

10 Single function definition

Now let us combine the things learned above into a single function.

def nearest_cells_over_time(df, max_dist = 10.0, x_column = 'X', y_column = 'Y'):
    """Trace cells over time"""
    gdf = geopandas.GeoDataFrame(
        df,
        geometry = geopandas.points_from_xy(df[x_column], df[y_column]))

    final_time = gdf.Frame.max()
    pairwise_distances = []
    for start_time in range(1, final_time):
        i = start_time
        j = i + 1
        ti_idx = gdf.Frame == i
        tj_idx = gdf.Frame == j
        print("Getting distances of dfs ", i, " and ", j, ".")
        ti = gdf[ti_idx]
        tj = gdf[tj_idx]
        ti_rows = ti.shape[0]
        tj_rows = tj.shape[0]
        titj = geopandas.sjoin_nearest(ti, tj, distance_col = "pairwise_dist")
        pairwise_distances.append(titj)

    max_dist = 10.0
    id_counter = 0
    ## Cell IDs pointing to a list of cells
    traced = {}
    ## Endpoints pointing to the cell IDs
    ends = {}
    for i in range(0, final_time - 1):
        query = pairwise_distances[i]
        passed_idx = query.pairwise_dist <= max_dist
        failed_idx = query.pairwise_dist > max_dist
        if (failed_idx.sum() > 0):
            print("Skipped ", failed_idx.sum(), " elements in segment ", i, ".")
        query = query[passed_idx]
        for row in query.itertuples():
            start_cell = row.ID_left
            end_cell = row.ID_right
            if start_cell in ends.keys():
                cell_id = ends[start_cell]
                current_value = traced[cell_id]
                current_value.append(end_cell)
                traced[cell_id] = current_value
                ends[end_cell] = cell_id
            else:
                id_counter = id_counter + 1
                traced[id_counter] = [start_cell, end_cell]
                ends[end_cell] = id_counter

    return traced
pander::pander(sessionInfo())

R version 4.2.0 (2022-04-22)

Platform: x86_64-pc-linux-gnu (64-bit)

locale: LC_CTYPE=en_US.UTF-8, LC_NUMERIC=C, LC_TIME=en_US.UTF-8, LC_COLLATE=en_US.UTF-8, LC_MONETARY=en_US.UTF-8, LC_MESSAGES=en_US.UTF-8, LC_PAPER=en_US.UTF-8, LC_NAME=en_US.UTF-8, LC_ADDRESS=en_US.UTF-8, LC_TELEPHONE=en_US.UTF-8, LC_MEASUREMENT=en_US.UTF-8 and LC_IDENTIFICATION=en_US.UTF-8

attached base packages: stats4, stats, graphics, grDevices, utils, datasets, methods and base

other attached packages: spatstat.geom(v.3.0-6), spatstat.data(v.3.0-0), hpgltools(v.1.0), testthat(v.3.1.6), reticulate(v.1.28), SummarizedExperiment(v.1.28.0), GenomicRanges(v.1.50.2), GenomeInfoDb(v.1.34.9), IRanges(v.2.32.0), S4Vectors(v.0.36.1), MatrixGenerics(v.1.10.0), matrixStats(v.0.63.0), Biobase(v.2.58.0) and BiocGenerics(v.0.44.0)

loaded via a namespace (and not attached): rappdirs(v.0.3.3), rtracklayer(v.1.58.0), tidyr(v.1.3.0), ggplot2(v.3.4.1), clusterGeneration(v.1.3.7), bit64(v.4.0.5), knitr(v.1.42), DelayedArray(v.0.24.0), data.table(v.1.14.6), KEGGREST(v.1.38.0), RCurl(v.1.98-1.10), doParallel(v.1.0.17), generics(v.0.1.3), GenomicFeatures(v.1.50.4), callr(v.3.7.3), RhpcBLASctl(v.0.23-42), cowplot(v.1.1.1), usethis(v.2.1.6), RSQLite(v.2.2.20), shadowtext(v.0.1.2), bit(v.4.0.5), enrichplot(v.1.18.3), xml2(v.1.3.3), httpuv(v.1.6.8), assertthat(v.0.2.1), viridis(v.0.6.2), xfun(v.0.37), hms(v.1.1.2), jquerylib(v.0.1.4), evaluate(v.0.20), promises(v.1.2.0.1), fansi(v.1.0.4), restfulr(v.0.0.15), progress(v.1.2.2), caTools(v.1.18.2), dbplyr(v.2.3.0), igraph(v.1.4.0), DBI(v.1.1.3), htmlwidgets(v.1.6.1), purrr(v.1.0.1), ellipsis(v.0.3.2), dplyr(v.1.1.0), backports(v.1.4.1), annotate(v.1.76.0), aod(v.1.3.2), deldir(v.1.0-6), biomaRt(v.2.54.0), vctrs(v.0.5.2), here(v.1.0.1), remotes(v.2.4.2), cachem(v.1.0.6), withr(v.2.5.0), ggforce(v.0.4.1), HDO.db(v.0.99.1), GenomicAlignments(v.1.34.0), treeio(v.1.22.0), prettyunits(v.1.1.1), DOSE(v.3.24.2), ape(v.5.6-2), lazyeval(v.0.2.2), crayon(v.1.5.2), genefilter(v.1.80.3), edgeR(v.3.40.2), pkgconfig(v.2.0.3), tweenr(v.2.0.2), nlme(v.3.1-162), pkgload(v.1.3.2), devtools(v.2.4.5), rlang(v.1.0.6), lifecycle(v.1.0.3), miniUI(v.0.1.1.1), downloader(v.0.4), filelock(v.1.0.2), BiocFileCache(v.2.6.0), rprojroot(v.2.0.3), polyclip(v.1.10-4), graph(v.1.76.0), Matrix(v.1.5-3), aplot(v.0.1.9), boot(v.1.3-28.1), processx(v.3.8.0), png(v.0.1-8), viridisLite(v.0.4.1), rjson(v.0.2.21), bitops(v.1.0-7), gson(v.0.0.9), KernSmooth(v.2.23-20), pander(v.0.6.5), Biostrings(v.2.66.0), blob(v.1.2.3), stringr(v.1.5.0), qvalue(v.2.30.0), remaCor(v.0.0.11), gridGraphics(v.0.5-1), scales(v.1.2.1), memoise(v.2.0.1), GSEABase(v.1.60.0), magrittr(v.2.0.3), plyr(v.1.8.8), gplots(v.3.1.3), zlibbioc(v.1.44.0), compiler(v.4.2.0), scatterpie(v.0.1.8), BiocIO(v.1.8.0), RColorBrewer(v.1.1-3), lme4(v.1.1-31), Rsamtools(v.2.14.0), cli(v.3.6.0), XVector(v.0.38.0), urlchecker(v.1.0.1), patchwork(v.1.1.2), ps(v.1.7.2), MASS(v.7.3-58.2), mgcv(v.1.8-41), tidyselect(v.1.2.0), stringi(v.1.7.12), yaml(v.2.3.7), GOSemSim(v.2.24.0), locfit(v.1.5-9.7), ggrepel(v.0.9.3), grid(v.4.2.0), sass(v.0.4.5), fastmatch(v.1.1-3), tools(v.4.2.0), parallel(v.4.2.0), rstudioapi(v.0.14), foreach(v.1.5.2), gridExtra(v.2.3), farver(v.2.1.1), ggraph(v.2.1.0), digest(v.0.6.31), shiny(v.1.7.4), Rcpp(v.1.0.10), broom(v.1.0.3), later(v.1.3.0), httr(v.1.4.4), AnnotationDbi(v.1.60.0), Rdpack(v.2.4), colorspace(v.2.1-0), brio(v.1.1.3), XML(v.3.99-0.13), fs(v.1.6.1), splines(v.4.2.0), yulab.utils(v.0.0.6), spatstat.utils(v.3.0-1), PROPER(v.1.30.0), tidytree(v.0.4.2), graphlayouts(v.0.8.4), ggplotify(v.0.1.0), plotly(v.4.10.1), sessioninfo(v.1.2.2), xtable(v.1.8-4), jsonlite(v.1.8.4), nloptr(v.2.0.3), ggtree(v.3.6.2), tidygraph(v.1.2.3), ggfun(v.0.0.9), R6(v.2.5.1), RUnit(v.0.4.32), profvis(v.0.3.7), pillar(v.1.8.1), htmltools(v.0.5.4), mime(v.0.12), glue(v.1.6.2), fastmap(v.1.1.0), minqa(v.1.2.5), clusterProfiler(v.4.6.0), BiocParallel(v.1.32.5), codetools(v.0.2-19), fgsea(v.1.24.0), pkgbuild(v.1.4.0), mvtnorm(v.1.1-3), utf8(v.1.2.3), lattice(v.0.20-45), bslib(v.0.4.2), tibble(v.3.1.8), sva(v.3.46.0), pbkrtest(v.0.5.2), curl(v.5.0.0), gtools(v.3.9.4), GO.db(v.3.16.0), survival(v.3.5-3), limma(v.3.54.1), rmarkdown(v.2.20), desc(v.1.4.2), munsell(v.0.5.0), GenomeInfoDbData(v.1.2.9), iterators(v.1.0.14), variancePartition(v.1.28.4), reshape2(v.1.4.4), gtable(v.0.3.1) and rbibutils(v.2.2.13)

message(paste0("This is hpgltools commit: ", get_git_commit()))
## If you wish to reproduce this exact build of hpgltools, invoke the following:
## > git clone http://github.com/abelew/hpgltools.git
## > git reset 465cd7740fb559303acb139de1d478adfc3f6612
## This is hpgltools commit: Fri Mar 24 14:37:21 2023 -0400: 465cd7740fb559303acb139de1d478adfc3f6612
this_save <- paste0(gsub(pattern="\\.Rmd", replace="", x=rmd_file), "-v", ver, ".rda.xz")
message(paste0("Saving to ", this_save))
## Saving to index-v20230330.rda.xz
tmp <- sm(saveme(filename=this_save))
LS0tCnRpdGxlOiAiTm90ZXMgZm9yIEphY3F1ZXMgaW1hZ2VzLiIKYXV0aG9yOiAiYXRiIGFiZWxld0BnbWFpbC5jb20iCmRhdGU6ICJgciBTeXMuRGF0ZSgpYCIKb3V0cHV0OgogIGh0bWxfZG9jdW1lbnQ6CiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlCiAgICBjb2RlX2ZvbGRpbmc6IHNob3cKICAgIGZpZ19jYXB0aW9uOiB0cnVlCiAgICBmaWdfaGVpZ2h0OiA3CiAgICBmaWdfd2lkdGg6IDcKICAgIGhpZ2hsaWdodDogdGFuZ28KICAgIGtlZXBfbWQ6IGZhbHNlCiAgICBtb2RlOiBzZWxmY29udGFpbmVkCiAgICBudW1iZXJfc2VjdGlvbnM6IHRydWUKICAgIHNlbGZfY29udGFpbmVkOiB0cnVlCiAgICB0aGVtZTogcmVhZGFibGUKICAgIHRvYzogdHJ1ZQogICAgdG9jX2Zsb2F0OgogICAgICBjb2xsYXBzZWQ6IGZhbHNlCiAgICAgIHNtb290aF9zY3JvbGw6IGZhbHNlCi0tLQoKPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KYm9keSwgdGQgewogIGZvbnQtc2l6ZTogMTZweDsKfQpjb2RlLnJ7CiAgZm9udC1zaXplOiAxNnB4Owp9CnByZSB7CiBmb250LXNpemU6IDE2cHgKfQo8L3N0eWxlPgoKYGBge3Igb3B0aW9ucywgaW5jbHVkZT1GQUxTRX0KbGlicmFyeShocGdsdG9vbHMpCmxpYnJhcnkocmV0aWN1bGF0ZSkKdHQgPC0gZGV2dG9vbHM6OmxvYWRfYWxsKCJ+L2hwZ2x0b29scyIpCmtuaXRyOjpvcHRzX2tuaXQkc2V0KAogIHdpZHRoID0gMTIwLCBwcm9ncmVzcyA9IFRSVUUsIHZlcmJvc2UgPSBUUlVFLCBlY2hvID0gVFJVRSkKa25pdHI6Om9wdHNfY2h1bmskc2V0KGVycm9yID0gVFJVRSwgZHBpID0gOTYpCmx1YV9maWx0ZXJzIDwtIHJtYXJrZG93bjo6cGFuZG9jX2x1YV9maWx0ZXJfYXJncygicGFuZG9jLXpvdHh0Lmx1YSIpCm9sZF9vcHRpb25zIDwtIG9wdGlvbnMoCiAgZGlnaXRzID0gNCwgc3RyaW5nc0FzRmFjdG9ycyA9IEZBTFNFLCBrbml0ci5kdXBsaWNhdGUubGFiZWwgPSAiYWxsb3ciKQpnZ3Bsb3QyOjp0aGVtZV9zZXQoZ2dwbG90Mjo6dGhlbWVfYncoYmFzZV9zaXplID0gMTApKQpydW5kYXRlIDwtIGZvcm1hdChTeXMuRGF0ZSgpLCBmb3JtYXQgPSAiJVklbSVkIikKcHJldmlvdXNfZmlsZSA8LSAiIgp2ZXIgPC0gZm9ybWF0KFN5cy5EYXRlKCksICIlWSVtJWQiKQoKIyN0bXAgPC0gc20obG9hZG1lKGZpbGVuYW1lPXBhc3RlMChnc3ViKHBhdHRlcm49IlxcLlJtZCIsIHJlcGxhY2U9IiIsIHg9cHJldmlvdXNfZmlsZSksICItdiIsIHZlciwgIi5yZGEueHoiKSkpCnJtZF9maWxlIDwtICJpbmRleC5SbWQiCmxpYnJhcnkoc3BhdHN0YXQuZ2VvbSkKYGBgCgojIENoYW5nZWxvZwoKKiAyMDIzMDMxNzogSW5pdGlhbCBjZWxscG9zZSBpbXBsZW1lbnRhdGlvbi4KKiAyMDIzMDMxNTogU2VwYXJhdGlvbiBvZiBpbnB1dCBpbWFnZSBpbnRvIHRpbWVwb2ludHMuCiogMjAyMzAzMTA6IFVzZWQgZ2VvcGFuZGFzIHRvIHRyYWNlIGNlbGxzIG92ZXIgdGltZSBieSBwb3NpdGlvbi4KKiAyMDIzMDMwNzogU2V0dGluZyB1cCBteSBlbnZpcm9ubWVudCB0byBoYW5kbGUgaXB5bmIgYW5kIHB5dGhvbiBtYXJrZG93bi4KCiMgU2V0dGluZ3Mgd2hpY2ggbWF5IGJlIG5lY2Vzc2FyeSBpbiBlbWFjcwoKSSBoYXZlIGJlZW4gbWVzc2luZyB3aXRoIG15IGVtYWNzIHNldHVwIHRvIG1ha2UgdGhpcyBwcm9qZWN0IGVhc2llci4KCmBgYHtlbGlzcCBzZXR1cF93b3Jrb24sIGV2YWw9RkFMU0V9CihkZWZ2YXIgZWxweS1ycGMtdmlydHVhbGVudi1wYXRoICdjdXJyZW50KQoocHl2ZW52LWFjdGl2YXRlICIvc3cvbG9jYWwvZmlqaS8yMDIyMTEiKQoocHl2ZW52LWFjdGl2YXRlICJ2ZW52IikKYGBgCgojIEludHJvZHVjdGlvbgoKSmFjcXVlcyBpcyBzZWVraW5nIHRvIHNlZ21lbnQgYW5kIGZvbGxvdyBjZWxscyBvdmVyIHRpbWUuCgpTdGVwcyBwZXJmb3JtZWQgc28gZmFyOgoKMS4gIFB5dGhvbi92ZW52IGluc3RhbGxhdGlvbiBvZiBmaWppCjIuICBGaWd1cmVkIG91dCBzb21lIGVhc3kgaW50ZXJhY3Rpb25zIGJldHdlZW4gcHl0aG9uIGFuZCBhY3R1YWwgaW1hZ2UgZGF0YS4KMy4gIEltcGxlbWVudGVkIGEgc2ltcGxlIGZ1bmN0aW9uIHRvIGZpbmQgbWluaW11bSBkaXN0YW5jZXMgYmV0d2VlbgogICAgcHV0YXRpdmUgY2VsbHMgdXNpbmcgdGhlIGV4dGFudCBtZXRob2RzLgoKIyBOZXh0IHN0ZXBzCgoxLiAgTm90ZSB0aGF0IHRoZSBjemkgaW1hZ2VzIGFyZSBpbW1lZGlhdGVseSBvcGVuYWJsZSB2aWEgZmlqaSdzIGJpb2Zvcm1hdCBpbnRlcmZhY2UuCjIuICBMb2FkIGRhdGFzZXQgdmlhIGZpamkKMy4gIEludm9rZSBjZWxscG9zZSB2aWEgdGhlIHB5dGhvbiBpbnRlcmZhY2UgKHNhbWUgYXMgcHlpbWFnZWopCjQuICBTYXZlIFJPSXMgcHJvZHVjZWQgYnkgY2VsbHBvc2UsIHNhdmUgdGhlbSB0byB6aXAgb3V0cHV0IGZpbGUKICBhLiAgVGhlIHJvaSBpbnRlcmZhY2UgaW4gZmlqaSBjYW4gYWRkcmVzcyB0aGVzZQo1LiAgQ3VycmVudGx5IGEgbWFjcm8gaXMgaW52b2tlZCB3aGljaCBwZXJmb3JtcyBhIHNldCBvZiBtZWFzdXJlbWVudHMKICAgIG9uIGV2ZXJ5IFJPSSBhbmQgc2F2ZXMgdGhlbSB0byBhIGNzdi4gIFRoaXMgY3JlYXRlcyB0aGUgcHJpbWFyeQogICAgZGF0YSBzdHJ1Y3R1cmUgdXNlZCBmb3IgYWxsIGZvbGxvd2luZyBwcm9jZXNzZXMuCjYuICBOZWVkIHRvIGNyZWF0ZSBhIGRhdGFzdHJ1Y3R1cmUgd2hpY2ggaWRlbnRpZmllcyBlYWNoIGluZGl2aWR1YWwKICAgIGNlbGwgb3ZlciB0aW1lIGdpdmVuIHRoZXNlIFJPSXMgd2hpY2ggeC95L3RpbWUoZnJhbWUpIGFsb25nIHdpdGgKICAgIHRoZSBtZWFzdXJlbWVudHMgb2YgaW50ZXJlc3QgKGFyZWEvbWVhbi9zdGRldi9pbnRlbnNpdGllcyBieQogICAgY29sb3IpLgogICAgYS4gIFNtYWxsIHBvc3Rwcm9jZXNzaW5nIGRldGFpbHM6IHRoZSBpbnRlbnNpdHkgdmFsdWVzIHByb2R1Y2VkCiAgICBtdXN0IGJlIG5vcm1hbGl6ZWQgYnkgY2VsbCBhcmVhLgoKIyBEZW1vbnN0cmF0ZSB0aGUgZmlyc3QgY291cGxlIG9mIHN0ZXBzIHVzaW5nIGFuIGFjdHVhbCBkYXRhc2V0CgpKYWNxdWVzIHNlbnQgbWUgYW4gaW1hZ2UgYWNxdWlyZWQgZnJvbSB0aGUgbWljcm9zY29wZSBhbmQgSSBzYXZlZCBpdAphcyAndGVzdF9kYXRhL3Jhdy50aWYnLCBoZSBhbHNvIHNlbnQgbWUgYSBjZWxscG9zZSBtb2RlbCB3aGljaCBJIHNhdmVkCnRvIHRoZSAnbW9kZWxzLycgZGlyZWN0b3J5LgoKR2l2ZW4gdGhlc2UgYXMgYSBzdGFydGluZyBwb2ludCwgbGV0IHVzIHRyeSB0byBvcGVuIHRoZSBpbWFnZSB3aXRoIGEKZmlqaSBpbnN0YW5jZSBhbmQgc3BsaXQgaXQgaW50byBhIHNlcmllcyBvZiBzbWFsbGVyIGZpbGVzIGJ5CnRpbWVwb2ludC4KCiMjIExvYWQgbmVjZXNzYXJ5IHB5dGhvbiBtb2R1bGVzCgpgYGB7cHl0aG9uIGxvYWR9CmltcG9ydCBvcwppbXBvcnQgZ2xvYgppbXBvcnQgaW1hZ2VqCmltcG9ydCBwYW5kYXMKZnJvbSBwYW5kYXMgaW1wb3J0IERhdGFGcmFtZQppbXBvcnQgbnVtcHkgYXMgbnAKaW1wb3J0IGdlb3BhbmRhcwppbXBvcnQgbWF0cGxvdGxpYi5weXBsb3QgYXMgcGx0CmltcG9ydCBzZWFib3JuCmltcG9ydCBzY3lqYXZhCmltcG9ydCBtdWx0aXByb2Nlc3NpbmcgYXMgbXAKZnJvbSBjZWxscG9zZSBpbXBvcnQgbW9kZWxzLCBpbwpmcm9tIGNlbGxwb3NlLmlvIGltcG9ydCAqCmZyb20gY29sbGVjdGlvbnMgaW1wb3J0IGRlZmF1bHRkaWN0CmZyb20ganB5cGUgaW1wb3J0IEpBcnJheSwgSkludApgYGAKCiMjIFNldCB1cCBzb21lIGluaXRpYWwgdmFyaWFibGVzCgpgYGB7cHl0aG9uIHBhcmFtZXRlcnN9CmJhc2VfZGlyID0gJy9sYWIvc2NyYXRjaC9hdGIvaW1hZ2luZy9tdGJfMjAyMycKb3MuY2hkaXIoYmFzZV9kaXIpCmlucHV0X2ZpbGUgPSBmIntiYXNlX2Rpcn0vdGVzdF9kYXRhL3Jhdy50aWYiCm91dHB1dF9kaXIgPSBmIntiYXNlX2Rpcn0vb3V0cHV0cyIKYGBgCgojIyBTdGFydCBpbWFnZWovZmlqaQoKTm90ZSB0aGF0IEkgYW0gdXNpbmcgZmlqaS9pbWFnZWogZnJvbSB3aXRoaW4gYSB2aXJ0dWFsIGVudmlyb25tZW50CndoaWNoIEkgc3ltYm9saWNhbGx5IGxpbmtlZCB0byB0aGUgbG9jYWwgZGlyZWN0b3J5ICd2ZW52LycuCgpBcyBhIHJlc3VsdCwgd2hlbiBJIGluaXRpYWxpemUgZmlqaSwgSSB3aWxsIGNhbGwgdGhlIGJhc2UgZGlyZWN0b3J5IG9mCnRoZSBkb3dubG9hZGVkIGZpamkgdHJlZSB3aXRoaW4gdGhlIHZpcnR1YWwgZW52aXJvbm1lbnQsIHdoaWNoIEkKc29tZXdoYXQgZXJyb25lb3VzbHkgcHV0IGluIGJpbi8uCgpgYGB7cHl0aG9uIHN0YXJ0X2Zpaml9CnNjeWphdmEuY29uZmlnLmFkZF9vcHRpb24oJy1YbXg2NGcnKQpzdGFydF9kaXIgPSBvcy5nZXRjd2QoKQppaiA9IGltYWdlai5pbml0KCd2ZW52L2Jpbi9GaWppLmFwcCcsIG1vZGUgPSAnaW50ZXJhY3RpdmUnKQojIyBTb21ldGhpbmcgYWJvdXQgdGhpcyBpbml0KCkgZnVuY3Rpb24gY2hhbmdlcyB0aGUgY3VycmVudCB3b3JraW5nIGRpcmVjdG9yeS4Kb3MuY2hkaXIoc3RhcnRfZGlyKQppai5nZXRWZXJzaW9uKCkKYGBgCgojIyBPcGVuIHRoZSBpbnB1dCBmaWxlIGFuZCBzcGxpdCBpdCBieSB0aW1lCgpPcGVuIHRoZSBmaWxlLCBmaWd1cmUgb3V0IGl0cyBkaW1lbnNpb25zLCBhbmQgd3JpdGUgcG9ydGlvbnMgb2YgaXQgdG8KYW4gb3V0cHV0IGRpcmVjdG9yeS4KCkkgd3JvdGUgdGhpcyB0aGlua2luZyBJIGNvdWxkIHBhcmFsbGVsaXplIHRoZSBvdXRwdXQgd3JpdGluZyB0byA4CmNwdXMuICBCdXQgSSB0aGluayBJIGRvIG5vdCB5ZXQgdW5kZXJzdGFuZCBob3cgcHl0aG9uIHNjb3BlcyB2YXJpYWJsZXMKaW4gdGhpcyBjb250ZXh0IGFuZCBzbyBpdCBkaWQgbm90IHF1aXRlIHdvcmsuICBJIHR1cm5lZCB0aGF0IG9mZiBmb3IKdGhlIG1vbWVudCBhbmQgcmFuIGl0IGFuZCBpdCBmaW5pc2hlZCBpbiBhYm91dCBhIG1pbnV0ZS4KClRoZSBmb2xsb3dpbmcgZnVuY3Rpb24gbWF5IHJlcXVpcmUgYSB0ZXN0IHRvIHNlZSBpZiB0aGUgb3V0cHV0CmRpcmVjdG9yeSBhbHJlYWR5IGV4aXN0cyBiZWNhdXNlIHNjaWphdmEgd2lsbCBmcmVhayBvdXQuCgpgYGB7cHl0aG9uIHNwbGl0X2ltYWdlLCBldmFsPUZBTFNFfQpkZWYgc2VwYXJhdGVfc2xpY2VzKGlucHV0X2ZpbGUsIG91dHB1dF9iYXNlID0gJ291dHB1dHMnLCB3YW50ZWRfeCA9IFRydWUsIHdhbnRlZF95ID0gVHJ1ZSwKICAgICAgICAgICAgICAgICAgICB3YW50ZWRfeiA9IDEsIHdhbnRlZF9jaGFubmVsID0gMiwgY3B1cyA9IDgsIG92ZXJ3cml0ZSA9IEZhbHNlKToKICAgIHBvb2wgPSBtcC5Qb29sKGNwdXMpCiAgICBpbnB1dF9iYXNlID0gb3MucGF0aC5iYXNlbmFtZShpbnB1dF9maWxlKQogICAgaW5wdXRfZGlyID0gb3MucGF0aC5kaXJuYW1lKGlucHV0X2ZpbGUpCiAgICBpbnB1dF9uYW1lID0gb3MucGF0aC5zcGxpdGV4dChpbnB1dF9iYXNlKVswXQogICAgb3V0cHV0X2RpcmVjdG9yeSA9IGYie2lucHV0X2Rpcn0vb3V0cHV0cy97aW5wdXRfbmFtZX1fc3Vic3RhY2siCiAgICBvcy5tYWtlZGlycyhvdXRwdXRfZGlyZWN0b3J5LCBleGlzdF9vayA9IFRydWUpCiAgICBwcmludCgiU3RhcnRpbmcgdG8gb3BlbiB0aGUgaW5wdXQgZmlsZSwgdGhpcyB0YWtlcyBhIG1vbWVudC4iKQogICAgZGF0YXNldCA9IGlqLmlvKCkub3BlbihpbnB1dF9maWxlKQogICAgcHJpbnQoZiJPcGVuZWQgaW5wdXQgZmlsZSwgd3JpdGluZyBpbWFnZXMgdG8ge291dHB1dF9kaXJlY3Rvcnl9IikKCiAgICBkYXRhX2luZm8gPSB7fQogICAgZm9yIGVsZW1lbnQgaW4gcmFuZ2UobGVuKGRhdGFzZXQuZGltcykpOgogICAgICAgIG5hbWUgPSBkYXRhc2V0LmRpbXNbZWxlbWVudF0KICAgICAgICBkYXRhX2luZm9bbmFtZV0gPSBkYXRhc2V0LnNoYXBlW2VsZW1lbnRdCiAgICBwcmludChmIlRoaXMgZGF0YXNldCBoYXMgZGltZW5zaW9uczogWDp7ZGF0YV9pbmZvWydYJ119IFk6e2RhdGFfaW5mb1snWSddfSBaOntkYXRhX2luZm9bJ1onXX0gVGltZTp7ZGF0YV9pbmZvWydUaW1lJ119IikKCiAgICBkZWYgc2F2ZV9zbGljZSh0aW1lcG9pbnQpOgogICAgICAgIHdhbnRlZF9zbGljZSA9IGRhdGFzZXRbOiwgOiwgd2FudGVkX2NoYW5uZWwsIHdhbnRlZF96LCB0aW1lcG9pbnRdCiAgICAgICAgc2xpY2VfaW1hZ2UgPSBpai5weS50b19kYXRhc2V0KHdhbnRlZF9zbGljZSkKICAgICAgICBvdXRwdXRfZmlsZW5hbWUgPSBmIntvdXRwdXRfZGlyZWN0b3J5fS97aW5wdXRfbmFtZX1fe3RpbWVwb2ludH0udGlmIgogICAgICAgIGlmIChvcy5wYXRoLmV4aXN0cyhvdXRwdXRfZmlsZW5hbWUpKToKICAgICAgICAgICAgaWYgb3ZlcndyaXRlOgogICAgICAgICAgICAgICAgcHJpbnQoZiJSZXdyaXRpbmcge291dHB1dF9maWxlbmFtZX0iKQogICAgICAgICAgICAgICAgb3MucmVtb3ZlKG91dHB1dF9maWxlbmFtZSkKICAgICAgICAgICAgICAgIHNhdmVkID0gaWouaW8oKS5zYXZlKHNsaWNlX2ltYWdlLCBvdXRwdXRfZmlsZW5hbWUpCiAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICBwcmludChmIlNraXBwaW5nIHtvdXRwdXRfZmlsZW5hbWV9LCBpdCBhbHJlYWR5IGV4aXN0cy4iKQogICAgICAgIGVsc2U6CiAgICAgICAgICAgIHNhdmVkID0gaWouaW8oKS5zYXZlKHNsaWNlX2ltYWdlLCBvdXRwdXRfZmlsZW5hbWUpCiAgICAgICAgICAgIHByaW50KGYiU2F2aW5nIGltYWdlIHtpbnB1dF9uYW1lfV97dGltZXBvaW50fS4iKQogICAgICAgIHJldHVybiB3YW50ZWRfc2xpY2UKCiAgICBzbGljZXMgPSBbXQogICAgZm9yIHRpbWVwb2ludCBpbiByYW5nZShkYXRhX2luZm9bJ1RpbWUnXSk6CiAgICAgICAgc2F2ZWQgPSBzYXZlX3NsaWNlKHRpbWVwb2ludCkKICAgICAgICBzbGljZXMuYXBwZW5kKHNhdmVkKQogICAgICAgICMjIEkgd2FudCB0byBzZWUgaWYgSSBjYW4gc3BsaXQgdGhlIHNhdmVzIGFjcm9zcyA4IGNwdXMuCiAgICAgICAgIyMgcG9vbC5hcHBseShzYXZlX3NsaWNlLCBhcmdzID0gKHRpbWVwb2ludCwgKSkKICAgICMjIHBvb2wuY2xvc2UoKQogICAgcmV0dXJuIGRhdGFzZXQsIHNsaWNlcywgb3V0cHV0X2RpcmVjdG9yeQoKcmF3X3NwbGl0LCBzYXZlZF9zbGljZXMsIG91dHB1dF9kaXJlY3RvcnkgPSBzZXBhcmF0ZV9zbGljZXMoaW5wdXRfZmlsZSkKYGBgCgojIyBDZWxscG9zZQoKQXQgdGhpcyBwb2ludCB3ZSBzaG91bGQgaGF2ZSBhIGRpcmVjdG9yeSBjb250YWluaW5nIGZpbGVzIG9mCmluZGl2aWR1YWwgdGltZXBvaW50cy4gIEphY3F1ZXMgc2VudCBtZSBhbiBpbml0aWFsIGltcGxlbWVudGF0aW9uIG9mCnRoZSB1c2FnZSBvZiBjZWxscG9zZSB0byBjYWxsIGluZGl2aWR1YWwgY2VsbHMuICBMZXQgdXMgaW5jbHVkZSB0aGF0Cm5vdy4gIEkgdGhpbmsgdGhlIHByZXZpb3VzIGZ1bmN0aW9uIHNob3VsZCBwcm9iYWJseSBhbHNvIHJldHVybiB0aGUKZGlyZWN0b3J5IG9mIHRoZSBzZXBhcmF0ZWQgaW5wdXQgZmlsZXMuCgpgYGB7cHl0aG9uIGNlbGxwb3NlLCBldmFsPUZBTFNFfQpkZWYgaW52b2tlX2NlbGxwb3NlKGlucHV0X2RpcmVjdG9yeSwgbW9kZWxfZmlsZSwgY2hhbm5lbHMgPSBbWzAsIDBdXSwgZGlhbWV0ZXIgPSAxNjAsCiAgICAgICAgICAgICAgICAgICAgdGhyZXNob2xkID0gMC40LCBkb18zRCA9IEZhbHNlLCBiYXRjaF9zaXplID0gNjQpOgogICAgIiIiIEludm9rZSBjZWxscG9zZSB1c2luZyB0aGUgc3BsaXQgZmlsZXMgZnJvbSB0aGUgcHJldmlvdXMgZnVuY3Rpb24uCgogICAgSmFjcXVlcyBtZW50aW9uZWQgdGhhdCB0aGlzIGlzIGdldHRpbmcgc2xpZ2h0bHkgZGlmZmVyZW50IHJlc3VsdHMgdGhhbiB0aGUKICAgIEdVSS1pbnZva2VkIHZlcnNpb24gb2YgdGhpcyBzYW1lIGZ1bmN0aW9uLiAgSSBmaWd1cmVkIHRoYXQgc2luY2UgSSBoYXZlIGEgbWludXRlLAogICAgSSBjYW4gcG9rZSBhdCB0aGlzIGFuZCBzZWUgaWYgSSBjYW4gdW5kZXJzdGFuZCB0aGUgcmVxdWlzaXRlcyBiZWZvcmUgaGUgYXJyaXZlcy4KICAgICIiIgoKICAgICMjIFJlbGV2YW50IG9wdGlvbnM6CiAgICAjIyBtb2RlbF90eXBlKGN5dG8sIG51Y2xlaSwgY3l0bzIpLCBuZXRfYXZnKFQvRiBpZiBsb2FkIGJ1aWx0IGluIG5ldHdvcmtzIGFuZCBhdmVyYWdlIHRoZW0pCiAgICBtb2RlbCA9IG1vZGVscy5DZWxscG9zZU1vZGVsKHByZXRyYWluZWRfbW9kZWwgPSBtb2RlbF9maWxlKQogICAgZmlsZXMgPSBnZXRfaW1hZ2VfZmlsZXMoaW5wdXRfZGlyZWN0b3J5LCAnX21hc2tzJywgbG9va19vbmVfbGV2ZWxfZG93biA9IEZhbHNlKQogICAgaW1ncyA9IFtpbXJlYWQoZikgZm9yIGYgaW4gZmlsZXNdCiAgICBuaW1nID0gbGVuKGltZ3MpCiAgICBwcmludChmIlJlYWQge25pbWd9IGltYWdlcywgc3RhcnRpbmcgY2VsbHBvc2UuIikKICAgICMjIFJlbGV2YW50IG9wdGlvbnM6CiAgICAjIyBiYXRjaF9zaXplKGluY3JlYXNlIGZvciBtb3JlIHBhcmFsbGVsaXphdGlvbiksIGNoYW5uZWxzKHR3byBlbGVtZW50IGxpc3Qgb2YgdHdvIGVsZW1lbnQKICAgICMjIGNoYW5uZWxzIHRvIHNlZ21lbnQ7IHRoZSBmaXJzdCBpcyB0aGUgc2VnbWVudCwgc2Vjb25kIGlzIG9wdGlvbmFsIG51Y2xldXM7CiAgICAjIyBpbnRlcm5hbCBlbGVtZW50cyBhcmUgY29sb3IgY2hhbm5lbHMgdG8gcXVlcnksIHNvIFtbMCwwXSxbMiwzXV0gbWVhbnMgZG8gbWFpbiBjZWxscyBpbgogICAgIyMgZ3JheXNjYWxlIGFuZCBhIHNlY29uZCB3aXRoIGNlbGxzIGluIGJsdWUsIG51Y2xlaSBpbiBncmVlbi4KICAgICMjIGNoYW5uZWxfYXhpcywgel9heGlzID8gaW52ZXJ0IChUL0YgZmxpcCBwaXhlbHMgZnJvbSBiL3cgSSBhc3N1bWUpLAogICAgIyMgbm9ybWFsaXplKFQvRiBwZXJjZW50aWxlIG5vcm1hbGl6ZSB0aGUgZGF0YSksIGRpYW1ldGVyLCBkb18zZCwKICAgICMjIGFuaXNvdHJvcHkgKHJlc2NhbGluZyBmYWN0b3IgZm9yIDNkIHNlZ21lbnRhdGlvbiksIG5ldF9hdmcgKGF2ZXJhZ2UgbW9kZWxzKSwKICAgICMjIGF1Z21lbnQgPywgdGlsZSA/LCByZXNhbXBsZSwgaW50ZXJwLCBmbG93X3RocmVzaG9sZCwgY2VsbHByb2JfdGhyZXNob2xkIChpbnRlcmVzdGluZyksCiAgICAjIyBtaW5fc2l6ZSAodHVybmVkIG9mZiB3aXRoIC0xKSwgc3RpdGNoX3RocmVzaG9sZCA/LCByZXNjYWxlID8uCiAgICBvdXRwdXRfZmlsZXMgPSBkZWZhdWx0ZGljdChkaWN0KQogICAgZXhpc3RpbmdfZmlsZXMgPSAwCiAgICBmb3IgZmlsZW5hbWUgaW4gZmlsZXM6CiAgICAgICAgZl9uYW1lID0gb3MucGF0aC5zcGxpdGV4dChmaWxlbmFtZSlbMF0KICAgICAgICBvdXRwdXRfbWFzayA9IGYie3N0YXJ0X2Rpcn0ve2ZfbmFtZX1fY3BfbWFza3MucG5nIgogICAgICAgIG91dHB1dF90eHQgPSBmIntzdGFydF9kaXJ9L3tmX25hbWV9X2NwX291dGxpbmVzLnR4dCIKICAgICAgICBvdXRwdXRfZmlsZXNbZmlsZW5hbWVdWydpbnB1dF9maWxlJ10gPSBmIntzdGFydF9kaXJ9L3tmaWxlbmFtZX0iCiAgICAgICAgb3V0cHV0X2ZpbGVzW2ZpbGVuYW1lXVsnb3V0cHV0X21hc2snXSA9IG91dHB1dF9tYXNrCiAgICAgICAgb3V0cHV0X2ZpbGVzW2ZpbGVuYW1lXVsnb3V0cHV0X3R4dCddID0gb3V0cHV0X3R4dAogICAgICAgIG91dHB1dF9maWxlc1tmaWxlbmFtZV1bJ2V4aXN0cyddID0gRmFsc2UKICAgICAgICBpZiAob3MucGF0aC5leGlzdHMob3V0cHV0X2ZpbGVzW2ZpbGVuYW1lXVsnb3V0cHV0X3R4dCddKSk6CiAgICAgICAgICAgIGV4aXN0aW5nX2ZpbGVzID0gZXhpc3RpbmdfZmlsZXMgKyAxCiAgICAgICAgICAgIG91dHB1dF9maWxlc1tmaWxlbmFtZV1bJ2V4aXN0cyddID0gVHJ1ZQoKICAgIGlmIGV4aXN0aW5nX2ZpbGVzID4gMDoKICAgICAgICBwcmludChmIk91dCBvZiB7bmltZ30gb3V0cHV0IGZpbGVzLCB7ZXhpc3RpbmdfZmlsZXN9IGFscmVhZHkgZXhpc3QsIG5vdCBydW5uaW5nIGNlbGxwb3NlLiIpCiAgICBlbHNlOgogICAgICAgIG1hc2tzLCBmbG93cywgc3R5bGVzID0gbW9kZWwuZXZhbCgKICAgICAgICAgICAgaW1ncywgZGlhbWV0ZXIgPSBkaWFtZXRlciwgY2hhbm5lbHMgPSBjaGFubmVscywgZmxvd190aHJlc2hvbGQgPSB0aHJlc2hvbGQsCiAgICAgICAgICAgIGRvXzNEID0gZG9fM0QsIGJhdGNoX3NpemUgPSBiYXRjaF9zaXplKQogICAgICAgIGlvLnNhdmVfdG9fcG5nKGltZ3MsIG1hc2tzLCBmbG93cywgZmlsZXMpCiAgICBwcmludCgiRmluaXNoZWQgY2VsbHBvc2UsIHJldHVybmluZyBvdXRwdXQgZmlsZW5hbWVzLiIpCiAgICByZXR1cm4gb3V0cHV0X2ZpbGVzCgpvcy5jaGRpcihzdGFydF9kaXIpCm91dHB1dF9kaXJlY3RvcnkgPSAndGVzdF9kYXRhL291dHB1dHMvcmF3X3N1YnN0YWNrJwpjZWxscG9zZV9yZXN1bHQgPSBpbnZva2VfY2VsbHBvc2Uob3V0cHV0X2RpcmVjdG9yeSwgJ21vZGVscy9DUF8yMDIyMDUyM18xMDQwMTYnKQpgYGAKCiMgQ3JlYXRlIFJlZ2lvbnMgb2YgaW50ZXJlc3QgZnJvbSBjZWxscG9zZSBvdXRwdXRzCgpJbiBKYWNxdWVzIG5vdGVib29rLCBpdCBsb29rcyBsaWtlIGhlIG9ubHkgZXh0cmFjdHMgUk9JcyBmcm9tIG9uZSBvZgp0aGUgY2VsbHBvc2Ugc2xpY2VzLiAgSSBhbSBhc3N1bWluZyB0aGUgZ29hbCBpcyB0byBleHRlbmQgdGhpcyBhY3Jvc3MKYWxsIGltYWdlcz8KClRoZXJlIGlzIGFuIGltcG9ydGFudCBjYXZlYXQgdGhhdCBJIG1pc3NlZDogaW1hZ2VqIGNvbWVzIHdpdGggYQpweXRob24yLWJhc2VkIHNjcmlwdGluZyBsYW5ndWFnZSBmcm9tIHdoaWNoIGl0IGFwcGVhcnMgc29tZSBvZiBoaXMKY29kZSBpcyBjb21pbmcuICBBcyBhIHJlc3VsdCBJIHNob3VsZCBsb29rIGNhcmVmdWxseSBiZWZvcmUgdXNpbmcgaXQsCmFuZCBwYXkgY2xvc2UgYXR0ZW50aW9uIHRvIHRoZSBleGFtcGxlcyBwcm92aWRlZCBoZXJlIGZvciB0aGUgbW9zdAphcHByb3ByaWF0ZSB3YXlzIG9mIGludGVyYWN0aW5nIHdpdGggdGhlIFJPSSBtYW5hZ2VyIGV0YzoKCmh0dHBzOi8vZ2l0aHViLmNvbS9pbWFnZWovcHlpbWFnZWovYmxvYi9tYWluL2RvYy9leGFtcGxlcy9ibG9iX2RldGVjdGlvbl9pbnRlcmFjdGl2ZS5weQoKYGBge3B5dGhvbiBjZWxscG9zZV90b19yb2ksIGV2YWw9RkFMU0V9CiMgZ2V0IEltYWdlSiByZXNvdXJjZXMKUG9seWdvblJvaSA9IHNjeWphdmEuamltcG9ydCgnaWouZ3VpLlBvbHlnb25Sb2knKQpPdmVybGF5ID0gc2N5amF2YS5qaW1wb3J0KCdpai5ndWkuT3ZlcmxheScpClJlZ2lvbnMgPSBzY3lqYXZhLmppbXBvcnQoJ25ldC5pbWdsaWIyLnJvaS5SZWdpb25zJykKTGFiZWxSZWdpb25zID0gc2N5amF2YS5qaW1wb3J0KCduZXQuaW1nbGliMi5yb2kubGFiZWxpbmcuTGFiZWxSZWdpb25zJykKb3YgPSBPdmVybGF5KCkKZnJvbSBwYW5kYXMgaW1wb3J0IERhdGFGcmFtZQpvcy5jaGRpcihzdGFydF9kaXIpCgojIyBUaGUgZm9sbG93aW5nIGlzIGZyb20gYSBtaXggb2YgYSBjb3VwbGUgb2YgaW1wbGVtZW50YXRpb25zIEkgZm91bmQ6CiMjIGh0dHBzOi8vcHlpbWFnZWoucmVhZHRoZWRvY3MuaW8vZW4vbGF0ZXN0L0NsYXNzaWMtU2VnbWVudGF0aW9uLmh0bWwKIyMgYW4gYWx0ZXJuYXRpdmUgbWV0aG9kIG1heSBiZSB0YWtlbiBmcm9tOgojIyBodHRwczovL3B5aW1hZ2VqLnJlYWR0aGVkb2NzLmlvL2VuL2xhdGVzdC9DbGFzc2ljLVNlZ21lbnRhdGlvbi5odG1sI3NlZ21lbnRhdGlvbi13b3JrZmxvdy13aXRoLWltYWdlajIKIyMgTXkgZ29hbCBpcyB0byBwYXNzIHRoZSBST0kgcmVnaW9ucyB0byB0aGlzIGZ1bmN0aW9uIGFuZCBjcmVhdGUgYSBzaW1pbGFyIGRmLgoKZGVmIHNsaWNlc190b19yb2lfbWVhc3VyZW1lbnRzKGNlbGxwb3NlX3Jlc3VsdCwgc2F2ZWRfc2xpY2VzKToKICAgIG91dHB1dF9kaWN0ID0gY2VsbHBvc2VfcmVzdWx0CiAgICBjZWxscG9zZV9zbGljZXMgPSBsaXN0KGNlbGxwb3NlX3Jlc3VsdC5rZXlzKCkpCiAgICBzbGljZV9udW1iZXIgPSAwCiAgICBmb3Igc2xpY2VfbmFtZSBpbiBjZWxscG9zZV9zbGljZXM6CiAgICAgICAgb3V0cHV0X2RpY3Rbc2xpY2VfbmFtZV1bJ3NsaWNlX251bWJlciddID0gc2xpY2VfbnVtYmVyCiAgICAgICAgc2xpY2VfZGF0YSA9IHNhdmVkX3NsaWNlc1tzbGljZV9udW1iZXJdCiAgICAgICAgaW5wdXRfdGlmID0gY2VsbHBvc2VfcmVzdWx0W3NsaWNlX25hbWVdWydpbnB1dF9maWxlJ10KICAgICAgICBpbnB1dF90eHQgPSBjZWxscG9zZV9yZXN1bHRbc2xpY2VfbmFtZV1bJ291dHB1dF90eHQnXQogICAgICAgIGlucHV0X21hc2sgPSBjZWxscG9zZV9yZXN1bHRbc2xpY2VfbmFtZV1bJ291dHB1dF9tYXNrJ10KICAgICAgICBwcmludChmIlByb2Nlc3NpbmcgY2VsbHBvc2Ugb3V0bGluZToge2lucHV0X3R4dH0iKQogICAgICAgICMgY29udmVydCBEYXRhc2V0IHRvIEltYWdlUGx1cwogICAgICAgIGltcCA9IGlqLnB5LnRvX2ltYWdlcGx1cyhzbGljZV9kYXRhKQogICAgICAgIHJtID0gaWouUm9pTWFuYWdlci5nZXRSb2lNYW5hZ2VyKCkKICAgICAgICAjIyBUaGUgbG9naWMgZm9yIHRoaXMgd2FzIHRha2VuIGZyb206CiAgICAgICAgIyMgaHR0cHM6Ly9zdGFja292ZXJmbG93LmNvbS9xdWVzdGlvbnMvNzM4NDk0MTgvaXMtdGhlcmUtYW55LXdheS10by1zd2l0Y2gtaW1hZ2VqLW1hY3JvLWNvZGUtdG8tcHl0aG9uMy1jb2RlCiAgICAgICAgdHh0X2ZoID0gb3BlbihpbnB1dF90eHQsICdyJykKICAgICAgICBpai5JSi5ydW4oIlNldCBNZWFzdXJlbWVudHMuLi4iLCAiYXJlYSBtZWFuIG1pbiBjZW50cm9pZCBtZWRpYW4gc2tld25lc3Mga3VydG9zaXMgcmVkaXJlY3Q9Tm9uZSBkZWNpbWFsPTMiKQogICAgICAgIHJvaV9zdGF0cyA9IGRlZmF1bHRkaWN0KGxpc3QpCiAgICAgICAgZm9yIGxpbmUgaW4gdHh0X2ZoOgogICAgICAgICAgICB4eSA9IGxpbmUucnN0cmlwKCkuc3BsaXQoIiwiKQogICAgICAgICAgICB4eV9jb29yZHMgPSBbaW50KGVsZW1lbnQpIGZvciBlbGVtZW50IGluIHh5XQogICAgICAgICAgICB4X2Nvb3JkcyA9IFtpbnQoZWxlbWVudCkgZm9yIGVsZW1lbnQgaW4geHlbOjoyXV0KICAgICAgICAgICAgeV9jb29yZHMgPSBbaW50KGVsZW1lbnQpIGZvciBlbGVtZW50IGluIHh5WzE6OjJdXQogICAgICAgICAgICB4Y29vcmRzX2ppbnQgPSBKQXJyYXkoSkludCkoeF9jb29yZHMpCiAgICAgICAgICAgIHljb29yZHNfamludCA9IEpBcnJheShKSW50KSh5X2Nvb3JkcykKICAgICAgICAgICAgcG9seWdvbl9yb2lfaW5zdGFuY2UgPSBzY3lqYXZhLmppbXBvcnQoJ2lqLmd1aS5Qb2x5Z29uUm9pJykKICAgICAgICAgICAgcm9pX2luc3RhbmNlID0gc2N5amF2YS5qaW1wb3J0KCdpai5ndWkuUm9pJykKICAgICAgICAgICAgaW1wb3J0ZWRfcG9seWdvbiA9IHBvbHlnb25fcm9pX2luc3RhbmNlKHhjb29yZHNfamludCwgeWNvb3Jkc19qaW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGVuKHhfY29vcmRzKSwgaW50KHJvaV9pbnN0YW5jZS5QT0xZR09OKSkKICAgICAgICAgICAgaW1wLnNldFJvaShpbXBvcnRlZF9wb2x5Z29uKQogICAgICAgICAgICBpai5JSi5ydW4oaW1wLCAnTWVhc3VyZScsICcnKQogICAgICAgIHNsaWNlX3Jlc3VsdCA9IGlqLlJlc3VsdHNUYWJsZS5nZXRSZXN1bHRzVGFibGUoKQogICAgICAgIHNsaWNlX3RhYmxlID0gaWouY29udmVydCgpLmNvbnZlcnQoc2xpY2VfcmVzdWx0LCBzY3lqYXZhLmppbXBvcnQoJ29yZy5zY2lqYXZhLnRhYmxlLlRhYmxlJykpCiAgICAgICAgc2xpY2VfbWVhc3VyZW1lbnRzID0gaWoucHkuZnJvbV9qYXZhKHNsaWNlX3RhYmxlKQogICAgICAgIG91dHB1dF9kaWN0W3NsaWNlX25hbWVdWydtZWFzdXJlbWVudHMnXSA9IHNsaWNlX21lYXN1cmVtZW50cwogICAgICAgIGlqLklKLnJ1bignQ2xlYXIgUmVzdWx0cycpCiAgICAgICAgdHh0X2ZoLmNsb3NlKCkKICAgICAgICBpbXAuc2V0T3ZlcmxheShvdikKICAgICAgICBpbXAuZ2V0UHJvY2Vzc29yKCkucmVzZXRNaW5BbmRNYXgoKQogICAgICAgIHNsaWNlX251bWJlciA9IHNsaWNlX251bWJlciArIDEKICAgIHJldHVybiBvdXRwdXRfZGljdAoKc2xpY2VfbWVhc3VyZW1lbnRzID0gc2xpY2VzX3RvX3JvaV9tZWFzdXJlbWVudHMoY2VsbHBvc2VfcmVzdWx0LCBzYXZlZF9zbGljZXMpCmBgYAoKIyBDb252ZXJ0IHRoZSBzbGljZSBtZWFzdXJlbWVudHMgdG8gcGFuZGFzIGRmCgpzbGljZXNfdG9fcm9pX21lYXN1cmVtZW50cygpIHJldHVybnMgYSBkaWN0aW9uYXJ5IHdpdGgga2V5cyB3aGljaCBhcmUKdGhlIGZpbGVuYW1lcyBvZiBlYWNoIHJhdyB0aWYgZmlsZS4gIEVhY2ggZWxlbWVudCBvZiB0aGF0IGRpY3Rpb25hcnkKaXMgaW4gdHVybiBhIGRpY3Rpb25hcnkgY29udGFpbmluZyBzb21lIGluZm9ybWF0aW9uIGFib3V0IHRoZSBmaWxlcwphbG9uZyB3aXRoIGEgZGYgb2YgdGhlIG1lYXN1cmVtZW50cyBwcm92aWRlZCBieSBpbWFnZWouCgpNeSBsaXR0bGUgZ2VvcGFuZGFzIGZ1bmN0aW9uIGFzc3VtZXMgYSBzaW5nbGUgbG9uZyBkZiB3aXRoIHNvbWUKY29sdW1ucyB3aGljaCB0ZWxsIGl0IHdoaWNoIHRpbWVwb2ludC4gIFNvIGxldHMgbWFrZSBhIHF1aWNrIGZ1bmN0aW9uCnRvIGdpdmUgdGhhdCBoZXJlLiAgT1RPSCBpdCBtYXkgYmUgd2lzZXIvYmV0dGVyIHRvIG1ha2Ugc29tZSBjaGFuZ2VzCnRvIHNsaWNlc190b19yb2lfbWVhc3VyZW1lbnRzKCkgc28gdGhhdCBpdCByZXR1cm5zIHRoYXQgZm9ybWF0IGRmOyBidXQKc2luY2UgSSBhbSB1c2luZyB0aGlzIGFzIGEgbGVhcm5pbmcgZXhwZXJpZW5jZSB0byBnZXQgbW9yZSBjb21mb3J0YWJsZQp3aXRoIHB5dGhvbiBkYXRhIHN0cnVjdHVyZXMsIEkgd2lsbCBub3QgZG8gaXQgdGhhdCB3YXkuCgpgYGB7cHl0aG9uIGNvbnZlcnRfc2xpY2VzX3RvX3BhbmRhcywgZXZhbD1GQUxTRX0KIyMgZGVmIGNvbnZlcnRfc2xpY2VzX3RvX3BhbmRhcyhzbGljZXMpOgoKY29uY2F0ZW5hdGVkID0gcGFuZGFzLkRhdGFGcmFtZSgpCnNsaWNlX2tleXMgPSBsaXN0KHNsaWNlX21lYXN1cmVtZW50cy5rZXlzKCkpCnNsaWNlX2NvdW50ZXIgPSAwCmZvciBrIGluIHNsaWNlX2tleXM6CiAgICBzbGljZV9jb3VudGVyID0gc2xpY2VfY291bnRlciArIDEKICAgIGN1cnJlbnRfc2xpY2UgPSBzbGljZV9tZWFzdXJlbWVudHNba10KICAgIHByaW50KGYiVGhlIHNsaWNlIGlzIHtrfSIpCiAgICBzbGljZV9udW1iZXIgPSBjdXJyZW50X3NsaWNlWydzbGljZV9udW1iZXInXQogICAgc2xpY2VfZGF0YSA9IGN1cnJlbnRfc2xpY2VbJ21lYXN1cmVtZW50cyddCiAgICBzbGljZV9kYXRhWydzbGljZV9udW1iZXInXSA9IHNsaWNlX251bWJlcgogICAgaWYgKHNsaWNlX2NvdW50ZXIgPT0gMSk6CiAgICAgICAgY29uY2F0ZW5hdGVkID0gc2xpY2VfZGF0YQogICAgZWxzZToKICAgICAgICBjb25jYXRlbmF0ZWQgPSBwYW5kYXMuY29uY2F0KFtjb25jYXRlbmF0ZWQsIHNsaWNlX2RhdGFdKQpgYGAKCiMgQ3JlYXRlIGNlbGwgZ3JvdXBzCgojIyBMb2FkIHRoZSBUU1YgZGF0YQoKSW4gdGhlIGFib3ZlIFIgaW1wbGVtZW50YXRpb24sIEkgZHVtcGVkIHRoZSBkYXRhIHRvIGEgdHN2IGZpbGUsIGFuZAp3aWxsIGp1c3QgcmVhZCB0aGF0IGhlcmUgZm9yIHNpbXBsaWNpdHkuCgpXaGlsZSBJIGFtIHRlc3RpbmcsIEkgd2lsbCBleHRyYWN0IG9ubHkgdGhlIGNvbHVtbnMgY29udGFpbmluZyB0aGUgSUQsCngsIHksIGFuZCB0aW1lKEZyYW1lKS4KCiMgTG9hZCB0aGUgY29vcmRpbmF0ZXMgaW50byBhIGdlb3NwYXRpYWwgZGlzdGFuY2UgbWV0aG9kCgpnZW9wYW5kYXMgbG9va3MgbGlrZSBpdCBoYW5kbGVzIGNvb3JkaW5hdGUgZGF0YSBpbiBhIHNpbWlsYXIgZmFzaGlvbgp0byBSJ3Mgc3BhdHN0YXQuZ2VvbS4gSSBhbSBub3Qgc3VyZSBpZiBpdCB3aWxsIGJlIGFibGUgdG8KaGFuZGxlIGEgei1zdGFjazsgYnV0IEkgYXNzdW1lIGl0IHdpbGwgcHJvdmUgZWFzeSB0byBleHRlbmQgdGhlCmltcGxlbWVudGF0aW9uIHRvIHBvaW50c19mcm9tX3h5eigpLgoKYGBge3B5dGhvbiBnZW9zcGF0aWFsLCBldmFsPUZBTFNFfQpnZGYgPSBnZW9wYW5kYXMuR2VvRGF0YUZyYW1lKAogICAgY29uY2F0ZW5hdGVkLAogICAgZ2VvbWV0cnkgPSBnZW9wYW5kYXMucG9pbnRzX2Zyb21feHkoY29uY2F0ZW5hdGVkLlgsIGNvbmNhdGVuYXRlZC5ZKSkKYGBgCgpJIHRoaW5rIHRoZSBlYXNpZXN0IHdheSB0byBleHRyYWN0IG1pbmltdW0gZGlzdGFuY2VzIGluIHB5dGhvbiBpcyB0bwpzcGxpdCB0aGlzIGRmIGludG8gcGFpcnMgYnkgRnJhbWUgZm9sbG93ZWQgYnkgdXNpbmcgdGhlCnNqb2luX25lYXJlc3QoKSBmdW5jdGlvbiB0byBmaW5kIG5lYXJlc3QgcG9pbnRzIGZyb20gYS0+Yi4KCiMjIHN1YnNldCB0aGUgZ2RmIGRhdGEKCiBJbiB0aGUgZm9sbG93aW5nIGJsb2NrLCBJIHdpbGwgaXRlcmF0ZSBvdmVyIGV2ZXJ5IHRpbWUgZnJvbSAxIHRvCiBlbmQtMSBhbmQgZXh0cmFjdCB0aGUgZWxlbWVudHMgb2YgdGhlIGdkZiBmcm9tIHRob3NlIHR3byB0aW1lIHBvaW50cy4KIFRoZW4gSSB3aWxsIHVzZSB0aGUgc2pvaW5fbmVhcmVzdCgpIHRvIGdldCBtZSBldmVyeSB0aW1lLXBhaXJ3aXNlCiBjbG9zZXN0IGRpc3RhbmNlIGFzIGEgbmV3IGRhdGFmcmFtZS4KCiBUaGVzZSBwYWlyd2lzZSBkaXN0YW5jZSBkYXRhZnJhbWVzIHdpbGwgYmUgYXBwZW5kZWQgdG8gdGhlIGVwb255bW91cwogcGFpcndpc2VfZGlzdGFuY2VzIGFycmF5OyB3aGljaCB1cG9uIGNvbXBsZXRpb24gd2lsbCBjb21wcmlzZSBuLTEKIGVsZW1lbnRzIHdoZXJlIHRoZSBmaXJzdCBlbGVtZW50IGluY2x1ZGVzIHRoZSByZXN1bHRzIGZvciB0aW1lcG9pbnRzCiAxLDIgYW5kIHRoZSBsYXN0IGVsZW1lbnQgaW5jbHVkZXMgbi0xLG4uCgpgYGB7cHl0aG9uIGdkZl9zdWJzZXQsIGV2YWw9RkFMU0V9CmZpbmFsX3RpbWUgPSBnZGYuRnJhbWUubWF4KCkKcGFpcndpc2VfZGlzdGFuY2VzID0gW10KZm9yIHN0YXJ0X3RpbWUgaW4gcmFuZ2UoMSwgZmluYWxfdGltZSk6CiAgICBpID0gc3RhcnRfdGltZQogICAgaiA9IGkgKyAxCiAgICB0aV9pZHggPSBnZGYuRnJhbWUgPT0gaQogICAgdGpfaWR4ID0gZ2RmLkZyYW1lID09IGoKICAgIHByaW50KCJHZXR0aW5nIGRpc3RhbmNlcyBvZiBkZnMgIiwgaSwgIiBhbmQgIiwgaiwgIi4iKQogICAgdGkgPSBnZGZbdGlfaWR4XQogICAgdGogPSBnZGZbdGpfaWR4XQogICAgdGlfcm93cyA9IHRpLnNoYXBlWzBdCiAgICB0al9yb3dzID0gdGouc2hhcGVbMF0KICAgIHRpdGogPSBnZW9wYW5kYXMuc2pvaW5fbmVhcmVzdCh0aSwgdGosIGRpc3RhbmNlX2NvbCA9ICJwYWlyd2lzZV9kaXN0IikKICAgIHBhaXJ3aXNlX2Rpc3RhbmNlcy5hcHBlbmQodGl0aikKYGBgCgojIyBGaWx0ZXIgdGhlIHBhaXJ3aXNlIGRpc3RhbmNlcyBhbmQgZmluZCBuZWlnaGJvcnMgb3ZlciB0aW1lCgpHaXZlbiB0aGUgcGFpcndpc2VfZGlzdGFuY2VzIGFycmF5LCBJIHNob3VsZCBiZSBhYmxlIHRvIGl0ZXJhdGUgb3ZlcgppdCBhbmQgbWFrZSBhIHNlcmllcyBvZiBhcnJheXMgY29udGFpbmluZyB0aGUgY2VsbCBJRHMgd2hpY2ggYXJlCmNvbXBvc2VkIG9mIGNlbGxzIHdoaWNoIGFyZSBjbG9zZXN0IHRvIGVhY2ggb3RoZXIgb3ZlciB0aW1lLgoKVGhlIHByaW1hcnkgY2F2ZWF0IGhlcmUgaXMgdGhhdCB3ZSBrbm93IHRoYXQgc29tZXRpbWVzIHRoZSBjZWxscyBhcmUKbWVyZ2VkIG9yIG90aGVyIHNoZW5hbmlnYW5zIGhhcHBlbiB3aGljaCBjYXVzZSB0aGUgY2xvc2VzdCBkaXN0YW5jZSB0bwpnZXQgcmVsYXRpdmVseSBsYXJnZSwgdGh1cyBJIHdpbGwgZmlsdGVyIG91dCBhbnkgcGFpcndpc2UgZGlzdGFuY2VzCndoaWNoIGFyZSBncmVhdGVyIHRoYW4gbWF4X2Rpc3QuCgpBcyBhIHJlc3VsdCwgdGhlIG51bWJlciBvZiBjZWxscyBvYnNlcnZlZCBvdmVyIHRpbWUgd2lsbCBieSBkZWZpbml0aW9uCm5vdCBiZSB0aGUgfiAyMCBvYnNlcnZlZCBpbiB0aGUgMTIxIHRpbWVwb2ludHMsIGJ1dCB3aWxsIGJlIHNvbWUKbGFyZ2VyIG51bWJlciBjb21wcmlzZWQgb2YgdGhlIHN1bSBvdmVyIHRpbWUuICBBIGxpa2VseSBuZXh0IHN0ZXAgd2lsbApiZSB0byBtZXJnZSB0aGVzZSBwYXJ0aWFsIGdyb3VwcyBwb3N0LWZhY3RvLCBidXQgbm90IHJpZ2h0IG5vdywgSSBqdXN0CndhbnQgdG8gZ2V0IHRoaXMgaW5pdGlhbCBpbXBsZW1lbnRhdGlvbiB3b3JraW5nLgoKSGVyZSBpcyB0aGUgbG9naWM6CgoxLiAgVXNlIGlkX2NvdW50ZXIgdG8gbWFrZSBhIGdsb2JhbCBjZWxsIElEIGFjcm9zcyBhbGwgdGltZXBvaW50cy4KMi4gIFVzZSB0cmFjZWR7fSB0byBhc3NvY2lhdGUgdGhlIGlkX2NvdW50ZXIgY2VsbCBJRCB0byBhbiBhcnJheSBvZgogICAgb2JzZXJ2ZWQgY2VsbHMgb3ZlciB0aW1lLgozLiAgVXNlIGVuZHN7fSB0byBhc3NvY2lhdGUgZXZlcnkga25vd24gY3VycmVudCBlbmRwb2ludCB3aXRoIGEKICAgIGlkX2NvdW50ZXIgY2VsbCBJRCBzbyB0aGF0IHdlIGNhbiBrZWVwIHRyYWNrIG9mIHdobyBpcyB3aG8uCjQuICBUaGVuIGl0ZXJhdGUgb3ZlciBldmVyeSBlbGVtZW50IG9mIHBhaXJ3aXNlX2Rpc3RhbmNlcyBhbmQgZG8gdGhlIGZvbGxvd2luZzoKICBhLiAgZHJvcCBlbGVtZW50cyB3aXRoIGRpc3RhbmNlcyA+IG1heF9kaXN0LgogIGIuICBjaGVjayB0byBzZWUgaWYgdGhlIGVhcmx5LXRpbWVwb2ludCBuZWFyZXN0IG5laWdoYm9yCiAgICAgIChzdGFydF9jZWxsKSBoYXMgYmVlbiBzZWVuIGluIGVuZHN7fToKICAgICogSWYgc28gYWRkIGVuZF9jZWxsIHRvIGl0IGFuZCB0byB0aGUgYXBwcm9wcmlhdGUgZWxlbWVudCBvZiB0cmFjZWR7fS4KICAgICogSWYgbm90LCBpbml0aWFsaXplIHRyYWNlZHt9IHdpdGggdGhlIHN0YXJ0L2VuZCBhbmQgYWRkIGl0IHRvIGVuZHN7fS4KClVwb24gY29tcGxldGlvbiBJIHNob3VsZCBoYXZlIGEgZGljdGlvbmFyeSBvZiBhcnJheXMgY29udGFpbmluZyBhbGwgb2YKdGhlIGNvbnRpdWdvdXMgbmVhcmVzdC1uZWlnaGJvciBjZWxscyB0aGF0IHN0YXllZCB3aXRoaW4gbWF4X2Rpc3Qgb2YKZWFjaCBvdGhlci4KCmBgYHtweXRob24gZXh0cmFjdF9jbG9zZXN0LCBldmFsPUZBTFNFfQojIyBTdGFydCBvdmVyIGlmIHRoZSBkaXN0YW5jZSBiZXR3ZWVuIHR3byBjZWxscyBpcyBiaWdnZXIgdGhhbiAxMCB1bml0cy4KbWF4X2Rpc3QgPSAxMC4wCiMjIFN0YXJ0IG92ZXIgaWYgdGhlIHByb3BvcnRpb24gYmV0d2VlbiB0aGUgYmlnZ2VyIGFuZCBzbWFsbGVyIGNlbGwgYXJlYSBoYXMgY2hhbmdlZAojIyBieSBtb3JlIHRoYW4gYSBmYWN0b3Igb2YgMC4yNS4KbWF4X3Byb3AgPSAwLjcKaWRfY291bnRlciA9IDAKIyMgQ2VsbCBJRHMgcG9pbnRpbmcgdG8gYSBsaXN0IG9mIGNlbGxzCnRyYWNlZCA9IHt9CiMjIEVuZHBvaW50cyBwb2ludGluZyB0byB0aGUgY2VsbCBJRHMKZW5kcyA9IHt9CmZvciBpIGluIHJhbmdlKDAsIGZpbmFsX3RpbWUgLSAxKToKICAgIHF1ZXJ5ID0gcGFpcndpc2VfZGlzdGFuY2VzW2ldCgogICAgcGFzc2VkX2lkeCA9IHF1ZXJ5LnBhaXJ3aXNlX2Rpc3QgPD0gbWF4X2Rpc3QKICAgIGZhaWxlZF9pZHggPSBxdWVyeS5wYWlyd2lzZV9kaXN0ID4gbWF4X2Rpc3QKICAgIGlmIChmYWlsZWRfaWR4LnN1bSgpID4gMCk6CiAgICAgICAgcHJpbnQoIlNraXBwZWQgIiwgZmFpbGVkX2lkeC5zdW0oKSwgIiBlbGVtZW50cyBpbiBzZWdtZW50ICIsIGksCiAgICAgICAgICAgICAgIiBiZWNhdXNlIGRpc3RhbmNlIGlzIHRvbyBsYXJnZS4iKQogICAgICAgIHF1ZXJ5ID0gcXVlcnlbcGFzc2VkX2lkeF0KCiAgICBwcm9wX2NoYW5nZSA9IHF1ZXJ5LkFyZWFfbGVmdCAvIHF1ZXJ5LkFyZWFfcmlnaHQKICAgIGluY3JlYXNlZF9pZHggPSBwcm9wX2NoYW5nZSA+IDEuMAogICAgcHJvcF9jaGFuZ2VbaW5jcmVhc2VkX2lkeF0gPSAxLjAgLyBwcm9wX2NoYW5nZVtpbmNyZWFzZWRfaWR4XQogICAgZmFpbGVkX2lkeCA9IHByb3BfY2hhbmdlIDwgbWF4X3Byb3AKICAgIHBhc3NlZF9pZHggPSBwcm9wX2NoYW5nZSA+PSBtYXhfcHJvcAogICAgaWYgKGZhaWxlZF9pZHguc3VtKCkgPiAwKToKICAgICAgICBwcmludCgiU2tpcHBlZCAiLCBmYWlsZWRfaWR4LnN1bSgpLCAiIGVsZW1lbnRzIGluIHNlZ21lbnQgIiwgaSwKICAgICAgICAgICAgICAiIGJlY2F1c2UgdGhlIHNpemUgY2hhbmdlZCB0b28gbXVjaC4iKQogICAgICAgIHF1ZXJ5ID0gcXVlcnlbcGFzc2VkX2lkeF0KCiAgICBmb3Igcm93IGluIHF1ZXJ5Lml0ZXJ0dXBsZXMoKToKICAgICAgICBzdGFydF9jZWxsID0gcm93LklEX2xlZnQKICAgICAgICBlbmRfY2VsbCA9IHJvdy5JRF9yaWdodAogICAgICAgIGlmIHN0YXJ0X2NlbGwgaW4gZW5kcy5rZXlzKCk6CiAgICAgICAgICAgIGNlbGxfaWQgPSBlbmRzW3N0YXJ0X2NlbGxdCiAgICAgICAgICAgIGN1cnJlbnRfdmFsdWUgPSB0cmFjZWRbY2VsbF9pZF0KICAgICAgICAgICAgY3VycmVudF92YWx1ZS5hcHBlbmQoZW5kX2NlbGwpCiAgICAgICAgICAgIHRyYWNlZFtjZWxsX2lkXSA9IGN1cnJlbnRfdmFsdWUKICAgICAgICAgICAgZW5kc1tlbmRfY2VsbF0gPSBjZWxsX2lkCiAgICAgICAgZWxzZToKICAgICAgICAgICAgaWRfY291bnRlciA9IGlkX2NvdW50ZXIgKyAxCiAgICAgICAgICAgIHRyYWNlZFtpZF9jb3VudGVyXSA9IFtzdGFydF9jZWxsLCBlbmRfY2VsbF0KICAgICAgICAgICAgZW5kc1tlbmRfY2VsbF0gPSBpZF9jb3VudGVyCgp0cmFjZWQKYGBgCgpPaywgSSBsb29rZWQgYXQgdGhlIHJlc3VsdCBvZiB0cmFjZWQgYW5kIGl0IHNlZW1zIHRvIG1ha2Ugc2Vuc2UgdG8gbWU7CmxldHMgcmV3cml0ZSB0aGlzIGFzIGEgc2luZ2xlIGZ1bmN0aW9uIHRoYXQgSmFjcXVlcyBjYW4gYWRkIHRvIGhpcwpub3RlYm9vay4gIEkgd2lsbCBhc3N1bWUgaGUgaGFzIGEgcGFuZGFzIGRmIGFzIGlucHV0LgoKIyMgR2V0IGluZm9ybWF0aW9uIGZyb20gYSBncm91cCBvZiBjZWxscwoKQXMgYSBmaW5hbCBzdGVwLCB3ZSBzaG91bGQgYmUgYWJsZSB0byBleHRyYWN0IGFuZCBwbGF5IHdpdGggdGhlCmluZm9ybWF0aW9uIGZyb20gb25lIG9yIG1vcmUgZ3JvdXBzIG9mIGNlbGxzLgoKYGBge3B5dGhvbiBnZXRfaW5mbywgZXZhbD1GQUxTRX0KY2VsbF9pZCA9IDYKY2VsbF9pZHggPSBpbnB1dFsnSUQnXS5pc2luKHRyYWNlZFtjZWxsX2lkXSkKY2VsbF9kYXRhID0gaW5wdXRbY2VsbF9pZHhdLnJlc2V0X2luZGV4KCkKCnBsdC5zY2F0dGVyKGNlbGxfZGF0YVsnWCddLCBjZWxsX2RhdGFbJ1knXSkKZmluYWxfcm93ID0gY2VsbF9kYXRhLkZyYW1lLm1heCgpCmZvciBzdGFydF90aW1lIGluIHJhbmdlKDEsIGZpbmFsX3JvdyAtIDEpOgogICAgdGlfaWR4ID0gY2VsbF9kYXRhLkZyYW1lID09IHN0YXJ0X3RpbWUKICAgIHRqX2lkeCA9IGNlbGxfZGF0YS5GcmFtZSA9PSBzdGFydF90aW1lICsgMQogICAgcDF4ID0gY2VsbF9kYXRhW3RpX2lkeF0uWAogICAgcDJ4ID0gY2VsbF9kYXRhW3RqX2lkeF0uWAogICAgcDF5ID0gY2VsbF9kYXRhW3RpX2lkeF0uWQogICAgcDJ5ID0gY2VsbF9kYXRhW3RqX2lkeF0uWQogICAgeF9wb2ludHMgPSBbcDF4LCBwMnhdCiAgICB5X3BvaW50cyA9IFtwMXksIHAyeV0KICAgIHBsdC5wbG90KHhfcG9pbnRzLCB5X3BvaW50cykKcGx0LnNob3coKQoKc2VhYm9ybi52aW9saW5wbG90KGRhdGEgPSBjZWxsX2RhdGEuQXJlYSkKcGx0LnNob3coKQpgYGAKCiMgU2luZ2xlIGZ1bmN0aW9uIGRlZmluaXRpb24KCk5vdyBsZXQgdXMgY29tYmluZSB0aGUgdGhpbmdzIGxlYXJuZWQgYWJvdmUgaW50byBhIHNpbmdsZSBmdW5jdGlvbi4KCmBgYHtweXRob24gbmVhcmVzdF9uZWlnaGJvcl9mdW5jdGlvbiwgZXZhbD1GQUxTRX0KZGVmIG5lYXJlc3RfY2VsbHNfb3Zlcl90aW1lKGRmLCBtYXhfZGlzdCA9IDEwLjAsIHhfY29sdW1uID0gJ1gnLCB5X2NvbHVtbiA9ICdZJyk6CiAgICAiIiJUcmFjZSBjZWxscyBvdmVyIHRpbWUiIiIKICAgIGdkZiA9IGdlb3BhbmRhcy5HZW9EYXRhRnJhbWUoCiAgICAgICAgZGYsCiAgICAgICAgZ2VvbWV0cnkgPSBnZW9wYW5kYXMucG9pbnRzX2Zyb21feHkoZGZbeF9jb2x1bW5dLCBkZlt5X2NvbHVtbl0pKQoKICAgIGZpbmFsX3RpbWUgPSBnZGYuRnJhbWUubWF4KCkKICAgIHBhaXJ3aXNlX2Rpc3RhbmNlcyA9IFtdCiAgICBmb3Igc3RhcnRfdGltZSBpbiByYW5nZSgxLCBmaW5hbF90aW1lKToKICAgICAgICBpID0gc3RhcnRfdGltZQogICAgICAgIGogPSBpICsgMQogICAgICAgIHRpX2lkeCA9IGdkZi5GcmFtZSA9PSBpCiAgICAgICAgdGpfaWR4ID0gZ2RmLkZyYW1lID09IGoKICAgICAgICBwcmludCgiR2V0dGluZyBkaXN0YW5jZXMgb2YgZGZzICIsIGksICIgYW5kICIsIGosICIuIikKICAgICAgICB0aSA9IGdkZlt0aV9pZHhdCiAgICAgICAgdGogPSBnZGZbdGpfaWR4XQogICAgICAgIHRpX3Jvd3MgPSB0aS5zaGFwZVswXQogICAgICAgIHRqX3Jvd3MgPSB0ai5zaGFwZVswXQogICAgICAgIHRpdGogPSBnZW9wYW5kYXMuc2pvaW5fbmVhcmVzdCh0aSwgdGosIGRpc3RhbmNlX2NvbCA9ICJwYWlyd2lzZV9kaXN0IikKICAgICAgICBwYWlyd2lzZV9kaXN0YW5jZXMuYXBwZW5kKHRpdGopCgogICAgbWF4X2Rpc3QgPSAxMC4wCiAgICBpZF9jb3VudGVyID0gMAogICAgIyMgQ2VsbCBJRHMgcG9pbnRpbmcgdG8gYSBsaXN0IG9mIGNlbGxzCiAgICB0cmFjZWQgPSB7fQogICAgIyMgRW5kcG9pbnRzIHBvaW50aW5nIHRvIHRoZSBjZWxsIElEcwogICAgZW5kcyA9IHt9CiAgICBmb3IgaSBpbiByYW5nZSgwLCBmaW5hbF90aW1lIC0gMSk6CiAgICAgICAgcXVlcnkgPSBwYWlyd2lzZV9kaXN0YW5jZXNbaV0KICAgICAgICBwYXNzZWRfaWR4ID0gcXVlcnkucGFpcndpc2VfZGlzdCA8PSBtYXhfZGlzdAogICAgICAgIGZhaWxlZF9pZHggPSBxdWVyeS5wYWlyd2lzZV9kaXN0ID4gbWF4X2Rpc3QKICAgICAgICBpZiAoZmFpbGVkX2lkeC5zdW0oKSA+IDApOgogICAgICAgICAgICBwcmludCgiU2tpcHBlZCAiLCBmYWlsZWRfaWR4LnN1bSgpLCAiIGVsZW1lbnRzIGluIHNlZ21lbnQgIiwgaSwgIi4iKQogICAgICAgIHF1ZXJ5ID0gcXVlcnlbcGFzc2VkX2lkeF0KICAgICAgICBmb3Igcm93IGluIHF1ZXJ5Lml0ZXJ0dXBsZXMoKToKICAgICAgICAgICAgc3RhcnRfY2VsbCA9IHJvdy5JRF9sZWZ0CiAgICAgICAgICAgIGVuZF9jZWxsID0gcm93LklEX3JpZ2h0CiAgICAgICAgICAgIGlmIHN0YXJ0X2NlbGwgaW4gZW5kcy5rZXlzKCk6CiAgICAgICAgICAgICAgICBjZWxsX2lkID0gZW5kc1tzdGFydF9jZWxsXQogICAgICAgICAgICAgICAgY3VycmVudF92YWx1ZSA9IHRyYWNlZFtjZWxsX2lkXQogICAgICAgICAgICAgICAgY3VycmVudF92YWx1ZS5hcHBlbmQoZW5kX2NlbGwpCiAgICAgICAgICAgICAgICB0cmFjZWRbY2VsbF9pZF0gPSBjdXJyZW50X3ZhbHVlCiAgICAgICAgICAgICAgICBlbmRzW2VuZF9jZWxsXSA9IGNlbGxfaWQKICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgIGlkX2NvdW50ZXIgPSBpZF9jb3VudGVyICsgMQogICAgICAgICAgICAgICAgdHJhY2VkW2lkX2NvdW50ZXJdID0gW3N0YXJ0X2NlbGwsIGVuZF9jZWxsXQogICAgICAgICAgICAgICAgZW5kc1tlbmRfY2VsbF0gPSBpZF9jb3VudGVyCgogICAgcmV0dXJuIHRyYWNlZApgYGAKCmBgYHtyIHNhdmVtZX0KcGFuZGVyOjpwYW5kZXIoc2Vzc2lvbkluZm8oKSkKbWVzc2FnZShwYXN0ZTAoIlRoaXMgaXMgaHBnbHRvb2xzIGNvbW1pdDogIiwgZ2V0X2dpdF9jb21taXQoKSkpCnRoaXNfc2F2ZSA8LSBwYXN0ZTAoZ3N1YihwYXR0ZXJuPSJcXC5SbWQiLCByZXBsYWNlPSIiLCB4PXJtZF9maWxlKSwgIi12IiwgdmVyLCAiLnJkYS54eiIpCm1lc3NhZ2UocGFzdGUwKCJTYXZpbmcgdG8gIiwgdGhpc19zYXZlKSkKdG1wIDwtIHNtKHNhdmVtZShmaWxlbmFtZT10aGlzX3NhdmUpKQpgYGAK