Presentación
En este proyecto se analizará la información de una base de datos referente a créditos a personas físicas, con el objetivo de identificar los factores que contribuyen en mayor medida a predecir la pérdida o fuga de clientes, así como estimar el periodo de tiempo antes de que ocurra el abandono.
Se iniciará con un análisis exploratorio para conocer la estructura de la información así como también identificar si existen valores faltantes. Lo anterior, servirá para irnos familiarizando con la información.
Se utilizarán varios algoritmos de Aprendizaje Supervisado para precedir la deserción, ya que es recomendable hacer comparaciones entre varios modelos, además, se irán identificando los factores más importantes que explican la fuga de clientes. También, se creará un modelo de ensamble como solución alternativa y se compararán sus resultados con los del mejor modelo.
Finalmente, se construirá un modelo para predecir el tiempo o momento de abandono mediante un análisis de supervivencia.
Los datos para este análisis provienen del repositorio de aprendizaje automático de kaggle, que es un sitio web para la comunidad de data science, donde se pueden encontrar bases de datos para practicar y competir en proyectos. También, se encuentran en mi repositorio de github junto con el código de esta publicación.
Contenido
- Presentación
- Contenido
- Set up
- Carga y preparación de los datos
- Análisis exploratorio
- Pre procesamiento de los datos
- Selección de variables
- Codificación de variables categóricas
- Escalamiento
- Balanceo de clases
- Métrica de evaluación
- Creación de modelos de aprendizaje automático
- Modelo base: Naive Bayes
- Regresión Logística
- Máquinas de Vectores Soporte
- Random Forest
- Extreme Gradient Boosting (XGBoost)
- Redes Neuronales (RNN)
- Gradient Boosting Machine (GBM)
- Comparación de modelos
- Evaluación con datos de validación
- Importancia de las variables
- Ensamble
- Elección del valor de corte adecuado
- Evaluación final: datos de test
- Identificando el tiempo de abandono del cliente
- Conclusiones
Set up
Iniciamos configurando las opciones generales que vamos a requerir para el desarrollo de este proyecto.
knitr::opts_chunk$set(echo = TRUE,
warning = FALSE,
message = FALSE,
warning = FALSE,
fig.align = "center"
)
paquetes <- c("knitr","tidyverse","ggplot2","gridExtra","DataExplorer","lubridate",
"scales","rpart","rpart.plot","kableExtra", "summarytools", "DT", "ggpubr",
"sfo", "plyr", "plotly", "psych", "janitor", "purrr", "caret", "ROSE", "klaR",
"parallel", "doSNOW", "recipes", "ranger", "randomForest", "ggthemes", "ROCR",
"sjPlot", "survival", "survminer", "pec")
instalados <- paquetes %in% installed.packages()
if(sum(instalados == FALSE) > 0) {
install.packages(paquetes[!instalados])
}
lapply(paquetes, require, character.only = TRUE)
# se cargan funciones
source("cutoff.R")
Carga y preparación de los datos
Comencemos por cargar el conjunto de datos y hacer un primer análisis descriptivo básico para tener una idea de su tamaño y el tipo de cada variable.
creditos <- read_csv("Churn_Modelling.csv")
creditos.raw <- creditos # para usarse al final con el modelo de permanencia
str(creditos)
## spec_tbl_df [10,000 x 14] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
## $ RowNumber : num [1:10000] 1 2 3 4 5 6 7 8 9 10 ...
## $ CustomerId : num [1:10000] 15634602 15647311 15619304 15701354 15737888 ...
## $ Surname : chr [1:10000] "Hargrave" "Hill" "Onio" "Boni" ...
## $ CreditScore : num [1:10000] 619 608 502 699 850 645 822 376 501 684 ...
## $ Geography : chr [1:10000] "France" "Spain" "France" "France" ...
## $ Gender : chr [1:10000] "Female" "Female" "Female" "Female" ...
## $ Age : num [1:10000] 42 41 42 39 43 44 50 29 44 27 ...
## $ Tenure : num [1:10000] 2 1 8 1 2 8 7 4 4 2 ...
## $ Balance : num [1:10000] 0 83808 159661 0 125511 ...
## $ NumOfProducts : num [1:10000] 1 1 3 2 1 2 2 4 2 1 ...
## $ HasCrCard : num [1:10000] 1 0 1 0 1 1 1 1 0 1 ...
## $ IsActiveMember : num [1:10000] 1 1 0 0 1 0 1 0 1 1 ...
## $ EstimatedSalary: num [1:10000] 101349 112543 113932 93827 79084 ...
## $ Exited : num [1:10000] 1 0 1 0 0 1 0 1 0 0 ...
## - attr(*, "spec")=
## .. cols(
## .. RowNumber = col_double(),
## .. CustomerId = col_double(),
## .. Surname = col_character(),
## .. CreditScore = col_double(),
## .. Geography = col_character(),
## .. Gender = col_character(),
## .. Age = col_double(),
## .. Tenure = col_double(),
## .. Balance = col_double(),
## .. NumOfProducts = col_double(),
## .. HasCrCard = col_double(),
## .. IsActiveMember = col_double(),
## .. EstimatedSalary = col_double(),
## .. Exited = col_double()
## .. )
DT::datatable(head(creditos, 20),
rownames = FALSE,
options = list(
pageLength = 5,
scrollX = TRUE))
Tenemos una base de 10,000 registros y 14 columnas, de las cuales solo 3 son de tipo character y el resto numéricas.
La última característica, ‘Exited’, es la variable objetivo e indica si el cliente se ha ido o abandonado la institución (0 = No, 1 = Sí).
Análisis exploratorio
Empecemos el análisis exploratorio identificando si hay datos faltantes y si los hay debemos conocer cuántos son y en qué variables:
creditos %>%
summarise_all(~sum(is.na(.))) %>%
t()
## [,1]
## RowNumber 0
## CustomerId 0
## Surname 0
## CreditScore 0
## Geography 0
## Gender 0
## Age 0
## Tenure 0
## Balance 0
## NumOfProducts 0
## HasCrCard 0
## IsActiveMember 0
## EstimatedSalary 0
## Exited 0
plot_missing(creditos, title = "Porcentaje de Datos Incompletos",
geom_label_args = list("size" = 3, "label.padding" = unit(0.1, "lines")),
ggtheme = theme_minimal())

Los resultados anteriores y la gráfica muestran que en la base de datos no hay observaciones faltantes. Esto es buena noticia, ya que no tendremos que hacer ninguna imputación. Sin embargo, debemos checar, también, que no haya valores “NULL” en alguna parte de la base, porque suele pasar que dicha palabra esté escrita como texto, situación que no identificaría R
como un valor perdido.
nulls <- function(variable) {
sum(if_else(variable == 'NULL', 1, 0))
}
creditos %>%
mutate_all(as.character) %>%
lapply(., nulls)
## $RowNumber
## [1] 0
##
## $CustomerId
## [1] 0
##
## $Surname
## [1] 0
##
## $CreditScore
## [1] 0
##
## $Geography
## [1] 0
##
## $Gender
## [1] 0
##
## $Age
## [1] 0
##
## $Tenure
## [1] 0
##
## $Balance
## [1] 0
##
## $NumOfProducts
## [1] 0
##
## $HasCrCard
## [1] 0
##
## $IsActiveMember
## [1] 0
##
## $EstimatedSalary
## [1] 0
##
## $Exited
## [1] 0
Afortunadamente, tampoco tenemos la palabra “NULL” dentro de la base de datos, por lo tanto, podemos seguir con el análisis descriptivo.
Finalmente, comprobamos que no tenemos registros duplicados:
nrow(creditos[duplicated(creditos), ])
## [1] 0
Pasemos a ver algunos resúmenes estadísticos básicos de las variables numéricas para empezar a comprender mejor la información que tenemos:
creditos %>%
dplyr::select(-c("RowNumber", "CustomerId", "Surname", "Geography", "Gender")) %>%
describeBy() %>%
kable(
format = "html",
digits = 2,
size = 5,
caption = "Resumen Estadístico",
format.args = list(big.mark = ","),
table.attr = "style='width:50%;'") %>%
kable_styling(bootstrap_options = c("striped", "hover"))
Resumen Estadístico
|
vars
|
n
|
mean
|
sd
|
median
|
trimmed
|
mad
|
min
|
max
|
range
|
skew
|
kurtosis
|
se
|
CreditScore
|
1
|
10,000
|
650.53
|
96.65
|
652.00
|
651.01
|
99.33
|
350.00
|
850.0
|
500.0
|
-0.07
|
-0.43
|
0.97
|
Age
|
2
|
10,000
|
38.92
|
10.49
|
37.00
|
37.91
|
8.90
|
18.00
|
92.0
|
74.0
|
1.01
|
1.39
|
0.10
|
Tenure
|
3
|
10,000
|
5.01
|
2.89
|
5.00
|
5.01
|
2.97
|
0.00
|
10.0
|
10.0
|
0.01
|
-1.17
|
0.03
|
Balance
|
4
|
10,000
|
76,485.89
|
62,397.41
|
97,198.54
|
74,827.80
|
69,336.44
|
0.00
|
250,898.1
|
250,898.1
|
-0.14
|
-1.49
|
623.97
|
NumOfProducts
|
5
|
10,000
|
1.53
|
0.58
|
1.00
|
1.49
|
0.00
|
1.00
|
4.0
|
3.0
|
0.75
|
0.58
|
0.01
|
HasCrCard
|
6
|
10,000
|
0.71
|
0.46
|
1.00
|
0.76
|
0.00
|
0.00
|
1.0
|
1.0
|
-0.90
|
-1.19
|
0.00
|
IsActiveMember
|
7
|
10,000
|
0.52
|
0.50
|
1.00
|
0.52
|
0.00
|
0.00
|
1.0
|
1.0
|
-0.06
|
-2.00
|
0.00
|
EstimatedSalary
|
8
|
10,000
|
100,090.24
|
57,510.49
|
100,193.91
|
100,114.86
|
72,941.18
|
11.58
|
199,992.5
|
199,980.9
|
0.00
|
-1.18
|
575.10
|
Exited
|
9
|
10,000
|
0.20
|
0.40
|
0.00
|
0.13
|
0.00
|
0.00
|
1.0
|
1.0
|
1.47
|
0.16
|
0.00
|
De la tabla anterior, podemos identificar algunas cosas interesantes:
- El rango de edad oscila entre 18 y 92 años, con un promedio de 39, aproximadamente.
- La permanencia media es de 5 años, por lo que, la mayoría de los clientes son leales (mad de 3).
- Aproximadamente el 50% de los clientes están activos.
creditos %>%
group_by(Gender) %>%
summarise(num.customers = n(),
media.crScore = round(mean(CreditScore), 2),
media.age = round(mean(Age), 2),
media.tenure = round(mean(Tenure), 2),
media.balance = round(mean(Balance), 2),
media.salary = round(mean(EstimatedSalary), 2)) %>%
kable(
format = "html",
digits = 2,
align = "c",
caption = "Resumen Estadístico por Género",
format.args = list(big.mark = ","),
table.attr = "style='width:50%;'") %>%
kable_styling(bootstrap_options = c("striped", "hover"))
Resumen Estadístico por Género
Gender
|
num.customers
|
media.crScore
|
media.age
|
media.tenure
|
media.balance
|
media.salary
|
Female
|
4,543
|
650.83
|
39.24
|
4.97
|
75,659.37
|
100,601.54
|
Male
|
5,457
|
650.28
|
38.66
|
5.05
|
77,173.97
|
99,664.58
|
En este otro resumen estadístico por género podemos identificar:
- Hay más clientes hombres que mujeres.
- Las edades promedio de ambos géneros casi son iguales, siendo el de las mujeres 1 año mayor.
- El tiempo de permanencia para ambos sexos es, prácticamente, el mismo: 5 años.
- El saldo que mantienen ahorrado es mayor en los hombres.
- La media del salario es ligeramente mayor en el grupo de las mujeres.
Hay variables que son numéricas pero que deberían ser de tipo factor como, por ejemplo, la variable objetivo Exited. A continuación, se harán los cambios en los tipos, pero antes quitaremos las variables que no serán útiles para el análisis: RowNumber, CustomerId y Surname:
creditos <- creditos %>%
dplyr::select(-c(RowNumber, CustomerId, Surname))
a.factor <- c("Tenure", "NumOfProducts", "HasCrCard", "IsActiveMember", "Exited")
creditos <- creditos %>%
mutate_at(all_of(a.factor), as.factor)
Ahora, confirmamos que están bien los niveles de las variables que pasamos a tipo factor y de paso vemos sus proporciones:
levels(creditos$Exited)
## [1] "0" "1"
prop.table(table(creditos$Exited))
##
## 0 1
## 0.7963 0.2037
La proporción de clientes que abandonaron es: 20.37%
La proporción de clientes que permanecen es: 79.63%
levels(creditos$NumOfProducts)
## [1] "1" "2" "3" "4"
prop.table(table(creditos$NumOfProducts))
##
## 1 2 3 4
## 0.5084 0.4590 0.0266 0.0060
La proporción de clientes con 1 producto es: 50.84%
La proporción de clientes con 2 productos es: 45.9%
La proporción de clientes con 3 productos es: 2.66%
La proporción de clientes con 4 productos es: 0.6%
levels(creditos$HasCrCard)
## [1] "0" "1"
prop.table(table(creditos$HasCrCard))
##
## 0 1
## 0.2945 0.7055
La proporción de clientes con tarjeta de crédito es: 70.55%
La proporción de clientes sin tarjeta de crédito es: 29.45%
levels(creditos$IsActiveMember)
## [1] "0" "1"
prop.table(table(creditos$IsActiveMember))
##
## 0 1
## 0.4849 0.5151
La proporción de los clientes activos es: 51.51%
La proporción de los clientes no activos es: 48.49%
Las variables que se pasaron a tipo factor están bien.
Variables categóricas
Se hará un análisis visual a las variables categóricas, para conocer sus distribuciones e identificar si presentan alta o baja cardinalidad.
creditos %>%
select_if(is.factor) %>%
mutate(id = 1:nrow(.)) %>%
pivot_longer(-id, names_to = 'variable', values_to = 'valor') %>%
ggplot(aes(valor)) +
geom_bar() +
scale_y_continuous(labels = scales::comma) +
facet_wrap(~variable, scales = 'free', ncol = 2)

creditos %>%
select_if(is.character) %>%
mutate(id = 1:nrow(.)) %>%
pivot_longer(-id, names_to = 'variable', values_to = 'valor') %>%
ggplot(aes(valor)) +
geom_bar() +
scale_y_continuous(labels = scales::comma) +
facet_wrap(~variable, scales = 'free', ncol = 2)

Las variables categóricas se ven bien, solo la variable NumOfProducts podríamos modificarla para juntar el producto 4 con el producto 3 y nombrar este grupo como “>2”
Ahora, veamos las distribuciones de las variables numéricas con apoyo de unos boxplots y de un resúmen estadístico básico.
Variables numéricas
creditos %>%
select_if(is.numeric) %>%
mutate(id = 1:nrow(.)) %>%
pivot_longer(-id, names_to = 'variable', values_to = 'valor') %>%
ggplot(aes(valor)) +
geom_boxplot(outlier.colour = "red", fill = '#A4A4A4', color = "darkred") +
scale_y_continuous(labels = scales::comma) +
facet_wrap(~variable, scales = 'free', ncol = 2)+
coord_flip()

creditos %>%
select_if(is.numeric) %>%
mutate(id = 1:nrow(.)) %>%
pivot_longer(-id, names_to = 'variable', values_to = 'valor') %>%
ggplot(aes(valor)) +
geom_histogram(fill = "steelblue", color = "firebrick") +
scale_x_continuous(labels = scales::comma) +
facet_wrap(~variable, scales = 'free', ncol = 2)

De los gráficos anteriores podemos identificar lo siguiente:
- La variable Age, presenta algunos valores atípicos que parecen corresponder a personas entre los 60 y 92 años.
- En la variable CreditScore, también, hay valores atípicos bajos (menores a 400 pts).
- La variable Balance, quitando el bloque de clientes que no tienen saldo, presenta una distibución normal.
- Finalmente, la variable EstimatedSalary presenta una distribución casi uniforme.
correlacion <- creditos %>%
select_if(is.numeric) %>%
cor() %>%
round(2)
corPlot(correlacion, cex = 1.8, main = "Matriz de Correlación")

test.cor <- creditos %>%
select_if(is.numeric)
findCorrelation(cor(test.cor), cutoff = 0.5, names = TRUE )
## character(0)
La gráfica de matriz muestra que no hay correlaciones altas entre las variables, por lo que, es buena noticia, ya que no tenemos el problema de la multicolinealidad. Lo anterior, también, se comprueba con la función findCorrelation del paquete caret en la cual se pone como umbral un coeficiente de correlación de 0.5 y los resultados muestran que no hay correlaciones que superan ese nivel.
creditos %>%
select_if(is.numeric) %>%
describeBy() %>%
kable(
format = "html",
digits = 2,
size = 5,
caption = "Resumen Estadístico: variables numéricas",
format.args = list(big.mark = ","),
table.attr = "style='width:50%;'") %>%
kable_styling(bootstrap_options = c("striped", "hover"))
Resumen Estadístico: variables numéricas
|
vars
|
n
|
mean
|
sd
|
median
|
trimmed
|
mad
|
min
|
max
|
range
|
skew
|
kurtosis
|
se
|
CreditScore
|
1
|
10,000
|
650.53
|
96.65
|
652.00
|
651.01
|
99.33
|
350.00
|
850.0
|
500.0
|
-0.07
|
-0.43
|
0.97
|
Age
|
2
|
10,000
|
38.92
|
10.49
|
37.00
|
37.91
|
8.90
|
18.00
|
92.0
|
74.0
|
1.01
|
1.39
|
0.10
|
Balance
|
3
|
10,000
|
76,485.89
|
62,397.41
|
97,198.54
|
74,827.80
|
69,336.44
|
0.00
|
250,898.1
|
250,898.1
|
-0.14
|
-1.49
|
623.97
|
EstimatedSalary
|
4
|
10,000
|
100,090.24
|
57,510.49
|
100,193.91
|
100,114.86
|
72,941.18
|
11.58
|
199,992.5
|
199,980.9
|
0.00
|
-1.18
|
575.10
|
A continuación, se crearán algunas gráficas combinando las variables para tener un panorama más completo e integral de los datos, además, esto servirá para ir identificando las posibles variables que pueden ser buenas predictoras.
creditos %>%
ggplot(aes(x = Geography, group = Gender)) +
geom_bar(aes(y = ..prop.., fill = factor(..x..)),
stat="count", color = "black", alpha = 0.7) +
scale_fill_manual(values = c("steelblue", "grey50", "cornflowerblue")) +
geom_text(aes(label = scales::percent(..prop..),
y= ..prop.. ), stat= "count", position = position_stack(vjust = 0.5), size = 4.5) +
facet_grid(~Gender) +
scale_y_continuous(labels = scales::percent)+
guides(fill = 'none')+
labs(title = "Género por País",
x = "", y = "Porcentaje")+
theme_minimal()+
theme(text = element_text(size = 12),
plot.title = element_text(hjust = 0.5), title = element_text(size = 12))

Como se puede ver en el gráfico, la distribución geográfica de clientes para hombres y mujeres es muy similar. Aproximadamente, la misma cantidad de clientes provienen de los 3 países: la mitad proviene de Francia, una cuarta parte proviene de Alemania y otra cuarta parte, de España. Esto es similar tanto para hombres como para mujeres.
creditos %>%
group_by(Gender, Geography) %>%
summarise(media_sal = round(mean(EstimatedSalary), 2)) %>%
ggplot(aes(x = Gender, y = media_sal, fill = Gender)) +
scale_fill_manual(values = c("steelblue", "grey50")) +
geom_bar(stat = "identity", color = "black", alpha = 0.7) +
geom_text(aes(label = scales::comma(media_sal),
vjust = -.5), size = 4) +
facet_grid(~ Geography) +
labs(title = "Composición de Salarios por País",
y = "Salario Medio") +
theme_minimal() +
coord_cartesian(ylim = c(95000, 103000)) +
scale_y_continuous(labels = scales::comma) +
guides(fill = 'none') +
theme(text = element_text(size = 12),
plot.title = element_text(hjust = 0.5), title = element_text(size = 12))

Otro aspecto interesante para explorar es la distribución geográfica de los salarios entre hombres y mujeres. En la gráfica anterior, se observa que el salario promedio en Alemania es mayor que en los otros dos países, siendo el salario promedio de las mujeres el mayor. También hay un mayor salario promedio para las mujeres en España, mientras que en Francia la situación es al revés.
Veamos cuántos clientes han abandonado por zona geográfica y género, para conocer más a profundidad la variable objetivo.
g1 <- ggplot(creditos, aes(x = Exited,
y = ..count../sum(..count..), fill = Exited)) +
geom_bar(color = "black", alpha = 0.7) +
scale_fill_manual(values = c("steelblue", "grey50")) +
geom_text(aes(label = paste0(format((..count../sum(..count..))*100,
digits = 4),"%")),
size = 4, stat= "count", position = position_stack(vjust = 0.5)) +
scale_y_continuous(labels = percent) +
labs(title = "Proporción de Abandono", y = "Porcentaje") +
scale_x_discrete(name = "¿El cliente abandonó?",
breaks = c("0", "1"),
labels = c("No", "Si")) +
theme_minimal() +
guides(fill = 'none') +
theme(text = element_text(size = 12),
plot.title = element_text(hjust = 0.5), title = element_text(size = 8))
g2 <- ggplot(creditos, aes(x = Exited, group = Gender)) +
geom_bar(aes(y = ..prop.., fill = factor(..x..)),
stat="count", color = "black", alpha = 0.7) +
scale_fill_manual(values = c("steelblue", "grey50")) +
geom_text(aes(label = scales::percent(..prop..),
y= ..prop.. ), stat= "count", position = position_stack(vjust = 0.5), size = 3) +
facet_grid(Gender ~ Geography) +
scale_y_continuous(labels = scales::percent) +
scale_x_discrete(name = "¿El cliente abandonó?",
breaks=c("0", "1"),
labels=c("No", "Si")) +
guides(fill = 'none') +
labs(title = "Proporciones de abandono por Género y País",
y = "Porcentaje") +
theme_minimal() +
theme(text = element_text(size = 12),
plot.title = element_text(hjust = 0.5), title = element_text(size = 8))
grid.arrange(g1, g2, ncol = 2)

En las gráficas anteriores podemos ver la información más importante sobre las tasas de abandono entre los clientes. Se puede decir que uno de cada cinco clientes abandonó la institución. Además, se puede observar que las tasas de abandono en Alemania son las más altas entre los demás países, lo anterior, también se ve reflejado tanto para hombres como para mujeres. La menor cantidad de abandono se localiza en Francia, mientras que España ocupa una posición intermedia.
Además, algo importante a considerar es que los datos estan desbalanceados, por lo que, debemos tenerlo presente a la hora de construir los modelos.
Tratemos de identificar, en promedio, el tiempo en el que los clientes abandonan el banco.
creditos.t <- creditos
creditos.t$Tenure <- as.double(creditos.t$Tenure)
duracion <- creditos.t %>%
group_by(Exited) %>%
summarise(tiempo.promedio = round(mean(Tenure), 2))
g3 <- ggplot(duracion, aes(x = Exited, y = tiempo.promedio, fill = Exited)) +
geom_bar(stat = "identity", color = "black", alpha = 0.7) +
geom_text(aes(label = tiempo.promedio, vjust = -.5), size = 4) +
scale_x_discrete(name = "El cliente abandonó?",
breaks = c("0", "1"),
labels = c("No", "Si")) +
theme_minimal() +
scale_fill_manual(values = c("steelblue", "grey50")) +
coord_cartesian(ylim = c(4.7, 6.1)) +
scale_y_continuous(breaks = seq(4.8, 6.1, 0.1)) +
guides(fill = 'none') +
theme(text = element_text(size = 12),
plot.title = element_text(hjust = 0.5), title = element_text(size = 8)) +
labs(title = "Estatus Permanencia del Cliente",
y = "Permanencia Promedio")
duracion2 <- creditos.t %>%
group_by(Exited, Gender, Geography) %>%
summarise(tiempo.promedio = round(mean(Tenure), 2))
g4 <- ggplot(duracion2,
aes(x = Exited, y = tiempo.promedio, group = Gender, fill = Exited)) +
geom_bar(stat = "identity", color = "black", alpha = 0.7) +
geom_text(aes(label = tiempo.promedio, vjust = -.5), size = 4) +
scale_x_discrete(name = "El cliente abandonó?",
breaks = c("0", "1"),
labels = c("No", "Si"))+
facet_grid(Gender ~ Geography) +
theme_minimal() +
scale_fill_manual(values = c("steelblue", "grey50")) +
coord_cartesian(ylim=c(4.5, 6.2)) +
scale_y_continuous(breaks = seq(4.5, 6.2, 0.1)) +
guides(fill = 'none')+
theme(text = element_text(size = 12),
plot.title = element_text(hjust = 0.5), title = element_text(size = 8)) +
labs(title = "Estatus Permanencia del Cliente por Género y País",
y = "Permanencia Promedio")
grid.arrange(g3, g4, ncol = 2)

En las gráficas de permanencia podemos destacar lo siguiente:
- El tiempo promedio de permanencia entre los clientes que abandonan el banco y los que permanecen son muy parecidos.
- España tiene los promedios más bajos de abandono tanto en hombres como para mujeres.
- Alemania presenta el promedio más alto de abandono en el grupo de las mujeres y Francia en el grupo de los hombres.
g5 <- creditos %>%
ggplot(aes(x = Exited, y = Age)) +
geom_boxplot(outlier.color = "red", fill = '#A4A4A4', color = "darkred") +
labs(
title = "Edad"
)
g6 <- creditos %>%
ggplot(aes(x = Exited, y = EstimatedSalary)) +
geom_boxplot(outlier.color = "red", fill = '#A4A4A4', color = "darkred") +
scale_y_continuous(labels = scales::comma) +
labs(
title = "Salario"
)
g7 <- creditos %>%
ggplot(aes(x = Exited, y = Balance)) +
geom_boxplot(outlier.color = "red", fill = '#A4A4A4', color = "darkred") +
scale_y_continuous(labels = scales::comma) +
labs(
title = "Balance"
)
g8 <- creditos %>%
ggplot(aes(x = Exited, y = CreditScore)) +
geom_boxplot(outlier.color = "red", fill = '#A4A4A4', color = "darkred") +
labs(
title = "CreditScore"
)
grid.arrange(g5, g6, g7, g8, ncol = 2)

En estas ultimas gráficas podemos apreciar que de las variables numéricas solo la edad pueder ser buen predictor, ya que las cajas de los boxplots no se solapan a diferencia de las demás variables.
Pre procesamiento de los datos
El preprocesamiento de datos es el proceso de convertir datos sin procesar o “crudos” en un formato legible que es adecuado para construir y entrenar modelos de aprendizaje automático.
Selección de variables
Casi al inicio del proyecto se empezó a realizar la selección de variables cuando se eliminaron las columnas ‘RowNumber’, ‘CustomerId’ y ‘Surname’. En la sección del análisis exploratorio identificamos varias características más que pueden descartarse ya que no proporcionan ningún valor para predecir nuestra variable objetivo: Exited.
- La variable EstimatedSalary muestra una distribución casi uniforme para ambos tipos de clientes, por lo que, se puede descartar.
- Las categorías de ‘Tenure’ y ‘HasCrCard’ tienen una tasa de abandono similar (alrededor de 20%) y se consideran redundantes. Esto se puede confirmar con una prueba de chi-cuadrado:
creditos %>%
tabyl(Tenure, Exited, show_missing_levels = FALSE) %>%
adorn_totals("row") %>%
adorn_percentages("all") %>%
adorn_pct_formatting(digits = 1) %>%
adorn_ns %>%
adorn_title
creditos %>%
tabyl(HasCrCard, Exited, show_missing_levels = FALSE) %>%
adorn_totals("row") %>%
adorn_percentages("all") %>%
adorn_pct_formatting(digits = 1) %>%
adorn_ns %>%
adorn_title
t <- creditos %>%
select_if(is.factor)
categorias <- t %>%
dplyr::select(-Exited) %>%
names()
chi <- vector()
p.value <- vector()
for (columna in categorias){
test <- stats::chisq.test(t[columna], t$Exited)
chi <- append(chi, test[["statistic"]][["X-squared"]])
p.value <- append(p.value, test[["p.value"]])
}
df.chi <- data.frame(categorias, chi, p.value) %>%
arrange(desc(-p.value))
df.chi %>%
kable(caption = "Prueba Chi-cuadrada", align = "c") %>%
kable_styling(bootstrap_options = c("striped", "hover"))
Prueba Chi-cuadrada
categorias
|
chi
|
p.value
|
NumOfProducts
|
1503.6293615
|
0.0000000
|
IsActiveMember
|
242.9853416
|
0.0000000
|
Tenure
|
13.9003726
|
0.1775846
|
HasCrCard
|
0.4713378
|
0.4923724
|
Las variables Tenure y HasCrCard tienen un chi-cuadrado pequeño y un p-valor mayor que 0.05 (nivel de significancia estándar), lo que confirma la conclusión anterior de que estas dos características no transmiten ninguna información útil. Por lo tanto, se recomienda eliminarlas del análisis, sin embargo, para poder tomar esa decisión con mayor certeza, se identificará la importancia que tiene cada variable.
Importancia de las variables
creditos.rf <- map_if(.x = creditos, .p = is.character, .f = as.factor) %>%
as.data.frame()
modelo.importancia <- randomForest(formula = Exited ~ . ,
data = na.omit(creditos),
mtry = 5,
importance = TRUE,
ntree = 1000)
importancia <- as.data.frame(modelo.importancia$importance)
importancia <- rownames_to_column(importancia, var = "variable")
g9 <- ggplot(data = importancia,
aes(x = reorder(variable, MeanDecreaseAccuracy),
y = MeanDecreaseAccuracy, fill = MeanDecreaseAccuracy)) +
labs(x = "variable",
title = "Reducción de Accuracy") +
geom_col() +
coord_flip() +
theme_bw() +
theme(legend.position = "bottom")
g10 <- ggplot(data = importancia,
aes(x = reorder(variable, MeanDecreaseGini),
y = MeanDecreaseGini, fill = MeanDecreaseGini)) +
labs(x = "variable", title = "Reducción de pureza (Gini)") +
geom_col() +
coord_flip() +
theme_bw() +
theme(legend.position = "bottom")
ggarrange(g9, g10)

En ambas gráficas, la variable HasCrcard tiene muy poca influencia sobre la variable objetivo. La variable Tenure parece que puede aportar algo al modelo, sin embargo, como se vio, anteriormente, dicha variable presenta una distribución uniforme, por lo que, no será un buen predictor. Lo mismo pasa con la variable EstimatedSalary.
creditos %>%
ggplot(aes(x = Tenure, y = ..count.., fill = Exited)) +
geom_bar() +
scale_fill_manual(values = c("steelblue", "gray50")) +
labs(title = "Tenure")

creditos %>%
ggplot(aes(x = EstimatedSalary, y = ..count.., fill = Exited)) +
geom_histogram() +
scale_fill_manual(values = c("steelblue", "gray50")) +
labs(title = "EstimatedSalary")

En las gráficas, ambas variables no muestran ser diferenciadoras o influyentes para determinar si un cliente se queda o abandona el banco, además, esto ya se había sospechado al ver los boxplots. Por tanto se eliminan.
creditos <- creditos %>%
dplyr::select(-c(Tenure, HasCrCard, EstimatedSalary))
Codificación de variables categóricas
La mayoría de los algoritmos de machine learning, generalmente, requieren que todas las características de entrada (y salida) sean numéricas. Por lo que, las variables categóricas deben convertirse (codificarse) en números antes de construir los modelos.
Hasta el aquí, el conjunto de datos contiene dos características que requieren codificación: Gender y Geography.
- Vamos a recodificar: masculino = 1 y Femenino = 0
- Para la variable Geography, se crearán solo dos grupos, Alemania = 1 y el resto en la categoría otros = 0, ya que en Francia y España la tasa de abandono es casi la misma y en Alemania es bastante baja, por lo que, creando estos dos grupos se podrá diferenciar mejor esta variable y le vendrá bien a los algoritmos.
creditos$Gender <- ifelse(creditos$Gender == 'Female', 0, 1)
creditos$Gender <- as.factor(creditos$Gender)
creditos$Geography <- ifelse(creditos$Geography == 'France' | creditos$Geography =='Spain',
0, 1)
creditos$Geography <- as.factor(creditos$Geography)
creditos <- creditos %>%
mutate(NumOfProducts = fct_collapse(NumOfProducts,
'1' = '1',
'2' = '2',
other_level = '>2'))
Escalamiento
El escalado de características es una técnica que se utiliza para normalizar el rango de variables en un conjunto de datos cuando éstas tienen diferentes magnitudes, por ejemplo, no es la misma diferencia entre el rango de la edad y el rango del Balance. Para que las variables con magnitudes grandes no dominen el resto de los datos se deben escalar todas las variables. Algunos algoritmos son sensibles a la escala de características (por ejemplo, SVM), mientras que otros son invariantes (por ejemplo, bosques aleatorios).
s <- c("Age", "Balance", "CreditScore")
creditos[s] <- scale(creditos[s])
Balanceo de clases
Como hemos visto anteriormente, existe un desbalanceo en las clases de la variable objetivo, con una clase (0 = no abandono) mucho más prevalente que la otra (1 = abandono):
table(creditos$Exited)
##
## 0 1
## 7963 2037
Antes de seguir adelante, por conveniencia, vamos a recodificar la variable Exited para que las clases queden de la siguiente manera:
- clase positiva: churn (abandona)
- clase negativa: stay (permanece)
creditos$Exited <- ifelse(creditos$Exited == '1', 'churn', 'stay')
creditos$Exited <- as.factor(creditos$Exited)
table(creditos$Exited)
##
## churn stay
## 2037 7963
El desbalanceo de clases suele ser un problema frecuente sobre todo en modelos de abandono de clientes o en temas de riesgo o fraudes. La clasificación que utiliza datos desequilibrados está sesgada a favor de la clase mayoritaria, lo que significa que los algoritmos de machine learning probablemente darán como resultado modelos que hacen poco más que predecir la clase más común. Además, las métricas comunes pueden ser engañosas cuando se manejan datos con desequilibrio de clases (por ejemplo, si un conjunto de datos contiene 99,9% de la clase “0” y 0,01% de la clase “1”, un clasificador que siempre predice “0” tendrá una precisión del 99,9%). Además, dan lugar a predicciones sesgadas.
Existen algunas estrategias que pueden abordar este problema como, por ejemplo, el submuestreo, sobremuestreo, generación de datos sintéticos, etc. Sin embargo, antes de continuar dividiremos el conjunto de datos en una parte de entrenamiento, validación y prueba, para evitar caer en un sobreajuste en el desempeño de los modelos. Las divisiones serán: 70%-15%-15%, respectivamente.
Entrenamiento:
Es el conjunto de datos que se utiliza para entrenar y hacer que el modelo aprenda las características / patrones ocultos en los datos.
Validación:
El conjunto de validación es un conjunto de datos separado del conjunto de entrenamiento, que se utiliza para validar el rendimiento de nuestro modelo durante el entrenamiento.
Prueba:
El conjunto de prueba es un conjunto separado de datos utilizados para probar el modelo después de completar el entrenamiento. Son observaciones que el algoritmo no ha visto.
set.seed(123)
div1 <- createDataPartition(y = creditos$Exited,
p = 0.7,
list = FALSE,
times = 1)
creditos.train <- creditos[div1,] # entrenamiento
creditos2 <- creditos[-div1,]
div2 <- createDataPartition(y = creditos2$Exited,
p = 0.5,
list = FALSE,
times = 1)
creditos.validacion <- creditos2[div2,] # validacion
creditos.test <- creditos2[-div2,] # prueba
Solo verificamos la distribución original de las clases en los tres conjuntos de datos:
# train
prop.table(table(creditos.train$Exited))
##
## churn stay
## 0.2036852 0.7963148
nrow(creditos.train)
## [1] 7001
# validacion
prop.table(table(creditos.validacion$Exited))
##
## churn stay
## 0.204 0.796
nrow(creditos.validacion)
## [1] 1500
# test
prop.table(table(creditos.test$Exited))
##
## churn stay
## 0.203469 0.796531
nrow(creditos.test)
## [1] 1499
Se utilizará la técnica de sobremuestreo SMOTE (Synthetic Minority Oversampling Technique), que consiste en encontrar un registro que es similar al registro que se está muestreando y crea un registro sintético que es un promedio ponderado aleatoriamente del registro original y el registro vecino, donde el peso se genera por separado para cada predictor. Para generar datos artificiales, utiliza bootstrapping y k-vecinos más cercanos.
set.seed(123)
creditos.bal <- ROSE(Exited ~ .,
data = creditos.train,
seed = 1)$data
table(creditos.bal$Exited)
##
## stay churn
## 3516 3485
prop.table(table(creditos.bal$Exited))
##
## stay churn
## 0.502214 0.497786
Métrica de evaluación
Es indispensable definir, en los modelos de aprendizaje supervisado, una métrica que sirva para evaluar la capacidad predictiva del modelo final, ademas, servirá para ir comparando los modelos generados así como su calibración.
Como este proyecto aborda un problema de clasificación, ya que la variable a predecir es categórica, y lo que se pretende es que el modelo clasifique correctamente las observaciones de la clase positiva, es decir, los clientes que abandonan, por ser más crítico para el banco, la métrica de evaluación elegida será la Sensibilidad o Recall, porque esta métrica representa la fracción de verdaderos positivos, para tenerlos bien identificados y dirigir campañas de fidelización con el objetivo de que no se vayan. En otras palabras, queremos evitar clasificar a un cliente que abandona el banco en la clase de los que permaneceran.
La fórmula es:
Sensitivity = VP/(VP + FN)
VP: Verdaderos positivos
FN: Falsos negativos
Creación de modelos de aprendizaje automático
Siempre que se va a hacer un modelo de machine learning y en específico un modelo de aprendizaje supervisado es altamente recomendable iniciar creando un modelo base a partir del cual se tratará de mejorar los resultados ya sea calibrando sus hiperparámetros (si los tiene) o creando otros algoritmos.
Para tener una medida de referencia y sobre la cual mejorar esos resultados, se creará un modelo que será: Naive Bayes.
Modelo base: Naive Bayes
Como se va a utilizar el paquete caret, por conveniencia vamos a crear el objeto recipe, para poder ejecutar los algoritmos:
# Se crea un objeto recipe
objeto.recipe <- recipe(formula = Exited ~ CreditScore + Geography + Gender + Age +
Balance + NumOfProducts + IsActiveMember,
data = creditos.bal)
objeto.recipe
## Recipe
##
## Inputs:
##
## role #variables
## outcome 1
## predictor 7
# Se entrena el objeto recipe
trained.recipe <- prep(objeto.recipe, training = creditos.bal)
trained.recipe
## Recipe
##
## Inputs:
##
## role #variables
## outcome 1
## predictor 7
##
## Training data contained 7001 data points and no missing data.
# Se preparan los objetos
creditos.train.prep <- bake(trained.recipe, new_data = creditos.bal)
creditos.validacion.prep <- bake(trained.recipe, new_data = creditos.validacion)
creditos.test.prep <- bake(trained.recipe, new_data = creditos.test)
glimpse(creditos.train.prep)
## Rows: 7,001
## Columns: 8
## $ CreditScore <dbl> 0.476236104, -0.327942574, -0.110973502, 0.409201095, 0~
## $ Geography <fct> 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0~
## $ Gender <fct> 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1~
## $ Age <dbl> 2.349714505, 0.005396935, 0.113704231, -1.119619671, -1~
## $ Balance <dbl> 0.65916998, -1.05626772, -1.26977342, 0.05753502, 0.436~
## $ NumOfProducts <fct> 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 2, 2, 1, 2, 1, 1, 1~
## $ IsActiveMember <fct> 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1~
## $ Exited <fct> stay, stay, stay, stay, stay, stay, stay, stay, stay, s~
glimpse(creditos.validacion.prep)
## Rows: 1,500
## Columns: 8
## $ CreditScore <dbl> -0.35724389, -0.61590034, -1.00905816, -1.84710509, -0.~
## $ Geography <fct> 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0~
## $ Gender <fct> 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1~
## $ Age <dbl> 0.579549215, 0.007456278, 0.198153924, 0.102805101, 2.1~
## $ Balance <dbl> 1.0680496, -1.2257864, 0.5389110, -1.2257864, 0.6560122~
## $ NumOfProducts <fct> 2, >2, 2, 1, 1, 2, 2, 1, 2, 1, 2, 2, 2, 2, 1, 1, 1, 2, ~
## $ IsActiveMember <fct> 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0~
## $ Exited <fct> stay, churn, stay, stay, stay, stay, stay, stay, stay, ~
glimpse(creditos.test.prep)
## Rows: 1,499
## Columns: 8
## $ CreditScore <dbl> -0.05720239, 0.34630168, -1.05044320, -0.65728538, 2.02~
## $ Geography <fct> 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1~
## $ Gender <fct> 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0~
## $ Age <dbl> 0.48420039, -1.13672960, -1.32742724, 0.57954922, -0.08~
## $ Balance <dbl> 0.597298727, 0.931416788, -1.225786377, -1.225786377, -~
## $ NumOfProducts <fct> 2, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2, 1, 1, 1~
## $ IsActiveMember <fct> 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0~
## $ Exited <fct> churn, stay, stay, stay, stay, stay, stay, stay, stay, ~
Naive Bayes
# paralelizacion del proceso
cores <- detectCores()-1
cl <- makeCluster(cores, type = "SOCK")
registerDoSNOW(cl)
# hiperparametros, no. repeticiones y semilla para cada repeticion
particiones <- 10
repeticiones <- 5
hiperparametros <- data.frame(usekernel = FALSE, fL = 0 , adjust = 0)
set.seed(123)
seeds <- vector(mode = "list", length = (particiones * repeticiones) + 1)
for (i in 1:(particiones * repeticiones)) {
seeds[[i]] <- sample.int(1000, nrow(hiperparametros))
}
seeds[[(particiones * repeticiones) + 1]] <- sample.int(1000, 1)
# definicion del entrenamiento
control.train <- trainControl(method = "repeatedcv",
number = particiones,
repeats = repeticiones,
seeds = seeds,
returnResamp = "final",
verboseIter = FALSE,
allowParallel = TRUE)
# ajuste del modelo
set.seed(342)
modelo_nb <- train(Exited ~ .,
data = creditos.train.prep[-6],
method = "nb",
tuneGrid = hiperparametros,
metric = "Accuracy",
trControl = control.train)
# se guarda el modelo
saveRDS(modelo_nb, file = "modelo.nb.rds")
modelo_nb <- readRDS(file = "modelo.nb.rds")
El Accuracy en el conjunto de entrenamiento del modelo Naive Bayes final fue de: 69.6130112%
cm.nb <- confusionMatrix(predict(modelo_nb), creditos.train.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.nb)

La Sensitivity en el set de train fue de: 68.0631277%
Ahora estamos listos para comenzar a construir modelos de aprendizaje automático. Los algoritmos clasificadores que se han seleccionado son los siguientes:
- Logistic Regression (LOGIT)
- Support Vector Machine (SVM)
- Random Forest (RF)
- Extreme Gradient Boosting (XGBoost)
- Neuronal Network (NNET)
- Gradient Boosting Machine (GBM)
Regresión Logística
# paralelizacion del proceso
cores <- detectCores()-1
cl <- makeCluster(cores, type = "SOCK")
registerDoSNOW(cl)
# hiperparametros, no. repeticiones y semilla para cada repeticion
particiones <- 10
repeticiones <- 5
hiperparametros <- data.frame(parameter = "none")
set.seed(123)
seeds <- vector(mode = "list", length = (particiones * repeticiones) + 1)
for (i in 1:(particiones * repeticiones)) {
seeds[[i]] <- sample.int(1000, nrow(hiperparametros))
}
seeds[[(particiones * repeticiones) + 1]] <- sample.int(1000, 1)
# definicion del entrenamiento
control.train <- trainControl(method = "repeatedcv",
number = particiones,
repeats = repeticiones,
seeds = seeds,
returnResamp = "final",
verboseIter = FALSE,
allowParallel = TRUE)
# ajuste del modelo
set.seed(342)
modelo_logistic <- train(Exited ~ .,
data = creditos.train.prep,
method = "glm",
tuneGrid = hiperparametros,
metric = "Accuracy",
trControl = control.train,
family = "binomial")
# se guarda el modelo
saveRDS(modelo_logistic, file = "modelo.logit.rds")
modelo_logistic <- readRDS(file = "modelo.logit.rds")
El Accuracy en el conjunto de entrenamiento del modelo hecho con la Regresión Logística fue de: 75.7865582%
cm.lg <- confusionMatrix(predict(modelo_logistic), creditos.train.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.lg)

La Sensitivity en el set de train fue de: 74.9497848%
Máquinas de Vector Soporte
# paralelizacion del proceso
cores <- detectCores()-1
cl <- makeCluster(cores, type = "SOCK")
registerDoSNOW(cl)
# hiperparametros, no. repeticiones y semilla para cada repeticion
particiones <- 10
repeticiones <- 5
hiperparametros <- expand.grid(sigma = c(0.001, 0.01, 0.1, 0.5, 1),
C = c(1 , 20, 50, 100, 200, 500, 700))
set.seed(123)
seeds <- vector(mode = "list", length = (particiones * repeticiones) + 1)
for (i in 1:(particiones * repeticiones)) {
seeds[[i]] <- sample.int(1000, nrow(hiperparametros))
}
seeds[[(particiones * repeticiones) + 1]] <- sample.int(1000, 1)
# definicion del entrenamiento
control.train <- trainControl(method = "repeatedcv",
number = particiones,
repeats = repeticiones,
seeds = seeds,
returnResamp = "final",
verboseIter = FALSE,
allowParallel = TRUE)
# ajuste del modelo
set.seed(342)
modelo_svm <- train(Exited ~ .,
data = creditos.train.prep,
method = "svmRadial",
tuneGrid = hiperparametros,
metric = "Accuracy",
trControl = control.train)
# se guarda el modelo
saveRDS(modelo_svm, file = "modelo.svm.rds")
modelo_svm <- readRDS(file = "modelo.svm.rds")
El accuracy promedio del modelo Support Vector Machine en el conjunto de entrenamiento fue de: 77.3878016%
cm.svm <- confusionMatrix(predict(modelo_svm), creditos.train.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.svm)

La Sensitivity en el set de train fue de: 82.553802%
# representacion gráfica
ggplot(modelo_svm, highlight = TRUE) +
labs(title = "Evolución del accuracy del modelo SVM Radial") +
theme_bw()

Random Forest
# paralelizacion del proceso
cores <- detectCores()-1
cl <- makeCluster(cores, type = "SOCK")
registerDoSNOW(cl)
# hiperparametros, no. repeticiones y semilla para cada repeticion
particiones <- 10
repeticiones <- 5
hiperparametros <- expand.grid(mtry = c(3, 4, 5, 7),
min.node.size = c(2, 3, 4, 5, 10, 15, 20, 30),
splitrule = "gini")
set.seed(123)
seeds <- vector(mode = "list", length = (particiones * repeticiones) + 1)
for (i in 1:(particiones * repeticiones)) {
seeds[[i]] <- sample.int(1000, nrow(hiperparametros))
}
seeds[[(particiones * repeticiones) + 1]] <- sample.int(1000, 1)
# definicion del entrenamiento
control.train <- trainControl(method = "repeatedcv",
number = particiones,
repeats = repeticiones,
seeds = seeds,
returnResamp = "final",
verboseIter = FALSE,
allowParallel = TRUE)
# ajuste del modelo
set.seed(342)
modelo_rf <- train(Exited ~ .,
data = creditos.train.prep,
method = "ranger",
tuneGrid = hiperparametros,
metric = "Accuracy",
trControl = control.train,
num.trees = 500)
# se guarda el modelo
saveRDS(modelo_rf, file = "modelo.rf.rds")
modelo_rf <- readRDS(file = "modelo.rf.rds")
El Accuracy promedio del modelo Random Forest en el conjunto de entrenamiento fue de: 78.5224959%
cm.rf <- confusionMatrix(predict(modelo_rf), creditos.train.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.rf)

La Sensitivity en el set de train fue de: 85.9110473%
# representación gráfica
ggplot(modelo_rf, highlight = TRUE) +
scale_x_continuous(breaks = 1:30) +
labs(title = "Evolución del accuracy del modelo Random Forest") +
guides(color = guide_legend(title = "mtry"),
shape = guide_legend(title = "mtry")) +
theme_bw()

XGBoost
cores <- detectCores()-1
cl <- makeCluster(cores, type = "SOCK")
registerDoSNOW(cl)
# hiperparametros, no. repeticiones y semilla para cada repeticion
particiones <- 10
repeticiones <- 5
hiperparametros <- expand.grid(eta = c(0.05, 0.075, 0.1),
nrounds = c(50, 75, 100),
max_depth = 6:8,
min_child_weight = c(2.0, 2.25, 2.5),
colsample_bytree = c(0.3, 0.4, 0.5),
gamma = 0,
subsample = 1)
set.seed(123)
seeds <- vector(mode = "list", length = (particiones * repeticiones) + 1)
for (i in 1:(particiones * repeticiones)) {
seeds[[i]] <- sample.int(1000, nrow(hiperparametros))
}
seeds[[(particiones * repeticiones) + 1]] <- sample.int(1000, 1)
# definicion del entrenamiento
control.train <- trainControl(method = "repeatedcv",
number = particiones,
repeats = repeticiones,
seeds = seeds,
returnResamp = "final",
verboseIter = FALSE,
search = "grid",
allowParallel = TRUE)
# ajuste del modelo
set.seed(342)
modelo_boost <- train(Exited ~ .,
data = creditos.train.prep,
method = "xgbTree",
tuneGrid = hiperparametros,
metric = "Accuracy",
trControl = control.train,
distribution = "adaboost",
verbose = FALSE)
# se guarda el modelo
saveRDS(modelo_boost, file = "modelo.xgb.rds")
modelo_xgb <- readRDS(file = "modelo.xgb.rds")
El accuracy promedio del modelo xgboost en el conjunto de entrenamiento fue de: 77.6005462%
cm.xgb <- confusionMatrix(predict(modelo_xgb), creditos.train.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.xgb)

La Sensitivity en el set de train fue de: 80.3443329%
Redes Neuronales (RNN)
cores <- detectCores()-1
cl <- makeCluster(cores, type = "SOCK")
registerDoSNOW(cl)
# hiperparametros, no. repeticiones y semilla para cada repeticion
particiones <- 10
repeticiones <- 5
hiperparametros <- expand.grid(size = c(10, 20, 50, 80, 100, 120),
decay = c(0.0001, 0.1, 0.5))
set.seed(123)
seeds <- vector(mode = "list", length = (particiones * repeticiones) + 1)
for (i in 1:(particiones * repeticiones)) {
seeds[[i]] <- sample.int(1000, nrow(hiperparametros))
}
seeds[[(particiones * repeticiones) + 1]] <- sample.int(1000, 1)
# definicion del entrenamiento
control.train <- trainControl(method = "repeatedcv",
number = particiones,
repeats = repeticiones,
seeds = seeds,
returnResamp = "final",
verboseIter = FALSE,
allowParallel = TRUE)
# ajuste del modelo
set.seed(342)
modelo_nnet <- train(Exited ~ .,
data = creditos.train.prep,
method = "nnet",
tuneGrid = hiperparametros,
metric = "Accuracy",
trControl = control.train,
rang =c(-0.7, 0.7),
MaxNWts = 2000,
trace = FALSE)
# se guarda el modelo
saveRDS(modelo_nnet, file = "modelo.nnet.rds")
modelo_nnet <- readRDS(file = "modelo.nnet.rds")
El accuracy promedio del modelo nnet en el conjunto de entrenamiento fue de: 78.3753218%
cm.nnet <- confusionMatrix(predict(modelo_nnet), creditos.train.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.nnet)

La Sensitivity en el set de train fue de: 77.0731707%
# representación gráfica
ggplot(modelo_nnet, highlight = TRUE) +
labs(title = "Evolución del Accuracy NNET") +
theme_bw()

Gradient Boosting Machine (GBM)
cores <- detectCores()-1
cl <- makeCluster(cores, type = "SOCK")
registerDoSNOW(cl)
# hiperparametros, no. repeticiones y semilla para cada repeticion
particiones <- 10
repeticiones <- 5
hiperparametros <- expand.grid(interaction.depth = c(1, 2),
n.trees = c(500, 1000, 2000),
shrinkage = c(0.001, 0.01, 0.1),
n.minobsinnode = c(2, 5, 15))
set.seed(123)
seeds <- vector(mode = "list", length = (particiones * repeticiones) + 1)
for (i in 1:(particiones * repeticiones)) {
seeds[[i]] <- sample.int(1000, nrow(hiperparametros))
}
seeds[[(particiones * repeticiones) + 1]] <- sample.int(1000, 1)
# definicion del entrenamiento
control.train <- trainControl(method = "repeatedcv",
number = particiones,
repeats = repeticiones,
seeds = seeds,
returnResamp = "final",
verboseIter = FALSE,
allowParallel = TRUE)
# ajuste del modelo
set.seed(342)
modelo_gbm <- train(Exited ~ .,
data = creditos.train.prep,
method = "gbm",
tuneGrid = hiperparametros,
metric = "Accuracy",
trControl = control.train,
distribution = "adaboost",
verbose = FALSE)
# se guarda el modelo
saveRDS(modelo_gbm, file = "modelo.gbm.rds")
modelo_gbm <- readRDS(file = "modelo.gbm.rds")
El accuracy promedio del modelo gbm en el conjunto de entrenamiento fue de: 76.1971264%
cm.gbm <- confusionMatrix(predict(modelo_gbm), creditos.train.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.gbm)

La Sensitivity en el set de train fue de: 75.6958393%
# representación gráfica
ggplot(modelo_gbm, highlight = TRUE) +
labs(title = "Evolución del Accuracy Gradient Boosting") +
guides(color = guide_legend(title = "shrinkage"),
shape = guide_legend(title = "shrinkage")) +
theme_bw() +
theme(legend.position = "bottom")

Comparación de modelos
Finalmente, ya tenemos los modelos entrenados, por lo que, debemos identificar el que consigue mejores resultados para el problema que tenemos, que es el de predecir la fuga de clientes.
Con apoyo de gráficos veremos el Accuracy y la Sensitivity de cada modelo, para facilitar la identificación.
Accuracy
Antes de elaborar la primer gráfica correspondiente al Accuracy promedio, debemos identificar el Accuracy basal. En problemas de clasificación, es la moda de la variable objetivo, que en este caso es Exited. En la base que se está trabajando éste nivel basal está dado por la clase mayoritaria “0”, que representa el 0.7963, el cual es el nivel mínimo que deben superar los modelos. Para que un modelo predictivo sea útil, debe de tener un porcentaje de acierto superior a lo esperado por azar.
prop.table(table(creditos$Exited))
##
## churn stay
## 0.2037 0.7963
models <- list(nb = modelo_nb,
logistic = modelo_logistic,
rf = modelo_rf,
xgb = modelo_xgb,
svm = modelo_svm,
nnet = modelo_nnet,
gbm = modelo_gbm
)
resultados <- resamples(models)
metricas <- resultados$values %>%
gather(key = "modelo", value = "valor", -Resample) %>%
separate(col = "modelo", into = c("modelo", "metrica"), sep = "~", remove = TRUE)
metricas %>%
filter(metrica == "Accuracy") %>%
group_by(modelo) %>%
summarise(media = mean(valor)) %>%
ggplot(aes(x = reorder(modelo, media),
y = media,
label = round(media, 2))) +
geom_segment(aes(x = reorder(modelo, media),
y = 0,
lwd = 2,
xend = modelo,
yend = media),
color = "darkblue",
lwd = 2) +
geom_point(size = 8.5, pch = 21, bg = "firebrick", color = "red") +
geom_text(color = "white", size = 3) +
scale_y_continuous(limits = c(0, 1)) +
geom_hline(yintercept = 0.79, linetype = "dashed") +
annotate(geom = "text", y = 0.84, x = 1.0, label = "Accuracy basal") +
labs(title = "Validación: Accuracy Promedio",
subtitle = "Modelos ordenados por media",
x = "modelo") +
coord_flip() +
theme_bw()

De los 7 modelos generados, solo tres son los que no superan el Accuracy basal: Naive Bayes, Regresión Logística y Gradient Boosting Machine. Hay que recordar que las clases están muy desbalanceadas, por lo que, son buenas noticias que la mayoría los modelos pasen ese nivel minimo. El modelo xgboost está a 0.006 de llegar al Accuracy basal, por lo que, se considera que pasa esta evaluación.
metricas %>%
group_by(modelo, metrica) %>%
summarise(media = mean(valor)) %>%
spread(key = metrica, value = media) %>%
arrange(desc(Accuracy)) %>%
kable(align = "c",
caption = "Promedio: Accuracy - Kappa",
format.args = list(big.mark = ","),
) %>%
kable_paper("hover", full_width = F)
Promedio: Accuracy - Kappa
modelo
|
Accuracy
|
Kappa
|
svm
|
0.7945747
|
0.5891222
|
rf
|
0.7911158
|
0.5821334
|
nnet
|
0.7877463
|
0.5754053
|
xgb
|
0.7840902
|
0.5680694
|
gbm
|
0.7812608
|
0.5623897
|
logistic
|
0.7578656
|
0.5156870
|
nb
|
0.6961301
|
0.3921686
|
metricas %>%
filter(metrica == "Accuracy") %>%
group_by(modelo) %>%
mutate(media = mean(valor)) %>%
ungroup() %>%
ggplot(aes(x = reorder(modelo, media), y = valor, color = modelo)) +
geom_boxplot(alpha = 0.6, outlier.shape = NA) +
geom_jitter(width = 0.1, alpha = 0.6) +
geom_hline(yintercept = 0.79, linetype = "dashed") +
annotate(geom = "text", y = 0.8, x = 1.0, label = "Accuracy basal") +
theme_bw() +
labs(title = "Validación: Accuracy Promedio",
subtitle = "Modelos ordenados por media") +
coord_flip() +
theme(legend.position = "none")

Sensitivity
s.nb <- round(cm.nb[["byClass"]][["Sensitivity"]], digits = 2)
s.lg <- round(cm.lg[["byClass"]][["Sensitivity"]], digits = 2)
s.svm <- round(cm.svm[["byClass"]][["Sensitivity"]], digits = 2)
s.rf <- round(cm.rf[["byClass"]][["Sensitivity"]], digits = 2)
s.xgb <- round(cm.xgb[["byClass"]][["Sensitivity"]], digits = 2)
s.nnet <- round(cm.nnet[["byClass"]][["Sensitivity"]], digits = 2)
s.gbm <- round(cm.gbm[["byClass"]][["Sensitivity"]], digits = 2)
comparacion.train <- tribble(
~Modelo,~Sensibilidad,
'nb', s.nb,
'lg', s.lg,
'svm', s.svm,
'rf', s.rf,
'xgb', s.xgb,
'nnet', s.nnet,
'gbm', s.gbm
)
comparacion.train$Modelo <- factor(comparacion.train$Modelo,
levels = comparacion.train$Modelo
[order(comparacion.train$Sensibilidad)])
comparacion.train %>%
ggplot(aes(x = Modelo, y = Sensibilidad)) +
geom_segment(aes(x = Modelo, xend = Modelo, y = 0, yend = Sensibilidad),
color = "darkblue", lwd = 2) +
geom_point(size = 9.5, pch = 21, bg = "steelblue", col = "red") +
geom_text(aes(label = Sensibilidad), color = "white", size = 3.5) +
labs(title = "Comparación por Sensibilidad",
y = "Modelo",
x = "Sensibilidad") +
coord_flip()

Nuevamente, los modelos que no superaron el Accuracy basal presentan la Sensitivity más bajas.
Evaluación con datos de validación
A pesar de que algunos modelos tuvieron buen desempeño, la evaluación final se debe de hacer con un conjunto de observaciones diferente, porque son observaciones que los algoritmos no han visto, además, esto nos permitirá conocer si los modelos generaron sobreajuste.
# Naive Bayes
preds.nb <- predict(modelo_nb, creditos.validacion.prep)
cm.nb.val <- confusionMatrix(preds.nb, creditos.validacion.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.nb.val)

# Logistic Regression
preds.log <- predict(modelo_logistic, creditos.validacion.prep)
cm.lg.val <- confusionMatrix(preds.log, creditos.validacion.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.lg.val)

# Suport Vector Machine
preds.svm <- predict(modelo_svm, creditos.validacion.prep)
cm.svm.val <- confusionMatrix(preds.svm, creditos.validacion.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.svm.val)

# Random Forest
preds.rf <- predict(modelo_rf, creditos.validacion.prep)
cm.rf.val <- confusionMatrix(preds.rf, creditos.validacion.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.rf.val)

# Extreme Gradient Boosting
preds.xgb <- predict(modelo_xgb, creditos.validacion.prep)
cm.xgb.val <- confusionMatrix(preds.xgb, creditos.validacion.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.xgb.val)

# Neuronal Network
preds.nnet <- predict(modelo_nnet, creditos.validacion.prep)
cm.nnet.val <- confusionMatrix(preds.nnet, creditos.validacion.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.nnet.val)

# Gradient Boosting Machine
preds.gbm <- predict(modelo_gbm, creditos.validacion.prep)
cm.gbm.val <- confusionMatrix(preds.gbm, creditos.validacion.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.gbm.val)

Accuracy.val <- tribble(
~Modelo,~Accuracy,~Kappa,
'nb', cm.nb.val[["overall"]][["Accuracy"]], cm.nb.val[["overall"]][["Kappa"]],
'logistic', cm.lg.val[["overall"]][["Accuracy"]], cm.lg.val[["overall"]][["Kappa"]],
'svm', cm.svm.val[["overall"]][["Accuracy"]], cm.svm.val[["overall"]][["Kappa"]],
'rf', cm.rf.val[["overall"]][["Accuracy"]], cm.rf.val[["overall"]][["Kappa"]],
'xgb', cm.xgb.val[["overall"]][["Accuracy"]], cm.xgb.val[["overall"]][["Kappa"]],
'nnet', cm.nnet.val[["overall"]][["Accuracy"]], cm.nnet.val[["overall"]][["Kappa"]],
'gbm', cm.gbm.val[["overall"]][["Accuracy"]], cm.gbm.val[["overall"]][["Kappa"]]
)
Accuracy.val %>%
arrange(desc(Accuracy)) %>%
kable(align = "c",
caption = "Promedio: Accuracy - Kappa"
) %>%
kable_paper("hover", full_width = F)
Promedio: Accuracy - Kappa
Modelo
|
Accuracy
|
Kappa
|
svm
|
0.8080000
|
0.5100308
|
gbm
|
0.8013333
|
0.4857671
|
rf
|
0.8000000
|
0.4885852
|
nnet
|
0.7980000
|
0.4850302
|
xgb
|
0.7933333
|
0.4747187
|
logistic
|
0.7786667
|
0.4528098
|
nb
|
0.6906667
|
0.2828350
|
Accuracy.val %>%
ggplot(aes(x = reorder(Modelo, Accuracy),
y = Accuracy,
label = round(Accuracy, 2))) +
geom_segment(aes(x = reorder(Modelo, Accuracy),
y = 0,
lwd = 2,
xend = Modelo,
yend = Accuracy),
color = "darkblue",
lwd = 2) +
geom_point(size = 8.5, pch = 21, bg = "firebrick", color = "red") +
geom_text(color = "white", size = 3) +
scale_y_continuous(limits = c(0, 1)) +
geom_hline(yintercept = 0.79, linetype = "dashed") +
annotate(geom = "text", y = 0.84, x = 1.0, label = "Accuracy basal") +
labs(title = "Validación Accuracy",
subtitle = "Modelos ordenados por Accuracy",
x = "modelo") +
coord_flip() +
theme_bw()

En el conjunto de validación el modelo que generó el más alto Accuracy fue el SVM seguido por el RF. El modelo que no mejoró dicha medida fue el Naive Bayes.
result.acc <- metricas %>%
group_by(modelo, metrica) %>%
summarise(media = mean(valor)) %>%
spread(key = metrica, value = media) %>%
rename(Modelo = modelo,
Accuracy.Train = Accuracy) %>%
dplyr::select(-Kappa) %>%
inner_join(Accuracy.val, by = "Modelo") %>%
dplyr::select(-Kappa) %>%
rename(Accuracy.Validacion = Accuracy) %>%
arrange(desc(Accuracy.Validacion))
brks <- seq(0.7, 0.99, 0.01)
clrs <- colorRampPalette(c("white", "#6baed6"))(length(brks) + 1)
datatable(result.acc,
escape = FALSE,
rownames = FALSE,
options = list(autoWidth = FALSE)) %>%
formatStyle(c("Accuracy.Validacion"),
backgroundColor = styleInterval(brks, clrs))
result.acc %>%
pivot_longer(cols = starts_with('Acc'),
names_to ='Tipo',
values_to = 'Accuracy'
) %>%
ggplot(aes(x = reorder(Modelo, Accuracy),
y = Accuracy,
color = Tipo,
label = round(Accuracy, 2))) +
geom_point(size = 8) +
scale_color_manual(values = c("gray50", "firebrick")) +
geom_text(color = "white", size = 3) +
geom_hline(yintercept = 0.79, linetype = "dashed") +
annotate(geom = "text", y = 0.79, x = 1.2, label = "Accuracy basal") +
coord_flip() +
labs(title = "Accuracy de Entrenamiento y Validación", x = "Modelo") +
geom_path(arrow = arrow(length = unit(1.5,'mm'), type = 'closed'), color = 'gray30') +
theme_bw() +
theme(legend.position = "bottom")

Ahora, veamos qué sucedió con la métrica que se eligió para evaluar los modelos: Sensibilidad.
# Sensibilidad
s.nb.val <- round(cm.nb.val[["byClass"]][["Sensitivity"]], digits = 2)
s.lg.val <- round(cm.lg.val[["byClass"]][["Sensitivity"]], digits = 2)
s.svm.val <- round(cm.svm.val[["byClass"]][["Sensitivity"]], digits = 2)
s.rf.val <- round(cm.rf.val[["byClass"]][["Sensitivity"]], digits = 2)
s.xgb.val <- round(cm.xgb.val[["byClass"]][["Sensitivity"]], digits = 2)
s.nnet.val <- round(cm.nnet.val[["byClass"]][["Sensitivity"]], digits = 2)
s.gbm.val <- round(cm.gbm.val[["byClass"]][["Sensitivity"]], digits = 2)
Sensibilidad.val <- tribble(
~Modelo,~Train,~Validacion,
'nb', s.nb, s.nb.val,
'lg', s.lg, s.lg.val,
'svm', s.svm, s.svm.val,
'rf', s.rf, s.rf.val,
'xgb', s.xgb, s.xgb.val,
'nnet', s.nnet, s.nnet.val,
'gbm', s.gbm, s.gbm.val
)
Sensibilidad.val %>%
arrange(desc(Validacion)) %>%
kable(align = "c",
caption = "Sensibilidad"
) %>%
kable_paper("hover", full_width = F)
Sensibilidad
Modelo
|
Train
|
Validacion
|
svm
|
0.83
|
0.81
|
lg
|
0.75
|
0.79
|
rf
|
0.86
|
0.78
|
xgb
|
0.80
|
0.78
|
nnet
|
0.77
|
0.78
|
gbm
|
0.76
|
0.77
|
nb
|
0.68
|
0.68
|
Sensibilidad.val %>%
pivot_longer(cols = ends_with('n'),
names_to ='Tipo',
values_to = 'Sensibilidad'
) %>%
mutate(Modelo = as.factor(Modelo),
Modelo = fct_reorder(Modelo, Sensibilidad)) %>%
ggplot(aes(x = Modelo,
y = Sensibilidad,
color = Tipo,
label = round(Sensibilidad, 2))) +
geom_point(size = 8) +
scale_color_manual(values = c("gray50", "firebrick")) +
geom_text(color = "white", size = 3) +
coord_flip() +
labs(title = "Sensibilidad de Entrenamiento y Validación", x = "Modelo") +
geom_path(arrow = arrow(length = unit(1.5,'mm'), type = 'closed'), color = 'gray30') +
theme_bw() +
theme(legend.position = "bottom")

La Sensitivity del modelo Naive Bayes en el conjunto de validación fue de: 68%
La Sensitivity del modelo de Regresión Logística en el conjunto de validación fue de: 79%
La Sensitivity del modelo de Máquina de Vector Soporte en el conjunto de validación fue de: 81%
La Sensitivity del modelo Random Forest en el conjunto de validación fue de: 78%
La Sensitivity del modelo XGBoost en el conjunto de validación fue de: 78%
La Sensitivity del modelo de Redes Neuronales en el conjunto de validación fue de: 78%
La Sensitivity del modelo Gradient Boosting en el conjunto de validación fue de: 77%
Prácticamente, ningún modelo mejoró la Sensibilidad a excepción de la red neuronal. Por lo tanto, haremos un ensamble con algunos modelos para ver si se obtienen mejores resultados que el mejor modelo: nnet.
Importancia de las variables
Veamos cuáles son las variables más importantes según el modelo que más alto Recall obtuvo:
varImp(modelo_nnet) %>% plot(main = "Modelo NNET")

En la gráfica se observa que el top 3 de las variables que más influyen en dicho modelo son: Age seguida por NumOfProducts que tiene 2 y Geography1.
Ensamble
Ahora, vamos a hacer un ensamble con tres modelos para ver si podemos mejorar los resultados obtenidos por el modelo nnet.
El ensamble consiste en combinar las predicciones de dos o más modelos con el objetivo de superar los resultados obtenidos por el mejor modelo individual. Esta estratégia se recomienda aplicarla cuando hay diferencias entre los distintos modelos generados, ya que la idea es compensar los fallos de unos con los aciertos de otros. Apriori no se esperan mejores resultados, ya que los modelos a ensamblar no presentan diferencias significativas, sin embargo, para fines de mostrar el procedimiento se aplicará esta estratégia.
En este caso, como la variable a predecir es categórica se utilizará la moda para el ensamble.
Los modelos a combinar son:
# data frame
Ensamble <- data.frame(
preds.svm,
preds.xgb,
preds.rf
)
colnames(Ensamble) <- c("SVM", "XGB", "RF")
moda <- function(x){
return(names(which.max(table(x))))
}
Ensamble.preds <- Ensamble %>%
mutate(Moda = apply(X = Ensamble, MARGIN = 1, FUN = moda))
Ensamble.preds$Moda <- as.factor(Ensamble.preds$Moda)
El Accuracy promedio obtenido del ensamble es de: 0.8026667
cm.ensamble <- confusionMatrix(Ensamble.preds$Moda,
creditos.validacion.prep$Exited,
positive = "churn")
draw_confusion_matrix(cm.ensamble)

Como se esperaba, no hubo mejoras significativas en comparación con el mejor modelo, pues solo se redujeron 7 falsos positivos con el esamble. Sin embargo, no está demás comocer esta técnica.
Elección del valor de corte adecuado
Todos los algoritmos cuando generan las predicciones lo hacen considerando un cutoff o punto de corte del 0.5, es decir, probabilidades mayores a ese nivel las etiquetan como 1 y 0 en otro caso. Sin embargo, este umbral se puede optimizar para tratar de minimizar el tipo de error que se desea reducir. En este análisis lo que se buscará es minimizar el error tipo II, los falsos negativos.
Trabajaremos con el modelo nnet, ya que fue uno de los dos modelos que generó la sensibilidad más alta y, además, tuvo poca diferencia entre la sensibilidad de entrenamiento y de validación.
Se vuelve a generar el modelo pero ahora agregando el argumento classProbs, para que calcule las probabilidades de ambas clases.
# paralelizacion del proceso
cores <- detectCores()-1
cl <- makeCluster(cores, type = "SOCK")
registerDoSNOW(cl)
# hiperparametros, no. repeticiones y semilla para cada repeticion
particiones <- 10
repeticiones <- 5
hiperparametros <- expand.grid(size = c(10, 20, 50, 80, 100, 120),
decay = c(0.0001, 0.1, 0.5))
set.seed(123)
seeds <- vector(mode = "list", length = (particiones * repeticiones) + 1)
for (i in 1:(particiones * repeticiones)) {
seeds[[i]] <- sample.int(1000, nrow(hiperparametros))
}
seeds[[(particiones * repeticiones) + 1]] <- sample.int(1000, 1)
# definicion del entrenamiento
control.train <- trainControl(method = "repeatedcv",
number = particiones,
repeats = repeticiones,
seeds = seeds,
returnResamp = "final",
verboseIter = FALSE,
classProbs = TRUE,
allowParallel = TRUE)
# ajuste del modelo
set.seed(342)
modelo_nnet_probs <- train(Exited ~ .,
data = creditos.train.prep,
method = "nnet",
tuneGrid = hiperparametros,
metric = "Accuracy",
trControl = control.train,
rang =c(-0.7, 0.7),
MaxNWts = 2000,
trace = FALSE)
# se guarda el modelo
saveRDS(modelo_nnet_probs, file = "modelo.nnet.probs.rds")
modelo_nnet_probs <- readRDS(file = "modelo.nnet.probs.rds")
# se genera una copia de los datasets
c.train <- creditos.train.prep
c.val <- creditos.validacion.prep
# se obtienen las probabilidades
predict.train <- predict(modelo_nnet_probs, creditos.train.prep, type = "prob")[2]
predict.val <- predict(modelo_nnet_probs, creditos.validacion.prep, type = "prob")[2]
# juntar las probabilidades
c.train$prediction <- predict.train$churn
c.val$prediction <- predict.val$churn
c.train <- as.data.table(c.train)
c.val <- as.data.table(c.val)
El objetivo final del modelo es clasificar a los nuevos clientes en una de dos clases: que permanezca o abandone, vamos a querer que el modelo otorgue probabilidades altas a las observaciones positivas (el cliente abandone) y probabilidades bajas a las observaciones negativas (clientes que no abandonen).
# gráficas de densidades
ggplot(c.train, aes(prediction, color = as.factor(Exited))) +
geom_density(size = 1) +
ggtitle("Probabilidades en el Conjunto de Entrenamiento") +
scale_color_economist(name = "Clase")

En la gráfica anterior, se muestan las densidades de ambas clases, las cuales están claramente separadas de manera correcta: la clase negativa a la izquierda (no abandona) y la clase positiva a la derecha (abandona). La división se aprecia bastante clara, porque hay que recordar que se hizo un balanceo de clases.
Cuando la variable a predecir es categórica, los algoritmos de clasificación en el fondo generan probabilidades, las cuales, como se mencionó anteriormente, se basan en un punto de corte para realizar la asignación de clases.
Usaremos una función para recorrer varios puntos de corte y calcular la precisión del modelo tanto en el conjunto de entrenamiento como en el conjunto de validación.
c.train$Exited <- ifelse(c.train$Exited == 'churn', '1', '0')
c.train$Exited <- as.factor(c.train$Exited)
c.val$Exited <- ifelse(c.val$Exited == 'churn', '1', '0')
c.val$Exited <- as.factor(c.val$Exited)
accuracy_info <- AccuracyCutoffInfo(train = c.train, valid = c.val,
predict = "prediction", actual = "Exited")
accuracy_info$plot

Se aprecia como en el conjunto de entrenamiento el Accuracy es mayor hasta casi la mitad del punto de corte, ya que a partir de ese nivel el Accuracy en el conjunto de validación lo rebasa. Se identificará cuál es el punto de corte donde la diferencia es menor entre ambos conjuntos:
corte <- function(x){
diff(range(x[1], x[2]))
}
Rangos <- accuracy_info$data
Cortes <- Rangos %>%
mutate(Rango = apply(accuracy_info$data[,-1], MARGIN = 1, FUN = corte))
min(Cortes$Rango)
## [1] 0.002316526
which.min(Cortes$Rango)
## [1] 8
Cortes[which.min(Cortes$Rango),]
cut.off <- Cortes[which.min(Cortes$Rango),]$cutoff
En el punto de corte en 0.45 es en donde el conjunto de validación supera el Accuracy del conjunto de entrenamiento. A continuación, se muestran los resultados de elegir ese punto de corte.
cm_info <- ConfusionMatrixInfo( data = c.train, predict = "prediction",
actual = "Exited", cutoff = cut.off)
cm_info$plot

La gráfica anterior, muestra las observaciones con ruido para poder distinguirlas más fácilmente. Se muestra la compensación al elegir ese punto de corte.
Si aumentamos el punto de corte, el número de verdaderos negativos (TN) aumenta y el número de verdaderos positivos (TP) disminuye, es decir, si aumentamos el valor del punto de corte, el número de falsos positivos (FP) se reduce, mientras que el número de falsos negativos (FN) aumenta.
Al estar modificando el punto de corte se está equilibrando entre la tasa de falsos positivos y la tasa de falsos negativos y, para tratar de minimizar el número de errores que el modelo está cometiendo, se suelen asignar costos. La curva ROC se utiliza para visualizar y cuantificar la compensación que estamos haciendo entre las dos medidas.
Esta curva se crea con la tasa de verdaderos positivos (TPR) y la tasa de falsos positivos (FPR), en varios cortes entre 0 y 1.
Como el objetivo es que tengamos pocos falsos negativos, es decir, pocos errores en los que el modelo predice que un cliente permanece cuando en realidad se fue, se asignará un costo mayor (el doble) con respecto a los falsos positivos.
cost_fp <- 50
cost_fn <- 100
roc_info <- ROCInfo(data = cm_info$data, predict = "predict",
actual = "actual", cost.fp = cost_fp, cost.fn = cost_fn)
grid.draw(roc_info$plot)

Al observar las gráficas se puede concluir lo siguiente:
- Al asignar un costo a un falso positivo y a un falso negativo de 50 y 100, respectivamente, el punto de corte óptimo es de 0.33 y el costo total de elegir ese nivel es de 102,500.
- La curva ROC muestra la compensación entre la tasa a la que puede predecir correctamente algo con la tasa de predicción incorrecta al elegir diferentes puntos de corte. El área bajo la curva ROC (AUC) es de 0.867, cuanto más alto es este resultado mejor será el modelo.
- El gráfico de costos calcula el costo asociado al elegir diferentes puntos de corte.
- Para ambas gráficas, la línea punteada indica dónde se encuentra ese punto de corte óptimo, para la gráfica de costos, muestra el valor de corte óptimo. En cuanto a la gráfica de la curva ROC, esta indica la ubicación de la tasa de falsos positivos (FPR) y la tasa de verdaderos positivos (TPR) correspondientes al valor de corte óptimo. El color en las curvas denota el costo asociado con ese punto, “más verde” significa que el costo es menor, mientras que “más negro” significa lo contrario.
Volvemos a generar la matriz de confusión como gráfico de dispersión con el punto de corte óptimo para ver los resultados obtenidos.
cm_info <- ConfusionMatrixInfo( data = c.train, predict = "prediction",
actual = "Exited", cutoff = roc_info$cutoff)
cm_info$plot

El gráfico de la matriz de confusión muestra claramente que al cambiar el valor de corte a 0.33 el modelo de clasificación está cometiendo menos error falso negativo (FN), ya que el costo asociado con él es 2 veces mayor que un falso positivo (FP).
Ahora, veamos la curva ROC
pred <- prediction(c.train$prediction, c.train$Exited)
perf <- performance(pred, "tpr", "fpr")
plot(perf, colorize = TRUE, main = "Curva ROC", xlab = "Tasa de falsos positivos",
ylab = "Tasa de verdaderos positivos")
abline(a = 0, b = 1, col = "blue", lty = 2)
grid()
auc <- as.numeric(performance(pred,"auc")@y.values)
legend("bottomright", legend = paste(" AUC =", round(auc,4)))

Podemos, también, generar los resultados para cada punto de corte:
cutoffs <- data.frame(cut = perf@alpha.values[[1]], fpr = perf@x.values[[1]],
tpr = perf@y.values[[1]])
head(cutoffs)
cutoffs <- cutoffs[order(cutoffs$tpr, decreasing = TRUE),]
head(subset(cutoffs, fpr < 0.33))
Evaluación final: datos de test
Finalmente, vamos a revisar el conjunto de test para conocer los resultados con el punto de corte óptimo.
predict.test <- predict(modelo_nnet_probs, creditos.test.prep, type = "prob")[2] %>%
as.data.frame()
creditos.test.prep$predict <- predict.test$churn
test.final <- creditos.test.prep %>%
mutate(prediccion = if_else(predict >= roc_info$cutoff, "churn", "stay"))
test.final$prediccion <- as.factor(test.final$prediccion)
cm.final <- confusionMatrix(test.final$prediccion, creditos.test.prep$Exited, positive = "churn")
draw_confusion_matrix(cm.final)

Después de generar las probabilidades y de considerar el punto de corte óptimo resultado de aplicarle costos a los errores, se obtuvo una Sensibilidad de 0.8622951, además, solo se obtuvieron 42 falsos negativos, es decir, clientes que el modelo predijo que no se iban a ir del banco y en realidad se fueron. Se logró minimizar este dato, sin embargo, fue acosta de aumentar los falsos positivos, ya que éstos aumentaron, pero es preferible para este caso.
Identificando el tiempo de abandono del cliente
Ya para terminar con el presente proyecto, vamos a identificar el tiempo en el que los clientes abandonan el banco, situación que es de suma importancia para tratar de detectar el momento en el que podemos implementar estrategias para evitar su abandono.
Aplicaremos un análisis de supervivencia, ya que es un conjunto de métodos estadísticos utilizados para conocer el tiempo que tarda en ocurrir un evento que, en este caso, será el abandono de clientes.
Para esto vamos a utilizar la copia de la base original que se generó al inicio del análisis, ya que hay que recordar que la base que se ha estado trabajado ha sufrido modificaciones y eliminaciones de variables no significativas para los algoritmos utilizados.
creditos.raw$status <- ifelse(creditos.raw$Exited == 1,"Churns","Stays")
creditos.raw$status <- as.factor(creditos.raw$status)
Veamos en una tabla las variables de interés: Tenure y Exited, con el propósto de identificar las distribuciones:
tab_xtab(creditos.raw$Tenure, creditos.raw$status, margin = "row", bar.pos = "stack",
show.summary = F)
Tenure
|
status
|
Total
|
Churns
|
Stays
|
0
|
95
|
318
|
413
|
1
|
232
|
803
|
1035
|
2
|
201
|
847
|
1048
|
3
|
213
|
796
|
1009
|
4
|
203
|
786
|
989
|
5
|
209
|
803
|
1012
|
6
|
196
|
771
|
967
|
7
|
177
|
851
|
1028
|
8
|
197
|
828
|
1025
|
9
|
213
|
771
|
984
|
10
|
101
|
389
|
490
|
Total
|
2037
|
7963
|
10000
|
Para facilitar la identificación de las distribuciones entre los clientes que abandonaron el banco y los que aún permanecen, se hará una gráfica de barras apiladas con sus respectivas proporciones en porcentajes y en nivel:
plot_xtab(creditos.raw$Tenure, creditos.raw$status, margin = "row", bar.pos = "stack",
show.summary = F, coord.flip = T,
axis.titles = "", legend.title = "Status",
geom.colors = c("#f3c6f3","lightblue")) +
theme_minimal() +
theme(text = element_text(size = 12),
plot.title = element_text(hjust = 0.5), title = element_text(size = 12)) +
labs(title = "Proporciones de Abandono por Tiempo de Permanencia",
x = "Permanencia (años)",
y = "Porcentaje %")

En la gráfica anterior, se aprecia claramente cómo es en los dos primeros años donde el porcentaje de abandono es ligeramente mayor con respecto al resto del periodo. Además, se puede identificar que no se aprecia una tendencia definida con el paso del tiempo.
Estimación del tiempo de supervivencia o permanencia en el banco
fitsv <- survfit(Surv(creditos.raw$Tenure, creditos.raw$Exited) ~ 1,
type = "kaplan-meier")
fitsv
## Call: survfit(formula = Surv(creditos.raw$Tenure, creditos.raw$Exited) ~
## 1, type = "kaplan-meier")
##
## n events median 0.95LCL 0.95UCL
## 10000 2037 10 10 NA
A partir de la salida anterior, se puede ver que de los 10,000 clientes analizados, fueron 2,037 los que abandonaron el banco durante el periodo. La mediana del tiempo de supervivencia es de 10 años, lo que significa que alrededor del 50% de los clientes no abandonan antes de alcanzar una duración de permanencia de 10 años.
Veamos lo que sucede por género:
fitsv1 <- survfit(Surv(Tenure, Exited) ~ Gender, data = creditos.raw)
fitsv1
## Call: survfit(formula = Surv(Tenure, Exited) ~ Gender, data = creditos.raw)
##
## n events median 0.95LCL 0.95UCL
## Gender=Female 4543 1139 10 10 10
## Gender=Male 5457 898 NA NA NA
El resumen anterior muestra que para el grupo de las mujeres el 25% no abandonó el banco hasta los 10 años, aproximadamente. Para el grupo de los hombres la mediana de supervivencia fue mayor (por eso aparece NA).
ggsurvplot(fitsv1,
pval = TRUE, conf.int = TRUE,
risk.table.col = "strata",
linetype = "strata",
ggtheme = theme_bw(),
palette = c("#E7B800", "#2E9FDF"))

En la gráfica anterior, se aprecia claramente como la probabilidad de supervivencia es mayor para los hombres respecto al grupo de las mujeres. Por ejemplo, en el tiempo 5, la probabilidad para los hombres es de 0.882, mientras que para las mujeres es de 0.8211, aproximadamente. El p-value al ser menor al 0.05 nos indica que hay diferencias significativas entre ambos grupos. Lo anterior, también se confirma con la prueba Log-Rank, que compara curvas de supervivencia y cuya hipótesis nula es que no hay diferencia en la supervivencia entre los grupos:
survdiff(Surv(Tenure, Exited) ~ Gender, data = creditos.raw)
## Call:
## survdiff(formula = Surv(Tenure, Exited) ~ Gender, data = creditos.raw)
##
## N Observed Expected (O-E)^2/E (O-E)^2/V
## Gender=Female 4543 1139 920 52.2 101
## Gender=Male 5457 898 1117 43.0 101
##
## Chisq= 101 on 1 degrees of freedom, p= <2e-16
res <- data.frame(time = fitsv1$time,
n.risk = fitsv1$n.risk,
n.event = fitsv1$n.event,
n.censor = fitsv1$n.censor,
surv = fitsv1$surv,
upper = fitsv1$upper,
lower = fitsv1$lower
)
res %>%
filter(time == 5)
Estimación del tiempo de supervivencia por País
fitsv2 <- survfit(Surv(Tenure, Exited) ~ Geography,
data = creditos.raw)
fitsv2
## Call: survfit(formula = Surv(Tenure, Exited) ~ Geography, data = creditos.raw)
##
## n events median 0.95LCL 0.95UCL
## Geography=France 5014 810 NA NA NA
## Geography=Germany 2509 814 9 9 9
## Geography=Spain 2477 413 NA NA NA
ggsurvplot(fitsv2,
pval = TRUE, conf.int = TRUE,
risk.table.col = "strata",
linetype = "strata",
surv.median.line = "hv",
ggtheme = theme_bw(),
palette = c("#E7B800", "#2E9FDF", "darkred"))

En la supervivencia por grupo geográfico, casi la tercera parte de los clientes de Alemania no abandonan hasta los 9 años de duración. Sin embargo, los clientes de Francia y España tienen una mediana de supervivencia superior a los 10 años (por eso hay NA).
Su respectiva gráfica muestra cómo los alemanes tienen probabilidades más bajas con respecto a los clientes de los otros dos países. Aquí tambien existen diferencias significativas.
survdiff(Surv(Tenure, Exited) ~ Geography, data = creditos.raw)
## Call:
## survdiff(formula = Surv(Tenure, Exited) ~ Geography, data = creditos.raw)
##
## N Observed Expected (O-E)^2/E (O-E)^2/V
## Geography=France 5014 810 1019 42.7 90.6
## Geography=Germany 2509 814 516 171.6 244.0
## Geography=Spain 2477 413 502 15.8 22.2
##
## Chisq= 244 on 2 degrees of freedom, p= <2e-16
Estimación del tiempo de supervivencia por tipo de miembro del sistema bancario
fitsv3 <- survfit(Surv(Tenure, Exited) ~ IsActiveMember,
data = creditos.raw)
fitsv3
## Call: survfit(formula = Surv(Tenure, Exited) ~ IsActiveMember, data = creditos.raw)
##
## n events median 0.95LCL 0.95UCL
## IsActiveMember=0 4849 1302 10 9 10
## IsActiveMember=1 5151 735 NA NA NA
De manera similar, se puede observar que entre los clientes que no son miembros activos del sistema bancario, el tiempo medio de supervivencia es de 10 años, mientras que para los que son miembros activos, el tiempo medio de supervivencia es superior a 10.
ggsurvplot(fitsv3,
pval = TRUE, conf.int = TRUE,
risk.table.col = "strata",
linetype = "strata",
ggtheme = theme_bw(),
palette = c("#E7B800", "#2E9FDF"))

Muestra la misma tendencia que el análisis por género y, también, hay diferencias significativas entre estar activo o no dentro del sistema bancario.
survdiff(Surv(Tenure, Exited) ~ IsActiveMember, data = creditos.raw)
## Call:
## survdiff(formula = Surv(Tenure, Exited) ~ IsActiveMember, data = creditos.raw)
##
## N Observed Expected (O-E)^2/E (O-E)^2/V
## IsActiveMember=0 4849 1302 1009 85.1 179
## IsActiveMember=1 5151 735 1028 83.5 179
##
## Chisq= 179 on 1 degrees of freedom, p= <2e-16
Vamos a predecir algunas probabilidades de supervivencia:
time1 <- 7
# Supervivencia
predictSurvProb(fitsv1, newdata = data.frame(Gender = "Male"), time = time1)
## [,1]
## [1,] 0.8173867
# No Supervivencia
1-predictSurvProb(fitsv1, newdata = data.frame(Gender = "Male"), time = time1)
## [,1]
## [1,] 0.1826133
time1 <- 7
# Supervivencia
predictSurvProb(fitsv1, newdata = data.frame(Gender = "Female"), time = time1)
## [,1]
## [1,] 0.7283317
# No Supervivencia
1-predictSurvProb(fitsv1, newdata = data.frame(Gender = "Female"), time = time1)
## [,1]
## [1,] 0.2716683
- Hombre de Alemania en el periodo 8:
time1 <- 8
# Supervivencia
predictSurvProb(fitsv1, newdata = data.frame(Gender = "Male",
Geography = "Germany"), time = time1)
## [,1]
## [1,] 0.7659714
# No Supervivencia
1-predictSurvProb(fitsv1, newdata = data.frame(Gender = "Male",
Geography = "Germany"), time = time1)
## [,1]
## [1,] 0.2340286
Conclusiones
Se pudo comprobar que para predecir el abandono de clientes el modelo que mejores resultados obtuvo fue la red neuronal, sin embargo, al hacer el ensamble con los modelos SVM, XGB y RF se obtuvo una mejora marginal, pero siguiendo el principio de parsimonia (simplicidad) se trabajo con el modelo NNET.
Se logró reducir los falsos negativos pero a costa de aumentar los falsos positivos. Lo anterior, fue resultado del objetivo buscado: reducir el error tipo II (pronosticar a un cliente en la clase de los que no se van a ir y al final terminan abandonando el banco).
En cuanto a las variables más importantes de abandono, según el modelo NNET, se pueden nombrar los siguientes: edad, número de productos, país de residencia, actividad como usuario del sistema bancario, balance o saldo y género.
Hablando sobre el tiempo de permanencia, alrededor del 50% de los clientes presumiblemente no van a abandonar dentro de los 10 años. Aún así, si se toman en consideración las covariables, ser ciudadano alemán aumenta la probabilidad de abandono, lo que reduce el tiempo de supervivencia a solo 9 años. Sin embargo, ser ciudadano de otro país y/o ser miembro activo del sistema bancario disminuye la probabilidad de abandono, lo que genera más de 10 años en los que no se espera que los clientes se vayan.
Por tanto, habría que trabajar en conjunto con el área de Marketing para enfocar estratégias y campañas de fidelización en las mujeres alemanas que no son miembros activos del sistema bancario.
LS0tDQp0aXRsZTogIk1vZGVsbyBQcmVkaWN0aXZvOiBBYmFuZG9ubyBkZSBDbGllbnRlcyINCnN1YnRpdGxlOiAiUHJveWVjdG8gRnVnYSBkZSBDbGllbnRlcyINCmF1dGhvcjogIkFsZWphbmRybyBSb2phcyBNb3Jlbm8iDQpkYXRlOiAiOS8xMi8yMDIxIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50Og0KICAgIGNvZGVfZm9sZGluZzogc2hvdw0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICBkZl9wcmludDogcGFnZWQNCiAgICB0aGVtZTogc3BhY2VsYWINCiAgICB0b2M6IHRydWUNCiAgICB0b2NfZmxvYXQ6IHRydWUNCmVkaXRvcl9vcHRpb25zOg0KICBjaHVua19vdXRwdXRfdHlwZTogY29uc29sZQ0KLS0tDQoNCjxicj4NCg0KIyAgPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigwLCAwLCAyMDUpIj5QcmVzZW50YWNpw7NuPC9zcGFuPg0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkVuIGVzdGUgcHJveWVjdG8gc2UgYW5hbGl6YXLDoSBsYSBpbmZvcm1hY2nDs24gZGUgdW5hIGJhc2UgZGUgZGF0b3MgcmVmZXJlbnRlIGEgY3LDqWRpdG9zIGEgcGVyc29uYXMgZsOtc2ljYXMsIGNvbiBlbCBvYmpldGl2byBkZSBpZGVudGlmaWNhciBsb3MgZmFjdG9yZXMgcXVlIGNvbnRyaWJ1eWVuIGVuIG1heW9yIG1lZGlkYSBhIHByZWRlY2lyIGxhIHDDqXJkaWRhIG8gZnVnYSBkZSBjbGllbnRlcywgYXPDrSBjb21vIGVzdGltYXIgZWwgcGVyaW9kbyBkZSB0aWVtcG8gYW50ZXMgZGUgcXVlIG9jdXJyYSBlbCBhYmFuZG9uby4NCg0KU2UgaW5pY2lhcsOhIGNvbiB1biBhbsOhbGlzaXMgZXhwbG9yYXRvcmlvIHBhcmEgY29ub2NlciBsYSBlc3RydWN0dXJhIGRlIGxhIGluZm9ybWFjacOzbiBhc8OtIGNvbW8gdGFtYmnDqW4gaWRlbnRpZmljYXIgc2kgZXhpc3RlbiB2YWxvcmVzIGZhbHRhbnRlcy4gTG8gYW50ZXJpb3IsIHNlcnZpcsOhIHBhcmEgaXJub3MgZmFtaWxpYXJpemFuZG8gY29uIGxhIGluZm9ybWFjacOzbi4NCg0KU2UgdXRpbGl6YXLDoW4gdmFyaW9zIGFsZ29yaXRtb3MgZGUgKipBcHJlbmRpemFqZSBTdXBlcnZpc2FkbyoqIHBhcmEgcHJlY2VkaXIgbGEgZGVzZXJjacOzbiwgeWEgcXVlIGVzIHJlY29tZW5kYWJsZSBoYWNlciBjb21wYXJhY2lvbmVzIGVudHJlIHZhcmlvcyBtb2RlbG9zLCBhZGVtw6FzLCBzZSBpcsOhbiBpZGVudGlmaWNhbmRvIGxvcyBmYWN0b3JlcyBtw6FzIGltcG9ydGFudGVzIHF1ZSBleHBsaWNhbiBsYSBmdWdhIGRlIGNsaWVudGVzLiBUYW1iacOpbiwgc2UgY3JlYXLDoSB1biBtb2RlbG8gZGUgKiplbnNhbWJsZSoqIGNvbW8gc29sdWNpw7NuIGFsdGVybmF0aXZhIHkgc2UgY29tcGFyYXLDoW4gc3VzIHJlc3VsdGFkb3MgY29uIGxvcyBkZWwgbWVqb3IgbW9kZWxvLg0KDQpGaW5hbG1lbnRlLCBzZSBjb25zdHJ1aXLDoSB1biBtb2RlbG8gcGFyYSBwcmVkZWNpciBlbCB0aWVtcG8gbyBtb21lbnRvIGRlIGFiYW5kb25vIG1lZGlhbnRlIHVuICoqYW7DoWxpc2lzIGRlIHN1cGVydml2ZW5jaWEuKioNCg0KTG9zIGRhdG9zIHBhcmEgZXN0ZSBhbsOhbGlzaXMgcHJvdmllbmVuIGRlbCByZXBvc2l0b3JpbyBkZSBhcHJlbmRpemFqZSBhdXRvbcOhdGljbyBkZSBba2FnZ2xlXShodHRwczovL3d3dy5rYWdnbGUuY29tL2FkYW1tYXVzL3ByZWRpY3RpbmctY2h1cm4tZm9yLWJhbmstY3VzdG9tZXJzIyksIHF1ZSBlcyB1biBzaXRpbyB3ZWIgcGFyYSBsYSBjb211bmlkYWQgZGUgKipkYXRhIHNjaWVuY2UqKiwgZG9uZGUgc2UgcHVlZGVuIGVuY29udHJhciBiYXNlcyBkZSBkYXRvcyBwYXJhIHByYWN0aWNhciB5IGNvbXBldGlyIGVuIHByb3llY3Rvcy4gVGFtYmnDqW4sIHNlIGVuY3VlbnRyYW4gZW4gbWkgcmVwb3NpdG9yaW8gZGUgW2dpdGh1Yl0oaHR0cHM6Ly9naXRodWIuY29tL2Fyb2phc21vci9GdWdhQ2xpZW50ZXMpIGp1bnRvIGNvbiBlbCBjw7NkaWdvIGRlIGVzdGEgcHVibGljYWNpw7NuLg0KPC9kaXY+DQoNCjxicj4NCg0KIyAgPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigwLCAwLCAyMDUpIj5Db250ZW5pZG88L3NwYW4+DQoNCiogUHJlc2VudGFjacOzbg0KKiBDb250ZW5pZG8NCiogU2V0IHVwDQoqIENhcmdhIHkgcHJlcGFyYWNpw7NuIGRlIGxvcyBkYXRvcw0KKiBBbsOhbGlzaXMgZXhwbG9yYXRvcmlvDQoqIFByZSBwcm9jZXNhbWllbnRvIGRlIGxvcyBkYXRvcw0KICArIFNlbGVjY2nDs24gZGUgdmFyaWFibGVzDQogICsgQ29kaWZpY2FjacOzbiBkZSB2YXJpYWJsZXMgY2F0ZWfDs3JpY2FzDQogICsgRXNjYWxhbWllbnRvDQogICsgQmFsYW5jZW8gZGUgY2xhc2VzDQoqIE3DqXRyaWNhIGRlIGV2YWx1YWNpw7NuDQoqIENyZWFjacOzbiBkZSBtb2RlbG9zIGRlIGFwcmVuZGl6YWplIGF1dG9tw6F0aWNvDQogICsgTW9kZWxvIGJhc2U6IE5haXZlIEJheWVzDQogICsgUmVncmVzacOzbiBMb2fDrXN0aWNhDQogICsgTcOhcXVpbmFzIGRlIFZlY3RvcmVzIFNvcG9ydGUNCiAgKyBSYW5kb20gRm9yZXN0DQogICsgRXh0cmVtZSBHcmFkaWVudCBCb29zdGluZyAoWEdCb29zdCkNCiAgKyBSZWRlcyBOZXVyb25hbGVzIChSTk4pDQogICsgR3JhZGllbnQgQm9vc3RpbmcgTWFjaGluZSAoR0JNKQ0KKiBDb21wYXJhY2nDs24gZGUgbW9kZWxvcw0KKiBFdmFsdWFjacOzbiBjb24gZGF0b3MgZGUgdmFsaWRhY2nDs24NCiogSW1wb3J0YW5jaWEgZGUgbGFzIHZhcmlhYmxlcw0KKiBFbnNhbWJsZQ0KKiBFbGVjY2nDs24gZGVsIHZhbG9yIGRlIGNvcnRlIGFkZWN1YWRvDQoqIEV2YWx1YWNpw7NuIGZpbmFsOiBkYXRvcyBkZSB0ZXN0DQoqIElkZW50aWZpY2FuZG8gZWwgdGllbXBvIGRlIGFiYW5kb25vIGRlbCBjbGllbnRlDQoqIENvbmNsdXNpb25lcw0KDQo8YnI+DQoNCiMgIDxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMCwgMCwgMjA1KSI+U2V0IHVwPC9zcGFuPg0KDQpJbmljaWFtb3MgY29uZmlndXJhbmRvIGxhcyBvcGNpb25lcyBnZW5lcmFsZXMgcXVlIHZhbW9zIGEgcmVxdWVyaXIgcGFyYSBlbCBkZXNhcnJvbGxvIGRlIGVzdGUgcHJveWVjdG8uDQoNCmBgYHtyIHNldHVwLCBtZXNzYWdlPUZBTFNFLCBjb21tZW50PSIiLCB3YXJuaW5nPUZBTFNFLCByZXN1bHRzPSdoaWRlJ30NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICB3YXJuaW5nID0gRkFMU0UsDQogICAgICAgICAgICAgICAgICAgICAgbWVzc2FnZSA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgICAgIHdhcm5pbmcgPSBGQUxTRSwNCiAgICAgICAgICAgICAgICAgICAgICBmaWcuYWxpZ24gPSAiY2VudGVyIg0KICAgICAgICAgICAgICAgICAgICAgICkNCg0KDQpwYXF1ZXRlcyA8LSBjKCJrbml0ciIsInRpZHl2ZXJzZSIsImdncGxvdDIiLCJncmlkRXh0cmEiLCJEYXRhRXhwbG9yZXIiLCJsdWJyaWRhdGUiLA0KICAgICAgICAgICAgICAic2NhbGVzIiwicnBhcnQiLCJycGFydC5wbG90Iiwia2FibGVFeHRyYSIsICJzdW1tYXJ5dG9vbHMiLCAiRFQiLCAiZ2dwdWJyIiwNCiAgICAgICAgICAgICAgInNmbyIsICJwbHlyIiwgInBsb3RseSIsICJwc3ljaCIsICJqYW5pdG9yIiwgInB1cnJyIiwgImNhcmV0IiwgIlJPU0UiLCAia2xhUiIsDQogICAgICAgICAgICAgICJwYXJhbGxlbCIsICJkb1NOT1ciLCAicmVjaXBlcyIsICJyYW5nZXIiLCAicmFuZG9tRm9yZXN0IiwgImdndGhlbWVzIiwgIlJPQ1IiLA0KICAgICAgICAgICAgICAic2pQbG90IiwgInN1cnZpdmFsIiwgInN1cnZtaW5lciIsICJwZWMiKQ0KDQppbnN0YWxhZG9zIDwtIHBhcXVldGVzICVpbiUgaW5zdGFsbGVkLnBhY2thZ2VzKCkNCg0KaWYoc3VtKGluc3RhbGFkb3MgPT0gRkFMU0UpID4gMCkgew0KICBpbnN0YWxsLnBhY2thZ2VzKHBhcXVldGVzWyFpbnN0YWxhZG9zXSkNCn0NCg0KbGFwcGx5KHBhcXVldGVzLCByZXF1aXJlLCBjaGFyYWN0ZXIub25seSA9IFRSVUUpDQoNCiMgc2UgY2FyZ2FuIGZ1bmNpb25lcw0Kc291cmNlKCJjdXRvZmYuUiIpDQoNCmBgYA0KDQo8YnI+DQoNCiMgIDxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMCwgMCwgMjA1KSI+Q2FyZ2EgeSBwcmVwYXJhY2nDs24gZGUgbG9zIGRhdG9zPC9zcGFuPg0KDQpDb21lbmNlbW9zIHBvciBjYXJnYXIgZWwgY29uanVudG8gZGUgZGF0b3MgeSBoYWNlciB1biBwcmltZXIgYW7DoWxpc2lzIGRlc2NyaXB0aXZvIGLDoXNpY28gcGFyYSB0ZW5lciB1bmEgaWRlYSBkZSBzdSB0YW1hw7FvIHkgZWwgdGlwbyBkZSBjYWRhIHZhcmlhYmxlLg0KDQpgYGB7cn0NCmNyZWRpdG9zIDwtIHJlYWRfY3N2KCJDaHVybl9Nb2RlbGxpbmcuY3N2IikNCg0KY3JlZGl0b3MucmF3IDwtIGNyZWRpdG9zICMgcGFyYSB1c2Fyc2UgYWwgZmluYWwgY29uIGVsIG1vZGVsbyBkZSBwZXJtYW5lbmNpYQ0KDQpzdHIoY3JlZGl0b3MpDQoNCkRUOjpkYXRhdGFibGUoaGVhZChjcmVkaXRvcywgMjApLA0KICAgICAgICAgICAgICByb3duYW1lcyA9IEZBTFNFLA0KICAgICAgICAgICAgICBvcHRpb25zID0gbGlzdCgNCiAgICAgICAgICAgICAgICBwYWdlTGVuZ3RoID0gNSwNCiAgICAgICAgICAgICAgICBzY3JvbGxYID0gVFJVRSkpDQoNCmBgYA0KDQpUZW5lbW9zIHVuYSBiYXNlIGRlIDEwLDAwMCByZWdpc3Ryb3MgeSAxNCBjb2x1bW5hcywgZGUgbGFzIGN1YWxlcyBzb2xvIDMgc29uIGRlIHRpcG8gY2hhcmFjdGVyIHkgZWwgcmVzdG8gbnVtw6lyaWNhcy4NCg0KTGEgw7psdGltYSBjYXJhY3RlcsOtc3RpY2EsICdFeGl0ZWQnLCBlcyBsYSB2YXJpYWJsZSBvYmpldGl2byBlIGluZGljYSBzaSBlbCBjbGllbnRlIHNlIGhhIGlkbyBvIGFiYW5kb25hZG8gbGEgaW5zdGl0dWNpw7NuICgwID0gTm8sIDEgPSBTw60pLg0KDQo8YnI+DQoNCiMgIDxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMCwgMCwgMjA1KSI+QW7DoWxpc2lzIGV4cGxvcmF0b3Jpbzwvc3Bhbj4NCg0KRW1wZWNlbW9zIGVsIGFuw6FsaXNpcyBleHBsb3JhdG9yaW8gaWRlbnRpZmljYW5kbyBzaSBoYXkgZGF0b3MgZmFsdGFudGVzIHkgc2kgbG9zIGhheSBkZWJlbW9zIGNvbm9jZXIgY3XDoW50b3Mgc29uIHkgZW4gcXXDqSB2YXJpYWJsZXM6DQoNCmBgYHtyfQ0KY3JlZGl0b3MgJT4lIA0KICBzdW1tYXJpc2VfYWxsKH5zdW0oaXMubmEoLikpKSAlPiUgDQogIHQoKQ0KDQpwbG90X21pc3NpbmcoY3JlZGl0b3MsIHRpdGxlID0gIlBvcmNlbnRhamUgZGUgRGF0b3MgSW5jb21wbGV0b3MiLA0KICAgICAgICAgICAgIGdlb21fbGFiZWxfYXJncyA9IGxpc3QoInNpemUiID0gMywgImxhYmVsLnBhZGRpbmciID0gdW5pdCgwLjEsICJsaW5lcyIpKSwNCiAgICAgICAgICAgICBnZ3RoZW1lID0gdGhlbWVfbWluaW1hbCgpKQ0KDQoNCmBgYA0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkxvcyByZXN1bHRhZG9zIGFudGVyaW9yZXMgeSBsYSBncsOhZmljYSBtdWVzdHJhbiBxdWUgZW4gbGEgYmFzZSBkZSBkYXRvcyBubyBoYXkgb2JzZXJ2YWNpb25lcyBmYWx0YW50ZXMuIEVzdG8gZXMgYnVlbmEgbm90aWNpYSwgeWEgcXVlIG5vIHRlbmRyZW1vcyBxdWUgaGFjZXIgbmluZ3VuYSBpbXB1dGFjacOzbi4gU2luIGVtYmFyZ28sIGRlYmVtb3MgY2hlY2FyLCB0YW1iacOpbiwgcXVlIG5vIGhheWEgdmFsb3JlcyAiTlVMTCIgZW4gYWxndW5hIHBhcnRlIGRlIGxhIGJhc2UsIHBvcnF1ZSBzdWVsZSBwYXNhciBxdWUgZGljaGEgcGFsYWJyYSBlc3TDqSBlc2NyaXRhIGNvbW8gdGV4dG8sIHNpdHVhY2nDs24gcXVlIG5vIGlkZW50aWZpY2Fyw61hIGBSYCBjb21vIHVuIHZhbG9yIHBlcmRpZG8uDQo8L2Rpdj4NCg0KYGBge3J9DQpudWxscyA8LSBmdW5jdGlvbih2YXJpYWJsZSkgew0KICBzdW0oaWZfZWxzZSh2YXJpYWJsZSA9PSAnTlVMTCcsIDEsIDApKQ0KfQ0KDQpjcmVkaXRvcyAlPiUgDQogIG11dGF0ZV9hbGwoYXMuY2hhcmFjdGVyKSAlPiUgDQogIGxhcHBseSguLCBudWxscykNCmBgYA0KDQpBZm9ydHVuYWRhbWVudGUsIHRhbXBvY28gdGVuZW1vcyBsYSBwYWxhYnJhICJOVUxMIiBkZW50cm8gZGUgbGEgYmFzZSBkZSBkYXRvcywgcG9yIGxvIHRhbnRvLCBwb2RlbW9zIHNlZ3VpciBjb24gZWwgYW7DoWxpc2lzIGRlc2NyaXB0aXZvLg0KDQpGaW5hbG1lbnRlLCBjb21wcm9iYW1vcyBxdWUgbm8gdGVuZW1vcyByZWdpc3Ryb3MgZHVwbGljYWRvczoNCg0KYGBge3J9DQpucm93KGNyZWRpdG9zW2R1cGxpY2F0ZWQoY3JlZGl0b3MpLCBdKQ0KYGBgDQoNClBhc2Vtb3MgYSB2ZXIgYWxndW5vcyByZXPDum1lbmVzIGVzdGFkw61zdGljb3MgYsOhc2ljb3MgZGUgbGFzIHZhcmlhYmxlcyBudW3DqXJpY2FzIHBhcmEgZW1wZXphciBhIGNvbXByZW5kZXIgbWVqb3IgbGEgaW5mb3JtYWNpw7NuIHF1ZSB0ZW5lbW9zOg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0KY3JlZGl0b3MgJT4lIA0KICBkcGx5cjo6c2VsZWN0KC1jKCJSb3dOdW1iZXIiLCAiQ3VzdG9tZXJJZCIsICJTdXJuYW1lIiwgIkdlb2dyYXBoeSIsICJHZW5kZXIiKSkgJT4lIA0KICBkZXNjcmliZUJ5KCkgJT4lIA0KICBrYWJsZSggDQogICAgICBmb3JtYXQgPSAiaHRtbCIsDQogICAgICBkaWdpdHMgPSAyLCANCiAgICAgIHNpemUgPSA1LA0KICAgICAgY2FwdGlvbiA9ICJSZXN1bWVuIEVzdGFkw61zdGljbyIsDQogICAgICBmb3JtYXQuYXJncyA9IGxpc3QoYmlnLm1hcmsgPSAiLCIpLA0KICAgICAgdGFibGUuYXR0ciA9ICJzdHlsZT0nd2lkdGg6NTAlOyciKSAlPiUNCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiaG92ZXIiKSkNCg0KYGBgDQoNCkRlIGxhIHRhYmxhIGFudGVyaW9yLCBwb2RlbW9zIGlkZW50aWZpY2FyIGFsZ3VuYXMgY29zYXMgaW50ZXJlc2FudGVzOg0KDQoqIEVsIHJhbmdvIGRlIGVkYWQgb3NjaWxhIGVudHJlIDE4IHkgOTIgYcOxb3MsIGNvbiB1biBwcm9tZWRpbyBkZSAzOSwgYXByb3hpbWFkYW1lbnRlLg0KKiBMYSBwZXJtYW5lbmNpYSBtZWRpYSBlcyBkZSA1IGHDsW9zLCBwb3IgbG8gcXVlLCBsYSBtYXlvcsOtYSBkZSBsb3MgY2xpZW50ZXMgc29uIGxlYWxlcyAobWFkIGRlIDMpLg0KKiBBcHJveGltYWRhbWVudGUgZWwgNTAlIGRlIGxvcyBjbGllbnRlcyBlc3TDoW4gYWN0aXZvcy4NCg0KYGBge3J9DQpjcmVkaXRvcyAlPiUNCiAgZ3JvdXBfYnkoR2VuZGVyKSAlPiUNCiAgc3VtbWFyaXNlKG51bS5jdXN0b21lcnMgPSBuKCksIA0KICAgICAgICAgICAgbWVkaWEuY3JTY29yZSA9IHJvdW5kKG1lYW4oQ3JlZGl0U2NvcmUpLCAyKSwNCiAgICAgICAgICAgIG1lZGlhLmFnZSA9IHJvdW5kKG1lYW4oQWdlKSwgMiksIA0KICAgICAgICAgICAgbWVkaWEudGVudXJlID0gcm91bmQobWVhbihUZW51cmUpLCAyKSwNCiAgICAgICAgICAgIG1lZGlhLmJhbGFuY2UgPSByb3VuZChtZWFuKEJhbGFuY2UpLCAyKSwNCiAgICAgICAgICAgIG1lZGlhLnNhbGFyeSA9IHJvdW5kKG1lYW4oRXN0aW1hdGVkU2FsYXJ5KSwgMikpICU+JSANCiAga2FibGUoIA0KICAgICAgZm9ybWF0ID0gImh0bWwiLA0KICAgICAgZGlnaXRzID0gMiwNCiAgICAgIGFsaWduID0gImMiLA0KICAgICAgY2FwdGlvbiA9ICJSZXN1bWVuIEVzdGFkw61zdGljbyBwb3IgR8OpbmVybyIsDQogICAgICBmb3JtYXQuYXJncyA9IGxpc3QoYmlnLm1hcmsgPSAiLCIpLA0KICAgICAgdGFibGUuYXR0ciA9ICJzdHlsZT0nd2lkdGg6NTAlOyciKSAlPiUNCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiaG92ZXIiKSkNCg0KYGBgDQoNCkVuIGVzdGUgb3RybyByZXN1bWVuIGVzdGFkw61zdGljbyBwb3IgZ8OpbmVybyBwb2RlbW9zIGlkZW50aWZpY2FyOg0KDQoqIEhheSBtw6FzIGNsaWVudGVzIGhvbWJyZXMgcXVlIG11amVyZXMuDQoqIExhcyBlZGFkZXMgcHJvbWVkaW8gZGUgYW1ib3MgZ8OpbmVyb3MgY2FzaSBzb24gaWd1YWxlcywgc2llbmRvIGVsIGRlIGxhcyBtdWplcmVzIDEgYcOxbyBtYXlvci4NCiogRWwgdGllbXBvIGRlIHBlcm1hbmVuY2lhIHBhcmEgYW1ib3Mgc2V4b3MgZXMsIHByw6FjdGljYW1lbnRlLCBlbCBtaXNtbzogNSBhw7Fvcy4NCiogRWwgc2FsZG8gcXVlIG1hbnRpZW5lbiBhaG9ycmFkbyBlcyBtYXlvciBlbiBsb3MgaG9tYnJlcy4NCiogTGEgbWVkaWEgZGVsIHNhbGFyaW8gZXMgbGlnZXJhbWVudGUgbWF5b3IgZW4gZWwgZ3J1cG8gZGUgbGFzIG11amVyZXMuDQoNCkhheSB2YXJpYWJsZXMgcXVlIHNvbiBudW3DqXJpY2FzIHBlcm8gcXVlIGRlYmVyw61hbiBzZXIgZGUgdGlwbyBmYWN0b3IgY29tbywgcG9yIGVqZW1wbG8sIGxhIHZhcmlhYmxlIG9iamV0aXZvIEV4aXRlZC4gQSBjb250aW51YWNpw7NuLCBzZSBoYXLDoW4gbG9zIGNhbWJpb3MgZW4gbG9zIHRpcG9zLCBwZXJvIGFudGVzIHF1aXRhcmVtb3MgbGFzIHZhcmlhYmxlcyBxdWUgbm8gc2Vyw6FuIMO6dGlsZXMgcGFyYSBlbCBhbsOhbGlzaXM6IFJvd051bWJlciwgQ3VzdG9tZXJJZCB5IFN1cm5hbWU6DQoNCmBgYHtyfQ0KY3JlZGl0b3MgPC0gY3JlZGl0b3MgJT4lIA0KICBkcGx5cjo6c2VsZWN0KC1jKFJvd051bWJlciwgQ3VzdG9tZXJJZCwgU3VybmFtZSkpDQoNCmBgYA0KDQpgYGB7cn0NCmEuZmFjdG9yIDwtIGMoIlRlbnVyZSIsICJOdW1PZlByb2R1Y3RzIiwgIkhhc0NyQ2FyZCIsICJJc0FjdGl2ZU1lbWJlciIsICJFeGl0ZWQiKQ0KDQpjcmVkaXRvcyA8LSBjcmVkaXRvcyAlPiUgDQogIG11dGF0ZV9hdChhbGxfb2YoYS5mYWN0b3IpLCBhcy5mYWN0b3IpDQoNCmBgYA0KDQpBaG9yYSwgY29uZmlybWFtb3MgcXVlIGVzdMOhbiBiaWVuIGxvcyBuaXZlbGVzIGRlIGxhcyB2YXJpYWJsZXMgcXVlIHBhc2Ftb3MgYSB0aXBvIGZhY3RvciB5IGRlIHBhc28gdmVtb3Mgc3VzIHByb3BvcmNpb25lczoNCg0KYGBge3J9DQpsZXZlbHMoY3JlZGl0b3MkRXhpdGVkKQ0KDQpwcm9wLnRhYmxlKHRhYmxlKGNyZWRpdG9zJEV4aXRlZCkpDQoNCmBgYA0KDQpMYSBwcm9wb3JjacOzbiBkZSBjbGllbnRlcyBxdWUgYWJhbmRvbmFyb24gZXM6ICoqYHIgcHJvcC50YWJsZSh0YWJsZShjcmVkaXRvcyRFeGl0ZWQpKVsyXSoxMDBgJSoqDQoNCkxhIHByb3BvcmNpw7NuIGRlIGNsaWVudGVzIHF1ZSBwZXJtYW5lY2VuIGVzOiAqKmByIHByb3AudGFibGUodGFibGUoY3JlZGl0b3MkRXhpdGVkKSlbMV0qMTAwYCUqKg0KDQpgYGB7cn0NCmxldmVscyhjcmVkaXRvcyROdW1PZlByb2R1Y3RzKQ0KDQpwcm9wLnRhYmxlKHRhYmxlKGNyZWRpdG9zJE51bU9mUHJvZHVjdHMpKQ0KDQpgYGANCg0KTGEgcHJvcG9yY2nDs24gZGUgY2xpZW50ZXMgY29uIDEgcHJvZHVjdG8gZXM6ICoqYHIgcHJvcC50YWJsZSh0YWJsZShjcmVkaXRvcyROdW1PZlByb2R1Y3RzKSlbMV0qMTAwYCUqKg0KDQpMYSBwcm9wb3JjacOzbiBkZSBjbGllbnRlcyBjb24gMiBwcm9kdWN0b3MgZXM6ICoqYHIgcHJvcC50YWJsZSh0YWJsZShjcmVkaXRvcyROdW1PZlByb2R1Y3RzKSlbMl0qMTAwYCUqKg0KDQpMYSBwcm9wb3JjacOzbiBkZSBjbGllbnRlcyBjb24gMyBwcm9kdWN0b3MgZXM6ICoqYHIgcHJvcC50YWJsZSh0YWJsZShjcmVkaXRvcyROdW1PZlByb2R1Y3RzKSlbM10qMTAwYCUqKg0KDQpMYSBwcm9wb3JjacOzbiBkZSBjbGllbnRlcyBjb24gNCBwcm9kdWN0b3MgZXM6ICoqYHIgcHJvcC50YWJsZSh0YWJsZShjcmVkaXRvcyROdW1PZlByb2R1Y3RzKSlbNF0qMTAwYCUqKg0KDQpgYGB7cn0NCmxldmVscyhjcmVkaXRvcyRIYXNDckNhcmQpDQoNCnByb3AudGFibGUodGFibGUoY3JlZGl0b3MkSGFzQ3JDYXJkKSkNCg0KYGBgDQoNCkxhIHByb3BvcmNpw7NuIGRlIGNsaWVudGVzIGNvbiB0YXJqZXRhIGRlIGNyw6lkaXRvIGVzOiAqKmByIHByb3AudGFibGUodGFibGUoY3JlZGl0b3MkSGFzQ3JDYXJkKSlbMl0qMTAwYCUqKg0KDQpMYSBwcm9wb3JjacOzbiBkZSBjbGllbnRlcyBzaW4gdGFyamV0YSBkZSBjcsOpZGl0byBlczogKipgciBwcm9wLnRhYmxlKHRhYmxlKGNyZWRpdG9zJEhhc0NyQ2FyZCkpWzFdKjEwMGAlKioNCg0KYGBge3J9DQpsZXZlbHMoY3JlZGl0b3MkSXNBY3RpdmVNZW1iZXIpDQoNCnByb3AudGFibGUodGFibGUoY3JlZGl0b3MkSXNBY3RpdmVNZW1iZXIpKQ0KDQpgYGANCg0KTGEgcHJvcG9yY2nDs24gZGUgbG9zIGNsaWVudGVzIGFjdGl2b3MgZXM6ICoqYHIgcHJvcC50YWJsZSh0YWJsZShjcmVkaXRvcyRJc0FjdGl2ZU1lbWJlcikpWzJdKjEwMGAlKioNCg0KTGEgcHJvcG9yY2nDs24gZGUgbG9zIGNsaWVudGVzIG5vIGFjdGl2b3MgZXM6ICoqYHIgcHJvcC50YWJsZSh0YWJsZShjcmVkaXRvcyRJc0FjdGl2ZU1lbWJlcikpWzFdKjEwMGAlKioNCg0KTGFzIHZhcmlhYmxlcyBxdWUgc2UgcGFzYXJvbiBhIHRpcG8gZmFjdG9yIGVzdMOhbiBiaWVuLg0KDQombmJzcDsNCg0KVmFyaWFibGVzIGNhdGVnw7NyaWNhcw0KDQpTZSBoYXLDoSB1biBhbsOhbGlzaXMgdmlzdWFsIGEgbGFzIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXMsIHBhcmEgY29ub2NlciBzdXMgZGlzdHJpYnVjaW9uZXMgZSBpZGVudGlmaWNhciBzaSBwcmVzZW50YW4gYWx0YSBvIGJhamEgY2FyZGluYWxpZGFkLg0KDQpgYGB7cn0NCmNyZWRpdG9zICU+JSANCiAgc2VsZWN0X2lmKGlzLmZhY3RvcikgJT4lIA0KICBtdXRhdGUoaWQgPSAxOm5yb3coLikpICU+JSANCiAgcGl2b3RfbG9uZ2VyKC1pZCwgbmFtZXNfdG8gPSAndmFyaWFibGUnLCB2YWx1ZXNfdG8gPSAndmFsb3InKSAlPiUgDQogIGdncGxvdChhZXModmFsb3IpKSArDQogIGdlb21fYmFyKCkgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpjb21tYSkgKyANCiAgZmFjZXRfd3JhcCh+dmFyaWFibGUsIHNjYWxlcyA9ICdmcmVlJywgbmNvbCA9IDIpDQoNCmBgYA0KDQpgYGB7cn0NCmNyZWRpdG9zICU+JSANCiAgc2VsZWN0X2lmKGlzLmNoYXJhY3RlcikgJT4lIA0KICBtdXRhdGUoaWQgPSAxOm5yb3coLikpICU+JSANCiAgcGl2b3RfbG9uZ2VyKC1pZCwgbmFtZXNfdG8gPSAndmFyaWFibGUnLCB2YWx1ZXNfdG8gPSAndmFsb3InKSAlPiUgDQogIGdncGxvdChhZXModmFsb3IpKSArDQogIGdlb21fYmFyKCkgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpjb21tYSkgKyANCiAgZmFjZXRfd3JhcCh+dmFyaWFibGUsIHNjYWxlcyA9ICdmcmVlJywgbmNvbCA9IDIpDQoNCmBgYA0KDQpMYXMgdmFyaWFibGVzIGNhdGVnw7NyaWNhcyBzZSB2ZW4gYmllbiwgc29sbyBsYSB2YXJpYWJsZSBOdW1PZlByb2R1Y3RzIHBvZHLDrWFtb3MgbW9kaWZpY2FybGEgcGFyYSBqdW50YXIgZWwgcHJvZHVjdG8gNCBjb24gZWwgcHJvZHVjdG8gMyB5IG5vbWJyYXIgZXN0ZSBncnVwbyBjb21vICI+MiINCg0KQWhvcmEsIHZlYW1vcyBsYXMgZGlzdHJpYnVjaW9uZXMgZGUgbGFzIHZhcmlhYmxlcyBudW3DqXJpY2FzIGNvbiBhcG95byBkZSB1bm9zIGJveHBsb3RzIHkgZGUgdW4gcmVzw7ptZW4gZXN0YWTDrXN0aWNvIGLDoXNpY28uDQoNCiZuYnNwOw0KDQpWYXJpYWJsZXMgbnVtw6lyaWNhcw0KDQpgYGB7cn0NCmNyZWRpdG9zICU+JSANCiAgc2VsZWN0X2lmKGlzLm51bWVyaWMpICU+JSANCiAgbXV0YXRlKGlkID0gMTpucm93KC4pKSAlPiUgDQogIHBpdm90X2xvbmdlcigtaWQsIG5hbWVzX3RvID0gJ3ZhcmlhYmxlJywgdmFsdWVzX3RvID0gJ3ZhbG9yJykgJT4lIA0KICBnZ3Bsb3QoYWVzKHZhbG9yKSkgKw0KICBnZW9tX2JveHBsb3Qob3V0bGllci5jb2xvdXIgPSAicmVkIiwgZmlsbCA9ICcjQTRBNEE0JywgY29sb3IgPSAiZGFya3JlZCIpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6Y29tbWEpICsgDQogIGZhY2V0X3dyYXAofnZhcmlhYmxlLCBzY2FsZXMgPSAnZnJlZScsIG5jb2wgPSAyKSsNCiAgY29vcmRfZmxpcCgpDQoNCmBgYA0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0KY3JlZGl0b3MgJT4lIA0KICBzZWxlY3RfaWYoaXMubnVtZXJpYykgJT4lIA0KICBtdXRhdGUoaWQgPSAxOm5yb3coLikpICU+JSANCiAgcGl2b3RfbG9uZ2VyKC1pZCwgbmFtZXNfdG8gPSAndmFyaWFibGUnLCB2YWx1ZXNfdG8gPSAndmFsb3InKSAlPiUgDQogIGdncGxvdChhZXModmFsb3IpKSArDQogIGdlb21faGlzdG9ncmFtKGZpbGwgPSAic3RlZWxibHVlIiwgY29sb3IgPSAiZmlyZWJyaWNrIikgKw0KICBzY2FsZV94X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpjb21tYSkgKyANCiAgZmFjZXRfd3JhcCh+dmFyaWFibGUsIHNjYWxlcyA9ICdmcmVlJywgbmNvbCA9IDIpDQoNCmBgYA0KDQoNCkRlIGxvcyBncsOhZmljb3MgYW50ZXJpb3JlcyBwb2RlbW9zIGlkZW50aWZpY2FyIGxvIHNpZ3VpZW50ZToNCg0KKiBMYSB2YXJpYWJsZSBBZ2UsIHByZXNlbnRhIGFsZ3Vub3MgdmFsb3JlcyBhdMOtcGljb3MgcXVlIHBhcmVjZW4gY29ycmVzcG9uZGVyIGEgcGVyc29uYXMgZW50cmUgbG9zIDYwIHkgOTIgYcOxb3MuDQoqIEVuIGxhIHZhcmlhYmxlIENyZWRpdFNjb3JlLCB0YW1iacOpbiwgaGF5IHZhbG9yZXMgYXTDrXBpY29zIGJham9zIChtZW5vcmVzIGEgNDAwIHB0cykuDQoqIExhIHZhcmlhYmxlIEJhbGFuY2UsIHF1aXRhbmRvIGVsIGJsb3F1ZSBkZSBjbGllbnRlcyBxdWUgbm8gdGllbmVuIHNhbGRvLCBwcmVzZW50YSB1bmEgZGlzdGlidWNpw7NuIG5vcm1hbC4NCiogRmluYWxtZW50ZSwgbGEgdmFyaWFibGUgRXN0aW1hdGVkU2FsYXJ5IHByZXNlbnRhIHVuYSBkaXN0cmlidWNpw7NuIGNhc2kgdW5pZm9ybWUuDQoNCmBgYHtyfQ0KY29ycmVsYWNpb24gPC0gY3JlZGl0b3MgJT4lIA0KICBzZWxlY3RfaWYoaXMubnVtZXJpYykgJT4lIA0KICBjb3IoKSAlPiUgDQogIHJvdW5kKDIpDQoNCmNvclBsb3QoY29ycmVsYWNpb24sIGNleCA9IDEuOCwgbWFpbiA9ICJNYXRyaXogZGUgQ29ycmVsYWNpw7NuIikNCg0KYGBgDQoNCmBgYHtyfQ0KdGVzdC5jb3IgPC0gY3JlZGl0b3MgJT4lIA0KICBzZWxlY3RfaWYoaXMubnVtZXJpYykNCg0KZmluZENvcnJlbGF0aW9uKGNvcih0ZXN0LmNvciksIGN1dG9mZiA9IDAuNSwgbmFtZXMgPSBUUlVFICkNCg0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KTGEgZ3LDoWZpY2EgZGUgbWF0cml6IG11ZXN0cmEgcXVlIG5vIGhheSBjb3JyZWxhY2lvbmVzIGFsdGFzIGVudHJlIGxhcyB2YXJpYWJsZXMsIHBvciBsbyBxdWUsIGVzIGJ1ZW5hIG5vdGljaWEsIHlhIHF1ZSBubyB0ZW5lbW9zIGVsIHByb2JsZW1hIGRlIGxhIG11bHRpY29saW5lYWxpZGFkLiBMbyBhbnRlcmlvciwgdGFtYmnDqW4sIHNlIGNvbXBydWViYSBjb24gbGEgZnVuY2nDs24gZmluZENvcnJlbGF0aW9uIGRlbCBwYXF1ZXRlIGNhcmV0IGVuIGxhIGN1YWwgc2UgcG9uZSBjb21vIHVtYnJhbCB1biBjb2VmaWNpZW50ZSBkZSBjb3JyZWxhY2nDs24gZGUgMC41IHkgbG9zIHJlc3VsdGFkb3MgbXVlc3RyYW4gcXVlIG5vIGhheSBjb3JyZWxhY2lvbmVzIHF1ZSBzdXBlcmFuIGVzZSBuaXZlbC4NCjwvZGl2Pg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0KY3JlZGl0b3MgJT4lIA0KICBzZWxlY3RfaWYoaXMubnVtZXJpYykgJT4lIA0KICBkZXNjcmliZUJ5KCkgJT4lIA0KICBrYWJsZSggDQogICAgICBmb3JtYXQgPSAiaHRtbCIsDQogICAgICBkaWdpdHMgPSAyLA0KICAgICAgc2l6ZSA9IDUsDQogICAgICBjYXB0aW9uID0gIlJlc3VtZW4gRXN0YWTDrXN0aWNvOiB2YXJpYWJsZXMgbnVtw6lyaWNhcyIsDQogICAgICBmb3JtYXQuYXJncyA9IGxpc3QoYmlnLm1hcmsgPSAiLCIpLA0KICAgICAgdGFibGUuYXR0ciA9ICJzdHlsZT0nd2lkdGg6NTAlOyciKSAlPiUNCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiaG92ZXIiKSkNCg0KYGBgDQoNCiZuYnNwOw0KDQpBIGNvbnRpbnVhY2nDs24sIHNlIGNyZWFyw6FuIGFsZ3VuYXMgZ3LDoWZpY2FzIGNvbWJpbmFuZG8gbGFzIHZhcmlhYmxlcyBwYXJhIHRlbmVyIHVuIHBhbm9yYW1hIG3DoXMgY29tcGxldG8gZSBpbnRlZ3JhbCBkZSBsb3MgZGF0b3MsIGFkZW3DoXMsIGVzdG8gc2Vydmlyw6EgcGFyYSBpciBpZGVudGlmaWNhbmRvIGxhcyBwb3NpYmxlcyB2YXJpYWJsZXMgcXVlIHB1ZWRlbiBzZXIgYnVlbmFzIHByZWRpY3RvcmFzLg0KDQpgYGB7cn0NCmNyZWRpdG9zICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gR2VvZ3JhcGh5LCBncm91cCA9IEdlbmRlcikpICsgDQogICAgZ2VvbV9iYXIoYWVzKHkgPSAuLnByb3AuLiwgZmlsbCA9IGZhY3RvciguLnguLikpLCANCiAgICBzdGF0PSJjb3VudCIsIGNvbG9yID0gImJsYWNrIiwgYWxwaGEgPSAwLjcpICsNCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gYygic3RlZWxibHVlIiwgImdyZXk1MCIsICJjb3JuZmxvd2VyYmx1ZSIpKSArDQogICAgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IHNjYWxlczo6cGVyY2VudCguLnByb3AuLiksDQogICAgeT0gLi5wcm9wLi4gKSwgc3RhdD0gImNvdW50IiwgcG9zaXRpb24gPSBwb3NpdGlvbl9zdGFjayh2anVzdCA9IDAuNSksIHNpemUgPSA0LjUpICsNCiAgICBmYWNldF9ncmlkKH5HZW5kZXIpICsNCiAgICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50KSsNCiAgICBndWlkZXMoZmlsbCA9ICdub25lJykrDQogICAgbGFicyh0aXRsZSA9ICJHw6luZXJvIHBvciBQYcOtcyIsIA0KICAgIHggPSAiIiwgeSA9ICJQb3JjZW50YWplIikrDQogICAgdGhlbWVfbWluaW1hbCgpKw0KICAgIHRoZW1lKHRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDEyKSwgDQogICAgcGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdCA9IDAuNSksIHRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSAxMikpDQoNCmBgYA0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkNvbW8gc2UgcHVlZGUgdmVyIGVuIGVsIGdyw6FmaWNvLCBsYSBkaXN0cmlidWNpw7NuIGdlb2dyw6FmaWNhIGRlIGNsaWVudGVzIHBhcmEgaG9tYnJlcyB5IG11amVyZXMgZXMgbXV5IHNpbWlsYXIuIEFwcm94aW1hZGFtZW50ZSwgbGEgbWlzbWEgY2FudGlkYWQgZGUgY2xpZW50ZXMgcHJvdmllbmVuIGRlIGxvcyAzIHBhw61zZXM6IGxhIG1pdGFkIHByb3ZpZW5lIGRlIEZyYW5jaWEsIHVuYSBjdWFydGEgcGFydGUgcHJvdmllbmUgZGUgQWxlbWFuaWEgeSBvdHJhIGN1YXJ0YSBwYXJ0ZSwgZGUgRXNwYcOxYS4gRXN0byBlcyBzaW1pbGFyIHRhbnRvIHBhcmEgaG9tYnJlcyBjb21vIHBhcmEgbXVqZXJlcy4NCjwvZGl2Pg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0KY3JlZGl0b3MgJT4lIA0KICBncm91cF9ieShHZW5kZXIsIEdlb2dyYXBoeSkgJT4lIA0KICBzdW1tYXJpc2UobWVkaWFfc2FsID0gcm91bmQobWVhbihFc3RpbWF0ZWRTYWxhcnkpLCAyKSkgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBHZW5kZXIsIHkgPSBtZWRpYV9zYWwsIGZpbGwgPSBHZW5kZXIpKSArIA0KICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBjKCJzdGVlbGJsdWUiLCAiZ3JleTUwIikpICsNCiAgICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IiwgY29sb3IgPSAiYmxhY2siLCBhbHBoYSA9IDAuNykgKw0KICAgIGdlb21fdGV4dChhZXMobGFiZWwgPSBzY2FsZXM6OmNvbW1hKG1lZGlhX3NhbCksIA0KICAgICAgICAgICAgICAgICAgdmp1c3QgPSAtLjUpLCBzaXplID0gNCkgKw0KICAgIGZhY2V0X2dyaWQofiBHZW9ncmFwaHkpICsNCiAgICBsYWJzKHRpdGxlID0gIkNvbXBvc2ljacOzbiBkZSBTYWxhcmlvcyBwb3IgUGHDrXMiLCANCiAgICB5ID0gIlNhbGFyaW8gTWVkaW8iKSArDQogICAgdGhlbWVfbWluaW1hbCgpICsNCiAgICBjb29yZF9jYXJ0ZXNpYW4oeWxpbSA9IGMoOTUwMDAsIDEwMzAwMCkpICsNCiAgICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gIHNjYWxlczo6Y29tbWEpICsNCiAgICBndWlkZXMoZmlsbCA9ICdub25lJykgKw0KICAgIHRoZW1lKHRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDEyKSwgDQogICAgcGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdCA9IDAuNSksIHRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSAxMikpDQoNCmBgYA0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCk90cm8gYXNwZWN0byBpbnRlcmVzYW50ZSBwYXJhIGV4cGxvcmFyIGVzIGxhIGRpc3RyaWJ1Y2nDs24gZ2VvZ3LDoWZpY2EgZGUgbG9zIHNhbGFyaW9zIGVudHJlIGhvbWJyZXMgeSBtdWplcmVzLiBFbiBsYSBncsOhZmljYSBhbnRlcmlvciwgc2Ugb2JzZXJ2YSBxdWUgZWwgc2FsYXJpbyBwcm9tZWRpbyBlbiBBbGVtYW5pYSBlcyBtYXlvciBxdWUgZW4gbG9zIG90cm9zIGRvcyBwYcOtc2VzLCBzaWVuZG8gZWwgc2FsYXJpbyBwcm9tZWRpbyBkZSBsYXMgbXVqZXJlcyBlbCBtYXlvci4gVGFtYmnDqW4gaGF5IHVuIG1heW9yIHNhbGFyaW8gcHJvbWVkaW8gcGFyYSBsYXMgbXVqZXJlcyBlbiBFc3Bhw7FhLCBtaWVudHJhcyBxdWUgZW4gRnJhbmNpYSBsYSBzaXR1YWNpw7NuIGVzIGFsIHJldsOpcy4NCg0KVmVhbW9zIGN1w6FudG9zIGNsaWVudGVzIGhhbiBhYmFuZG9uYWRvIHBvciB6b25hIGdlb2dyw6FmaWNhIHkgZ8OpbmVybywgcGFyYSBjb25vY2VyIG3DoXMgYSBwcm9mdW5kaWRhZCBsYSB2YXJpYWJsZSBvYmpldGl2by4NCjwvZGl2Pg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0KZzEgPC0gZ2dwbG90KGNyZWRpdG9zLCBhZXMoeCA9IEV4aXRlZCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICB5ID0gLi5jb3VudC4uL3N1bSguLmNvdW50Li4pLCBmaWxsID0gRXhpdGVkKSkgKyANCiAgICAgICAgICBnZW9tX2Jhcihjb2xvciA9ICJibGFjayIsIGFscGhhID0gMC43KSArDQogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IGMoInN0ZWVsYmx1ZSIsICJncmV5NTAiKSkgKw0KICAgICAgICAgIGdlb21fdGV4dChhZXMobGFiZWwgPSBwYXN0ZTAoZm9ybWF0KCguLmNvdW50Li4vc3VtKC4uY291bnQuLikpKjEwMCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkaWdpdHMgPSA0KSwiJSIpKSwgDQogICAgICAgICAgc2l6ZSA9IDQsIHN0YXQ9ICJjb3VudCIsIHBvc2l0aW9uID0gcG9zaXRpb25fc3RhY2sodmp1c3QgPSAwLjUpKSArDQogICAgICAgICAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHBlcmNlbnQpICsNCiAgICAgICAgICBsYWJzKHRpdGxlID0gIlByb3BvcmNpw7NuIGRlIEFiYW5kb25vIiwgeSAgPSAiUG9yY2VudGFqZSIpICsNCiAgICAgICAgICBzY2FsZV94X2Rpc2NyZXRlKG5hbWUgID0gIsK/RWwgY2xpZW50ZSBhYmFuZG9uw7M/IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgYnJlYWtzID0gYygiMCIsICIxIiksDQogICAgICAgICAgICAgICAgICAgICAgICAgIGxhYmVscyA9IGMoIk5vIiwgIlNpIikpICsNCiAgICAgICAgICB0aGVtZV9taW5pbWFsKCkgKw0KICAgICAgICAgIGd1aWRlcyhmaWxsID0gJ25vbmUnKSArDQogICAgICAgICAgdGhlbWUodGV4dCA9IGVsZW1lbnRfdGV4dChzaXplID0gMTIpLCANCiAgICAgICAgICBwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41KSwgdGl0bGUgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDgpKQ0KDQpnMiA8LSBnZ3Bsb3QoY3JlZGl0b3MsIGFlcyh4ID0gRXhpdGVkLCBncm91cCA9IEdlbmRlcikpICsgDQogICAgZ2VvbV9iYXIoYWVzKHkgPSAuLnByb3AuLiwgZmlsbCA9IGZhY3RvciguLnguLikpLCANCiAgICBzdGF0PSJjb3VudCIsIGNvbG9yID0gImJsYWNrIiwgYWxwaGEgPSAwLjcpICsNCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gYygic3RlZWxibHVlIiwgImdyZXk1MCIpKSArDQogICAgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IHNjYWxlczo6cGVyY2VudCguLnByb3AuLiksDQogICAgeT0gLi5wcm9wLi4gKSwgc3RhdD0gImNvdW50IiwgcG9zaXRpb24gPSBwb3NpdGlvbl9zdGFjayh2anVzdCA9IDAuNSksIHNpemUgPSAzKSArDQogICAgZmFjZXRfZ3JpZChHZW5kZXIgfiBHZW9ncmFwaHkpICsNCiAgICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50KSArDQogICAgc2NhbGVfeF9kaXNjcmV0ZShuYW1lICA9ICLCv0VsIGNsaWVudGUgYWJhbmRvbsOzPyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIGJyZWFrcz1jKCIwIiwgIjEiKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgbGFiZWxzPWMoIk5vIiwgIlNpIikpICsNCiAgICBndWlkZXMoZmlsbCA9ICdub25lJykgKw0KICAgIGxhYnModGl0bGUgPSAiUHJvcG9yY2lvbmVzIGRlIGFiYW5kb25vIHBvciBHw6luZXJvIHkgUGHDrXMiLA0KICAgICAgICAgeSA9ICJQb3JjZW50YWplIikgKw0KICAgIHRoZW1lX21pbmltYWwoKSArDQogICAgdGhlbWUodGV4dCA9IGVsZW1lbnRfdGV4dChzaXplID0gMTIpLCANCiAgICBwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41KSwgdGl0bGUgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDgpKQ0KDQpncmlkLmFycmFuZ2UoZzEsIGcyLCBuY29sID0gMikNCg0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KRW4gbGFzIGdyw6FmaWNhcyBhbnRlcmlvcmVzIHBvZGVtb3MgdmVyIGxhIGluZm9ybWFjacOzbiBtw6FzIGltcG9ydGFudGUgc29icmUgbGFzIHRhc2FzIGRlIGFiYW5kb25vIGVudHJlIGxvcyBjbGllbnRlcy4gU2UgcHVlZGUgZGVjaXIgcXVlIHVubyBkZSBjYWRhIGNpbmNvIGNsaWVudGVzIGFiYW5kb27DsyBsYSBpbnN0aXR1Y2nDs24uIEFkZW3DoXMsIHNlIHB1ZWRlIG9ic2VydmFyIHF1ZSBsYXMgdGFzYXMgZGUgYWJhbmRvbm8gZW4gQWxlbWFuaWEgc29uIGxhcyBtw6FzIGFsdGFzIGVudHJlIGxvcyBkZW3DoXMgcGHDrXNlcywgbG8gYW50ZXJpb3IsIHRhbWJpw6luIHNlIHZlIHJlZmxlamFkbyB0YW50byBwYXJhIGhvbWJyZXMgY29tbyBwYXJhIG11amVyZXMuIExhIG1lbm9yIGNhbnRpZGFkIGRlIGFiYW5kb25vIHNlIGxvY2FsaXphIGVuIEZyYW5jaWEsIG1pZW50cmFzIHF1ZSBFc3Bhw7FhIG9jdXBhIHVuYSBwb3NpY2nDs24gaW50ZXJtZWRpYS4NCg0KQWRlbcOhcywgYWxnbyBpbXBvcnRhbnRlIGEgY29uc2lkZXJhciBlcyBxdWUgbG9zIGRhdG9zIGVzdGFuIGRlc2JhbGFuY2VhZG9zLCBwb3IgbG8gcXVlLCBkZWJlbW9zIHRlbmVybG8gcHJlc2VudGUgYSBsYSBob3JhIGRlIGNvbnN0cnVpciBsb3MgbW9kZWxvcy4NCg0KVHJhdGVtb3MgZGUgaWRlbnRpZmljYXIsIGVuIHByb21lZGlvLCBlbCB0aWVtcG8gZW4gZWwgcXVlIGxvcyBjbGllbnRlcyBhYmFuZG9uYW4gZWwgYmFuY28uDQo8L2Rpdj4NCg0KYGBge3J9DQpjcmVkaXRvcy50IDwtIGNyZWRpdG9zDQpjcmVkaXRvcy50JFRlbnVyZSA8LSBhcy5kb3VibGUoY3JlZGl0b3MudCRUZW51cmUpDQoNCmR1cmFjaW9uIDwtIGNyZWRpdG9zLnQgJT4lIA0KICBncm91cF9ieShFeGl0ZWQpICU+JQ0KICBzdW1tYXJpc2UodGllbXBvLnByb21lZGlvID0gcm91bmQobWVhbihUZW51cmUpLCAyKSkNCg0KZzMgPC0gZ2dwbG90KGR1cmFjaW9uLCBhZXMoeCA9IEV4aXRlZCwgeSA9IHRpZW1wby5wcm9tZWRpbywgZmlsbCA9IEV4aXRlZCkpICsgDQogICAgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIsIGNvbG9yID0gImJsYWNrIiwgYWxwaGEgPSAwLjcpICsNCiAgICBnZW9tX3RleHQoYWVzKGxhYmVsID0gdGllbXBvLnByb21lZGlvLCB2anVzdCA9IC0uNSksIHNpemUgPSA0KSArDQogICAgc2NhbGVfeF9kaXNjcmV0ZShuYW1lICA9ICJFbCBjbGllbnRlIGFiYW5kb27Dsz8iLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBicmVha3MgPSBjKCIwIiwgIjEiKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgbGFiZWxzID0gYygiTm8iLCAiU2kiKSkgKw0KICAgIHRoZW1lX21pbmltYWwoKSArDQogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IGMoInN0ZWVsYmx1ZSIsICJncmV5NTAiKSkgKw0KICAgIGNvb3JkX2NhcnRlc2lhbih5bGltID0gYyg0LjcsIDYuMSkpICsNCiAgICBzY2FsZV95X2NvbnRpbnVvdXMoYnJlYWtzID0gc2VxKDQuOCwgNi4xLCAwLjEpKSArDQogICAgZ3VpZGVzKGZpbGwgPSAnbm9uZScpICsNCiAgICB0aGVtZSh0ZXh0ID0gZWxlbWVudF90ZXh0KHNpemUgPSAxMiksIA0KICAgICAgICAgIHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpLCB0aXRsZSA9IGVsZW1lbnRfdGV4dChzaXplID0gOCkpICsNCiAgICBsYWJzKHRpdGxlID0gIkVzdGF0dXMgUGVybWFuZW5jaWEgZGVsIENsaWVudGUiLCANCiAgICAgICAgIHkgPSAiUGVybWFuZW5jaWEgUHJvbWVkaW8iKQ0KDQoNCmR1cmFjaW9uMiA8LSBjcmVkaXRvcy50ICU+JSANCiAgZ3JvdXBfYnkoRXhpdGVkLCBHZW5kZXIsIEdlb2dyYXBoeSkgJT4lIA0KICBzdW1tYXJpc2UodGllbXBvLnByb21lZGlvID0gcm91bmQobWVhbihUZW51cmUpLCAyKSkNCg0KZzQgPC0gZ2dwbG90KGR1cmFjaW9uMiwgDQogICAgICAgICAgICAgYWVzKHggPSBFeGl0ZWQsIHkgPSB0aWVtcG8ucHJvbWVkaW8sIGdyb3VwID0gR2VuZGVyLCBmaWxsID0gRXhpdGVkKSkgKyANCiAgICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IiwgY29sb3IgPSAiYmxhY2siLCBhbHBoYSA9IDAuNykgKw0KICAgIGdlb21fdGV4dChhZXMobGFiZWwgPSB0aWVtcG8ucHJvbWVkaW8sIHZqdXN0ID0gLS41KSwgc2l6ZSA9IDQpICsNCiAgICBzY2FsZV94X2Rpc2NyZXRlKG5hbWUgID0gIkVsIGNsaWVudGUgYWJhbmRvbsOzPyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIGJyZWFrcyA9IGMoIjAiLCAiMSIpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBsYWJlbHMgPSBjKCJObyIsICJTaSIpKSsNCiAgICBmYWNldF9ncmlkKEdlbmRlciB+IEdlb2dyYXBoeSkgKw0KICAgIHRoZW1lX21pbmltYWwoKSArDQogICAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gYygic3RlZWxibHVlIiwgImdyZXk1MCIpKSArDQogICAgY29vcmRfY2FydGVzaWFuKHlsaW09Yyg0LjUsIDYuMikpICsNCiAgICBzY2FsZV95X2NvbnRpbnVvdXMoYnJlYWtzID0gc2VxKDQuNSwgNi4yLCAwLjEpKSArDQogICAgZ3VpZGVzKGZpbGwgPSAnbm9uZScpKw0KICAgIHRoZW1lKHRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDEyKSwgDQogICAgICAgICAgcGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdCA9IDAuNSksIHRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSA4KSkgKw0KICAgIGxhYnModGl0bGUgPSAiRXN0YXR1cyBQZXJtYW5lbmNpYSBkZWwgQ2xpZW50ZSBwb3IgR8OpbmVybyB5IFBhw61zIiwgDQogICAgICAgICB5ID0gIlBlcm1hbmVuY2lhIFByb21lZGlvIikNCg0KZ3JpZC5hcnJhbmdlKGczLCBnNCwgbmNvbCA9IDIpDQoNCmBgYA0KDQpFbiBsYXMgZ3LDoWZpY2FzIGRlIHBlcm1hbmVuY2lhIHBvZGVtb3MgZGVzdGFjYXIgbG8gc2lndWllbnRlOg0KDQoqIEVsIHRpZW1wbyBwcm9tZWRpbyBkZSBwZXJtYW5lbmNpYSBlbnRyZSBsb3MgY2xpZW50ZXMgcXVlIGFiYW5kb25hbiBlbCBiYW5jbyB5IGxvcyBxdWUgcGVybWFuZWNlbiBzb24gbXV5IHBhcmVjaWRvcy4NCiogRXNwYcOxYSB0aWVuZSBsb3MgcHJvbWVkaW9zIG3DoXMgYmFqb3MgZGUgYWJhbmRvbm8gdGFudG8gZW4gaG9tYnJlcyBjb21vIHBhcmEgbXVqZXJlcy4NCiogQWxlbWFuaWEgcHJlc2VudGEgZWwgcHJvbWVkaW8gbcOhcyBhbHRvIGRlIGFiYW5kb25vIGVuIGVsIGdydXBvIGRlIGxhcyBtdWplcmVzIHkgRnJhbmNpYSBlbiBlbCBncnVwbyBkZSBsb3MgaG9tYnJlcy4NCg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0KZzUgPC0gY3JlZGl0b3MgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBFeGl0ZWQsIHkgPSBBZ2UpKSArDQogIGdlb21fYm94cGxvdChvdXRsaWVyLmNvbG9yID0gInJlZCIsIGZpbGwgPSAnI0E0QTRBNCcsIGNvbG9yID0gImRhcmtyZWQiKSArDQogIGxhYnMoDQogICAgdGl0bGUgPSAiRWRhZCINCiAgKQ0KDQpnNiA8LSBjcmVkaXRvcyAlPiUgDQogIGdncGxvdChhZXMoeCA9IEV4aXRlZCwgeSA9IEVzdGltYXRlZFNhbGFyeSkpICsNCiAgZ2VvbV9ib3hwbG90KG91dGxpZXIuY29sb3IgPSAicmVkIiwgZmlsbCA9ICcjQTRBNEE0JywgY29sb3IgPSAiZGFya3JlZCIpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6Y29tbWEpICsNCiAgbGFicygNCiAgICB0aXRsZSA9ICJTYWxhcmlvIg0KICApDQoNCmc3IDwtIGNyZWRpdG9zICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gRXhpdGVkLCB5ID0gQmFsYW5jZSkpICsNCiAgZ2VvbV9ib3hwbG90KG91dGxpZXIuY29sb3IgPSAicmVkIiwgZmlsbCA9ICcjQTRBNEE0JywgY29sb3IgPSAiZGFya3JlZCIpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6Y29tbWEpICsNCiAgbGFicygNCiAgICB0aXRsZSA9ICJCYWxhbmNlIg0KICApDQoNCmc4IDwtIGNyZWRpdG9zICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gRXhpdGVkLCB5ID0gQ3JlZGl0U2NvcmUpKSArDQogIGdlb21fYm94cGxvdChvdXRsaWVyLmNvbG9yID0gInJlZCIsIGZpbGwgPSAnI0E0QTRBNCcsIGNvbG9yID0gImRhcmtyZWQiKSArDQogIGxhYnMoDQogICAgdGl0bGUgPSAiQ3JlZGl0U2NvcmUiDQogICkNCg0KZ3JpZC5hcnJhbmdlKGc1LCBnNiwgZzcsIGc4LCBuY29sID0gMikNCg0KYGBgDQoNCkVuIGVzdGFzIHVsdGltYXMgZ3LDoWZpY2FzIHBvZGVtb3MgYXByZWNpYXIgcXVlIGRlIGxhcyB2YXJpYWJsZXMgbnVtw6lyaWNhcyBzb2xvIGxhIGVkYWQgcHVlZGVyIHNlciBidWVuIHByZWRpY3RvciwgeWEgcXVlIGxhcyBjYWphcyBkZSBsb3MgYm94cGxvdHMgbm8gc2Ugc29sYXBhbiBhIGRpZmVyZW5jaWEgZGUgbGFzIGRlbcOhcyB2YXJpYWJsZXMuDQoNCjxicj4NCg0KIyAgPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigwLCAwLCAyMDUpIj5QcmUgcHJvY2VzYW1pZW50byBkZSBsb3MgZGF0b3M8L3NwYW4+DQoNCkVsIHByZXByb2Nlc2FtaWVudG8gZGUgZGF0b3MgZXMgZWwgcHJvY2VzbyBkZSBjb252ZXJ0aXIgZGF0b3Mgc2luIHByb2Nlc2FyIG8gImNydWRvcyIgZW4gdW4gZm9ybWF0byBsZWdpYmxlIHF1ZSBlcyBhZGVjdWFkbyBwYXJhIGNvbnN0cnVpciB5IGVudHJlbmFyIG1vZGVsb3MgZGUgYXByZW5kaXphamUgYXV0b23DoXRpY28uDQoNCiMjICA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPlNlbGVjY2nDs24gZGUgdmFyaWFibGVzPC9zcGFuPg0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkNhc2kgYWwgaW5pY2lvIGRlbCBwcm95ZWN0byBzZSBlbXBlesOzIGEgcmVhbGl6YXIgbGEgc2VsZWNjacOzbiBkZSB2YXJpYWJsZXMgY3VhbmRvIHNlIGVsaW1pbmFyb24gbGFzIGNvbHVtbmFzICdSb3dOdW1iZXInLCAnQ3VzdG9tZXJJZCcgeSAnU3VybmFtZScuIEVuIGxhIHNlY2Npw7NuIGRlbCBhbsOhbGlzaXMgZXhwbG9yYXRvcmlvIGlkZW50aWZpY2Ftb3MgdmFyaWFzIGNhcmFjdGVyw61zdGljYXMgbcOhcyBxdWUgcHVlZGVuIGRlc2NhcnRhcnNlIHlhIHF1ZSBubyBwcm9wb3JjaW9uYW4gbmluZ8O6biB2YWxvciBwYXJhIHByZWRlY2lyIG51ZXN0cmEgdmFyaWFibGUgb2JqZXRpdm86IEV4aXRlZC4NCjwvZGl2Pg0KDQoqIExhIHZhcmlhYmxlIEVzdGltYXRlZFNhbGFyeSBtdWVzdHJhIHVuYSBkaXN0cmlidWNpw7NuIGNhc2kgdW5pZm9ybWUgcGFyYSBhbWJvcyB0aXBvcyBkZSBjbGllbnRlcywgcG9yIGxvIHF1ZSwgc2UgcHVlZGUgZGVzY2FydGFyLg0KKiBMYXMgY2F0ZWdvcsOtYXMgZGUgJ1RlbnVyZScgeSAnSGFzQ3JDYXJkJyB0aWVuZW4gdW5hIHRhc2EgZGUgYWJhbmRvbm8gc2ltaWxhciAoYWxyZWRlZG9yIGRlIDIwJSkgeSBzZSBjb25zaWRlcmFuIHJlZHVuZGFudGVzLiBFc3RvIHNlIHB1ZWRlIGNvbmZpcm1hciBjb24gdW5hIHBydWViYSBkZSBjaGktY3VhZHJhZG86DQoNCmBgYHtyfQ0KY3JlZGl0b3MgJT4lIA0KICB0YWJ5bChUZW51cmUsIEV4aXRlZCwgc2hvd19taXNzaW5nX2xldmVscyA9IEZBTFNFKSAlPiUgDQogIGFkb3JuX3RvdGFscygicm93IikgJT4lDQogIGFkb3JuX3BlcmNlbnRhZ2VzKCJhbGwiKSAlPiUNCiAgYWRvcm5fcGN0X2Zvcm1hdHRpbmcoZGlnaXRzID0gMSkgJT4lDQogIGFkb3JuX25zICU+JQ0KICBhZG9ybl90aXRsZQ0KDQpjcmVkaXRvcyAlPiUgDQogIHRhYnlsKEhhc0NyQ2FyZCwgRXhpdGVkLCBzaG93X21pc3NpbmdfbGV2ZWxzID0gRkFMU0UpICU+JSANCiAgYWRvcm5fdG90YWxzKCJyb3ciKSAlPiUNCiAgYWRvcm5fcGVyY2VudGFnZXMoImFsbCIpICU+JQ0KICBhZG9ybl9wY3RfZm9ybWF0dGluZyhkaWdpdHMgPSAxKSAlPiUNCiAgYWRvcm5fbnMgJT4lDQogIGFkb3JuX3RpdGxlDQoNCmBgYA0KDQpgYGB7cn0NCnQgPC0gY3JlZGl0b3MgJT4lIA0KICBzZWxlY3RfaWYoaXMuZmFjdG9yKQ0KDQpjYXRlZ29yaWFzIDwtIHQgJT4lIA0KICBkcGx5cjo6c2VsZWN0KC1FeGl0ZWQpICU+JQ0KICBuYW1lcygpDQoNCmNoaSA8LSB2ZWN0b3IoKQ0KcC52YWx1ZSA8LSB2ZWN0b3IoKQ0KDQpmb3IgKGNvbHVtbmEgaW4gY2F0ZWdvcmlhcyl7DQogIA0KICB0ZXN0IDwtIHN0YXRzOjpjaGlzcS50ZXN0KHRbY29sdW1uYV0sIHQkRXhpdGVkKQ0KICANCiAgY2hpIDwtIGFwcGVuZChjaGksIHRlc3RbWyJzdGF0aXN0aWMiXV1bWyJYLXNxdWFyZWQiXV0pDQogIHAudmFsdWUgPC0gYXBwZW5kKHAudmFsdWUsIHRlc3RbWyJwLnZhbHVlIl1dKQ0KICANCiAgfQ0KDQpkZi5jaGkgPC0gZGF0YS5mcmFtZShjYXRlZ29yaWFzLCBjaGksIHAudmFsdWUpICU+JSANCiAgYXJyYW5nZShkZXNjKC1wLnZhbHVlKSkNCg0KZGYuY2hpICU+JSANCiAga2FibGUoY2FwdGlvbiA9ICJQcnVlYmEgQ2hpLWN1YWRyYWRhIiwgYWxpZ24gPSAiYyIpICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gYygic3RyaXBlZCIsICJob3ZlciIpKQ0KDQpgYGANCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpMYXMgdmFyaWFibGVzIFRlbnVyZSB5IEhhc0NyQ2FyZCB0aWVuZW4gdW4gY2hpLWN1YWRyYWRvIHBlcXVlw7FvIHkgdW4gcC12YWxvciBtYXlvciBxdWUgMC4wNSAobml2ZWwgZGUgc2lnbmlmaWNhbmNpYSBlc3TDoW5kYXIpLCBsbyBxdWUgY29uZmlybWEgbGEgY29uY2x1c2nDs24gYW50ZXJpb3IgZGUgcXVlIGVzdGFzIGRvcyBjYXJhY3RlcsOtc3RpY2FzIG5vIHRyYW5zbWl0ZW4gbmluZ3VuYSBpbmZvcm1hY2nDs24gw7p0aWwuIFBvciBsbyB0YW50bywgc2UgcmVjb21pZW5kYSBlbGltaW5hcmxhcyBkZWwgYW7DoWxpc2lzLCBzaW4gZW1iYXJnbywgcGFyYSBwb2RlciB0b21hciBlc2EgZGVjaXNpw7NuIGNvbiBtYXlvciBjZXJ0ZXphLCBzZSBpZGVudGlmaWNhcsOhIGxhIGltcG9ydGFuY2lhIHF1ZSB0aWVuZSBjYWRhIHZhcmlhYmxlLg0KPC9kaXY+DQoNCkltcG9ydGFuY2lhIGRlIGxhcyB2YXJpYWJsZXMNCg0KYGBge3J9DQpjcmVkaXRvcy5yZiA8LSBtYXBfaWYoLnggPSBjcmVkaXRvcywgLnAgPSBpcy5jaGFyYWN0ZXIsIC5mID0gYXMuZmFjdG9yKSAlPiUNCiAgYXMuZGF0YS5mcmFtZSgpDQoNCm1vZGVsby5pbXBvcnRhbmNpYSA8LSByYW5kb21Gb3Jlc3QoZm9ybXVsYSA9IEV4aXRlZCB+IC4gLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhID0gbmEub21pdChjcmVkaXRvcyksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG10cnkgPSA1LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW1wb3J0YW5jZSA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG50cmVlID0gMTAwMCkNCg0KaW1wb3J0YW5jaWEgPC0gYXMuZGF0YS5mcmFtZShtb2RlbG8uaW1wb3J0YW5jaWEkaW1wb3J0YW5jZSkNCmltcG9ydGFuY2lhIDwtIHJvd25hbWVzX3RvX2NvbHVtbihpbXBvcnRhbmNpYSwgdmFyID0gInZhcmlhYmxlIikNCg0KZzkgPC0gZ2dwbG90KGRhdGEgPSBpbXBvcnRhbmNpYSwgDQogICAgICAgICAgICAgYWVzKHggPSByZW9yZGVyKHZhcmlhYmxlLCBNZWFuRGVjcmVhc2VBY2N1cmFjeSksIA0KICAgICAgICAgICAgICAgICB5ID0gTWVhbkRlY3JlYXNlQWNjdXJhY3ksIGZpbGwgPSBNZWFuRGVjcmVhc2VBY2N1cmFjeSkpICsgDQogIGxhYnMoeCA9ICJ2YXJpYWJsZSIsDQogICAgICAgdGl0bGUgPSAiUmVkdWNjacOzbiBkZSBBY2N1cmFjeSIpICsNCiAgZ2VvbV9jb2woKSArIA0KICBjb29yZF9mbGlwKCkgKw0KICB0aGVtZV9idygpICsNCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gImJvdHRvbSIpDQoNCmcxMCA8LSBnZ3Bsb3QoZGF0YSA9IGltcG9ydGFuY2lhLA0KICAgICAgICAgICAgICBhZXMoeCA9IHJlb3JkZXIodmFyaWFibGUsIE1lYW5EZWNyZWFzZUdpbmkpLA0KICAgICAgICAgICAgICAgICAgeSA9IE1lYW5EZWNyZWFzZUdpbmksIGZpbGwgPSBNZWFuRGVjcmVhc2VHaW5pKSkgKw0KICBsYWJzKHggPSAidmFyaWFibGUiLCB0aXRsZSA9ICJSZWR1Y2Npw7NuIGRlIHB1cmV6YSAoR2luaSkiKSArDQogIGdlb21fY29sKCkgKw0KICBjb29yZF9mbGlwKCkgKw0KICB0aGVtZV9idygpICsNCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gImJvdHRvbSIpDQoNCmdnYXJyYW5nZShnOSwgZzEwKQ0KDQpgYGANCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpFbiBhbWJhcyBncsOhZmljYXMsIGxhIHZhcmlhYmxlIEhhc0NyY2FyZCB0aWVuZSBtdXkgcG9jYSBpbmZsdWVuY2lhIHNvYnJlIGxhIHZhcmlhYmxlIG9iamV0aXZvLiBMYSB2YXJpYWJsZSBUZW51cmUgcGFyZWNlIHF1ZSBwdWVkZSBhcG9ydGFyIGFsZ28gYWwgbW9kZWxvLCBzaW4gZW1iYXJnbywgY29tbyBzZSB2aW8sIGFudGVyaW9ybWVudGUsIGRpY2hhIHZhcmlhYmxlIHByZXNlbnRhIHVuYSBkaXN0cmlidWNpw7NuIHVuaWZvcm1lLCBwb3IgbG8gcXVlLCBubyBzZXLDoSB1biBidWVuIHByZWRpY3Rvci4gTG8gbWlzbW8gcGFzYSBjb24gbGEgdmFyaWFibGUgRXN0aW1hdGVkU2FsYXJ5Lg0KPC9kaXY+DQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0V9DQpjcmVkaXRvcyAlPiUgDQogIGdncGxvdChhZXMoeCA9IFRlbnVyZSwgeSA9IC4uY291bnQuLiwgZmlsbCA9IEV4aXRlZCkpICsNCiAgZ2VvbV9iYXIoKSArDQogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IGMoInN0ZWVsYmx1ZSIsICJncmF5NTAiKSkgKw0KICBsYWJzKHRpdGxlID0gIlRlbnVyZSIpDQoNCmNyZWRpdG9zICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gRXN0aW1hdGVkU2FsYXJ5LCB5ID0gLi5jb3VudC4uLCBmaWxsID0gRXhpdGVkKSkgKw0KICBnZW9tX2hpc3RvZ3JhbSgpICsNCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gYygic3RlZWxibHVlIiwgImdyYXk1MCIpKSArDQogIGxhYnModGl0bGUgPSAiRXN0aW1hdGVkU2FsYXJ5IikNCg0KYGBgDQoNCkVuIGxhcyBncsOhZmljYXMsIGFtYmFzIHZhcmlhYmxlcyBubyBtdWVzdHJhbiBzZXIgZGlmZXJlbmNpYWRvcmFzIG8gaW5mbHV5ZW50ZXMgcGFyYSBkZXRlcm1pbmFyIHNpIHVuIGNsaWVudGUgc2UgcXVlZGEgbyBhYmFuZG9uYSBlbCBiYW5jbywgYWRlbcOhcywgZXN0byB5YSBzZSBoYWLDrWEgc29zcGVjaGFkbyBhbCB2ZXIgbG9zIGJveHBsb3RzLiBQb3IgdGFudG8gc2UgZWxpbWluYW4uDQoNCmBgYHtyfQ0KY3JlZGl0b3MgPC0gY3JlZGl0b3MgJT4lIA0KICBkcGx5cjo6c2VsZWN0KC1jKFRlbnVyZSwgSGFzQ3JDYXJkLCBFc3RpbWF0ZWRTYWxhcnkpKQ0KDQpgYGANCg0KPGJyPg0KDQojIyAgPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigwLCAwLCAyMDUpIj5Db2RpZmljYWNpw7NuIGRlIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXM8L3NwYW4+DQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KTGEgbWF5b3LDrWEgZGUgbG9zIGFsZ29yaXRtb3MgZGUgbWFjaGluZSBsZWFybmluZywgZ2VuZXJhbG1lbnRlLCByZXF1aWVyZW4gcXVlIHRvZGFzIGxhcyBjYXJhY3RlcsOtc3RpY2FzIGRlIGVudHJhZGEgKHkgc2FsaWRhKSBzZWFuIG51bcOpcmljYXMuIFBvciBsbyBxdWUsIGxhcyB2YXJpYWJsZXMgY2F0ZWfDs3JpY2FzIGRlYmVuIGNvbnZlcnRpcnNlIChjb2RpZmljYXJzZSkgZW4gbsO6bWVyb3MgYW50ZXMgZGUgY29uc3RydWlyIGxvcyBtb2RlbG9zLg0KDQpIYXN0YSBlbCBhcXXDrSwgZWwgY29uanVudG8gZGUgZGF0b3MgY29udGllbmUgZG9zIGNhcmFjdGVyw61zdGljYXMgcXVlIHJlcXVpZXJlbiBjb2RpZmljYWNpw7NuOiBHZW5kZXIgeSBHZW9ncmFwaHkuDQoNCiogVmFtb3MgYSByZWNvZGlmaWNhcjogbWFzY3VsaW5vID0gMSB5IEZlbWVuaW5vID0gMA0KKiBQYXJhIGxhIHZhcmlhYmxlIEdlb2dyYXBoeSwgc2UgY3JlYXLDoW4gc29sbyBkb3MgZ3J1cG9zLCBBbGVtYW5pYSA9IDEgeSBlbCByZXN0byBlbiBsYSBjYXRlZ29yw61hIG90cm9zID0gMCwgeWEgcXVlIGVuIEZyYW5jaWEgeSBFc3Bhw7FhIGxhIHRhc2EgZGUgYWJhbmRvbm8gZXMgY2FzaSBsYSBtaXNtYSB5IGVuIEFsZW1hbmlhIGVzIGJhc3RhbnRlIGJhamEsIHBvciBsbyBxdWUsIGNyZWFuZG8gZXN0b3MgZG9zIGdydXBvcyBzZSBwb2Ryw6EgZGlmZXJlbmNpYXIgbWVqb3IgZXN0YSB2YXJpYWJsZSB5IGxlIHZlbmRyw6EgYmllbiBhIGxvcyBhbGdvcml0bW9zLg0KPC9kaXY+DQoNCmBgYHtyfQ0KY3JlZGl0b3MkR2VuZGVyIDwtIGlmZWxzZShjcmVkaXRvcyRHZW5kZXIgPT0gJ0ZlbWFsZScsIDAsIDEpDQpjcmVkaXRvcyRHZW5kZXIgPC0gYXMuZmFjdG9yKGNyZWRpdG9zJEdlbmRlcikNCg0KY3JlZGl0b3MkR2VvZ3JhcGh5IDwtIGlmZWxzZShjcmVkaXRvcyRHZW9ncmFwaHkgPT0gJ0ZyYW5jZScgfCBjcmVkaXRvcyRHZW9ncmFwaHkgPT0nU3BhaW4nLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAwLCAxKQ0KDQpjcmVkaXRvcyRHZW9ncmFwaHkgPC0gYXMuZmFjdG9yKGNyZWRpdG9zJEdlb2dyYXBoeSkNCg0KY3JlZGl0b3MgPC0gY3JlZGl0b3MgJT4lIA0KICBtdXRhdGUoTnVtT2ZQcm9kdWN0cyA9IGZjdF9jb2xsYXBzZShOdW1PZlByb2R1Y3RzLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnMScgPSAnMScsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICcyJyA9ICcyJywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb3RoZXJfbGV2ZWwgPSAnPjInKSkNCg0KYGBgDQoNCjxicj4NCg0KIyMgIDxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMCwgMCwgMjA1KSI+RXNjYWxhbWllbnRvPC9zcGFuPg0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkVsIGVzY2FsYWRvIGRlIGNhcmFjdGVyw61zdGljYXMgZXMgdW5hIHTDqWNuaWNhIHF1ZSBzZSB1dGlsaXphIHBhcmEgbm9ybWFsaXphciBlbCByYW5nbyBkZSB2YXJpYWJsZXMgZW4gdW4gY29uanVudG8gZGUgZGF0b3MgY3VhbmRvIMOpc3RhcyB0aWVuZW4gZGlmZXJlbnRlcyBtYWduaXR1ZGVzLCBwb3IgZWplbXBsbywgbm8gZXMgbGEgbWlzbWEgZGlmZXJlbmNpYSBlbnRyZSBlbCByYW5nbyBkZSBsYSBlZGFkIHkgZWwgcmFuZ28gZGVsIEJhbGFuY2UuIFBhcmEgcXVlIGxhcyB2YXJpYWJsZXMgY29uIG1hZ25pdHVkZXMgZ3JhbmRlcyBubyBkb21pbmVuIGVsIHJlc3RvIGRlIGxvcyBkYXRvcyBzZSBkZWJlbiBlc2NhbGFyIHRvZGFzIGxhcyB2YXJpYWJsZXMuIEFsZ3Vub3MgYWxnb3JpdG1vcyBzb24gc2Vuc2libGVzIGEgbGEgZXNjYWxhIGRlIGNhcmFjdGVyw61zdGljYXMgKHBvciBlamVtcGxvLCBTVk0pLCBtaWVudHJhcyBxdWUgb3Ryb3Mgc29uIGludmFyaWFudGVzIChwb3IgZWplbXBsbywgYm9zcXVlcyBhbGVhdG9yaW9zKS4NCjwvZGl2Pg0KDQpgYGB7cn0NCnMgPC0gYygiQWdlIiwgIkJhbGFuY2UiLCAiQ3JlZGl0U2NvcmUiKQ0KDQpjcmVkaXRvc1tzXSA8LSBzY2FsZShjcmVkaXRvc1tzXSkNCg0KYGBgDQoNCjxicj4NCg0KIyMgIDxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMCwgMCwgMjA1KSI+QmFsYW5jZW8gZGUgY2xhc2VzPC9zcGFuPg0KDQpDb21vIGhlbW9zIHZpc3RvIGFudGVyaW9ybWVudGUsIGV4aXN0ZSB1biBkZXNiYWxhbmNlbyBlbiBsYXMgY2xhc2VzIGRlIGxhIHZhcmlhYmxlIG9iamV0aXZvLCBjb24gdW5hIGNsYXNlICgwID0gbm8gYWJhbmRvbm8pIG11Y2hvIG3DoXMgcHJldmFsZW50ZSBxdWUgbGEgb3RyYSAoMSA9IGFiYW5kb25vKToNCg0KYGBge3J9DQp0YWJsZShjcmVkaXRvcyRFeGl0ZWQpDQpgYGANCg0KQW50ZXMgZGUgc2VndWlyIGFkZWxhbnRlLCBwb3IgY29udmVuaWVuY2lhLCB2YW1vcyBhIHJlY29kaWZpY2FyIGxhIHZhcmlhYmxlIEV4aXRlZCBwYXJhIHF1ZSBsYXMgY2xhc2VzIHF1ZWRlbiBkZSBsYSBzaWd1aWVudGUgbWFuZXJhOg0KDQoqIGNsYXNlIHBvc2l0aXZhOiAqKmNodXJuIChhYmFuZG9uYSkqKg0KKiBjbGFzZSBuZWdhdGl2YTogKipzdGF5IChwZXJtYW5lY2UpKioNCg0KYGBge3J9DQpjcmVkaXRvcyRFeGl0ZWQgPC0gaWZlbHNlKGNyZWRpdG9zJEV4aXRlZCA9PSAnMScsICdjaHVybicsICdzdGF5JykNCmNyZWRpdG9zJEV4aXRlZCA8LSBhcy5mYWN0b3IoY3JlZGl0b3MkRXhpdGVkKQ0KDQp0YWJsZShjcmVkaXRvcyRFeGl0ZWQpDQoNCmBgYA0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkVsIGRlc2JhbGFuY2VvIGRlIGNsYXNlcyBzdWVsZSBzZXIgdW4gcHJvYmxlbWEgZnJlY3VlbnRlIHNvYnJlIHRvZG8gZW4gbW9kZWxvcyBkZSBhYmFuZG9ubyBkZSBjbGllbnRlcyBvIGVuIHRlbWFzIGRlIHJpZXNnbyBvIGZyYXVkZXMuIExhIGNsYXNpZmljYWNpw7NuIHF1ZSB1dGlsaXphIGRhdG9zIGRlc2VxdWlsaWJyYWRvcyBlc3TDoSBzZXNnYWRhIGEgZmF2b3IgZGUgbGEgY2xhc2UgbWF5b3JpdGFyaWEsIGxvIHF1ZSBzaWduaWZpY2EgcXVlIGxvcyBhbGdvcml0bW9zIGRlIG1hY2hpbmUgbGVhcm5pbmcgcHJvYmFibGVtZW50ZSBkYXLDoW4gY29tbyByZXN1bHRhZG8gbW9kZWxvcyBxdWUgaGFjZW4gcG9jbyBtw6FzIHF1ZSBwcmVkZWNpciBsYSBjbGFzZSBtw6FzIGNvbcO6bi4gQWRlbcOhcywgbGFzIG3DqXRyaWNhcyBjb211bmVzIHB1ZWRlbiBzZXIgZW5nYcOxb3NhcyBjdWFuZG8gc2UgbWFuZWphbiBkYXRvcyBjb24gZGVzZXF1aWxpYnJpbyBkZSBjbGFzZXMgKHBvciBlamVtcGxvLCBzaSB1biBjb25qdW50byBkZSBkYXRvcyBjb250aWVuZSA5OSw5JSBkZSBsYSBjbGFzZSAiMCIgeSAwLDAxJSBkZSBsYSBjbGFzZSAiMSIsIHVuIGNsYXNpZmljYWRvciBxdWUgc2llbXByZSBwcmVkaWNlICIwIiB0ZW5kcsOhIHVuYSBwcmVjaXNpw7NuIGRlbCA5OSw5JSkuIEFkZW3DoXMsIGRhbiBsdWdhciBhIHByZWRpY2Npb25lcyBzZXNnYWRhcy4NCg0KRXhpc3RlbiBhbGd1bmFzIGVzdHJhdGVnaWFzIHF1ZSBwdWVkZW4gYWJvcmRhciBlc3RlIHByb2JsZW1hIGNvbW8sIHBvciBlamVtcGxvLCBlbCBzdWJtdWVzdHJlbywgIHNvYnJlbXVlc3RyZW8sIGdlbmVyYWNpw7NuIGRlIGRhdG9zIHNpbnTDqXRpY29zLCBldGMuIFNpbiBlbWJhcmdvLCBhbnRlcyBkZSBjb250aW51YXIgZGl2aWRpcmVtb3MgZWwgY29uanVudG8gZGUgZGF0b3MgZW4gdW5hIHBhcnRlIGRlIGVudHJlbmFtaWVudG8sIHZhbGlkYWNpw7NuICB5IHBydWViYSwgcGFyYSBldml0YXIgY2FlciBlbiB1biBzb2JyZWFqdXN0ZSBlbiBlbCBkZXNlbXBlw7FvIGRlIGxvcyBtb2RlbG9zLiBMYXMgZGl2aXNpb25lcyBzZXLDoW46IDcwJS0xNSUtMTUlLCByZXNwZWN0aXZhbWVudGUuDQoNCioqRW50cmVuYW1pZW50bzoqKg0KDQpFcyBlbCBjb25qdW50byBkZSBkYXRvcyBxdWUgc2UgdXRpbGl6YSBwYXJhIGVudHJlbmFyIHkgaGFjZXIgcXVlIGVsIG1vZGVsbyBhcHJlbmRhIGxhcyBjYXJhY3RlcsOtc3RpY2FzIC8gcGF0cm9uZXMgb2N1bHRvcyBlbiBsb3MgZGF0b3MuDQoNCioqVmFsaWRhY2nDs246KioNCg0KRWwgY29uanVudG8gZGUgdmFsaWRhY2nDs24gZXMgdW4gY29uanVudG8gZGUgZGF0b3Mgc2VwYXJhZG8gZGVsIGNvbmp1bnRvIGRlIGVudHJlbmFtaWVudG8sIHF1ZSBzZSB1dGlsaXphIHBhcmEgdmFsaWRhciBlbCByZW5kaW1pZW50byBkZSBudWVzdHJvIG1vZGVsbyBkdXJhbnRlIGVsIGVudHJlbmFtaWVudG8uDQoNCioqUHJ1ZWJhOioqDQoNCkVsIGNvbmp1bnRvIGRlIHBydWViYSBlcyB1biBjb25qdW50byBzZXBhcmFkbyBkZSBkYXRvcyB1dGlsaXphZG9zIHBhcmEgcHJvYmFyIGVsIG1vZGVsbyBkZXNwdcOpcyBkZSBjb21wbGV0YXIgZWwgZW50cmVuYW1pZW50by4gU29uIG9ic2VydmFjaW9uZXMgcXVlIGVsIGFsZ29yaXRtbyBubyBoYSB2aXN0by4NCjwvZGl2Pg0KDQpgYGB7cn0NCnNldC5zZWVkKDEyMykNCg0KZGl2MSA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKHkgPSBjcmVkaXRvcyRFeGl0ZWQsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHAgPSAwLjcsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsaXN0ID0gRkFMU0UsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRpbWVzID0gMSkNCg0KY3JlZGl0b3MudHJhaW4gPC0gY3JlZGl0b3NbZGl2MSxdICMgZW50cmVuYW1pZW50bw0KY3JlZGl0b3MyIDwtIGNyZWRpdG9zWy1kaXYxLF0NCg0KDQpkaXYyIDwtIGNyZWF0ZURhdGFQYXJ0aXRpb24oeSA9IGNyZWRpdG9zMiRFeGl0ZWQsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHAgPSAwLjUsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsaXN0ID0gRkFMU0UsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRpbWVzID0gMSkNCg0KDQpjcmVkaXRvcy52YWxpZGFjaW9uIDwtIGNyZWRpdG9zMltkaXYyLF0gIyB2YWxpZGFjaW9uDQpjcmVkaXRvcy50ZXN0IDwtIGNyZWRpdG9zMlstZGl2MixdICMgcHJ1ZWJhDQoNCmBgYA0KDQpTb2xvIHZlcmlmaWNhbW9zIGxhIGRpc3RyaWJ1Y2nDs24gb3JpZ2luYWwgZGUgbGFzIGNsYXNlcyBlbiBsb3MgdHJlcyBjb25qdW50b3MgZGUgZGF0b3M6DQoNCmBgYHtyfQ0KIyB0cmFpbg0KcHJvcC50YWJsZSh0YWJsZShjcmVkaXRvcy50cmFpbiRFeGl0ZWQpKQ0KbnJvdyhjcmVkaXRvcy50cmFpbikNCg0KIyB2YWxpZGFjaW9uDQpwcm9wLnRhYmxlKHRhYmxlKGNyZWRpdG9zLnZhbGlkYWNpb24kRXhpdGVkKSkNCm5yb3coY3JlZGl0b3MudmFsaWRhY2lvbikNCg0KIyB0ZXN0DQpwcm9wLnRhYmxlKHRhYmxlKGNyZWRpdG9zLnRlc3QkRXhpdGVkKSkNCm5yb3coY3JlZGl0b3MudGVzdCkNCg0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KU2UgdXRpbGl6YXLDoSBsYSB0w6ljbmljYSBkZSBzb2JyZW11ZXN0cmVvICoqU01PVEUgKFN5bnRoZXRpYyBNaW5vcml0eSBPdmVyc2FtcGxpbmcgVGVjaG5pcXVlKSoqLCBxdWUgY29uc2lzdGUgZW4gZW5jb250cmFyIHVuIHJlZ2lzdHJvIHF1ZSBlcyBzaW1pbGFyIGFsIHJlZ2lzdHJvIHF1ZSBzZSBlc3TDoSBtdWVzdHJlYW5kbyB5IGNyZWEgdW4gcmVnaXN0cm8gc2ludMOpdGljbyBxdWUgZXMgdW4gcHJvbWVkaW8gcG9uZGVyYWRvIGFsZWF0b3JpYW1lbnRlIGRlbCByZWdpc3RybyBvcmlnaW5hbCB5IGVsIHJlZ2lzdHJvIHZlY2lubywgZG9uZGUgZWwgcGVzbyBzZSBnZW5lcmEgcG9yIHNlcGFyYWRvIHBhcmEgY2FkYSBwcmVkaWN0b3IuIFBhcmEgZ2VuZXJhciBkYXRvcyBhcnRpZmljaWFsZXMsIHV0aWxpemEgYm9vdHN0cmFwcGluZyB5IGstdmVjaW5vcyBtw6FzIGNlcmNhbm9zLg0KPC9kaXY+DQoNCmBgYHtyfQ0Kc2V0LnNlZWQoMTIzKQ0KDQpjcmVkaXRvcy5iYWwgPC0gUk9TRShFeGl0ZWQgfiAuLCANCiAgICAgICAgICAgICAgICAgIGRhdGEgPSBjcmVkaXRvcy50cmFpbiwNCiAgICAgICAgICAgICAgICAgIHNlZWQgPSAxKSRkYXRhDQoNCnRhYmxlKGNyZWRpdG9zLmJhbCRFeGl0ZWQpDQpwcm9wLnRhYmxlKHRhYmxlKGNyZWRpdG9zLmJhbCRFeGl0ZWQpKQ0KDQpgYGANCg0KPGJyPg0KDQojICA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPk3DqXRyaWNhIGRlIGV2YWx1YWNpw7NuPC9zcGFuPg0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkVzIGluZGlzcGVuc2FibGUgZGVmaW5pciwgZW4gbG9zIG1vZGVsb3MgZGUgYXByZW5kaXphamUgc3VwZXJ2aXNhZG8sIHVuYSBtw6l0cmljYSBxdWUgc2lydmEgcGFyYSBldmFsdWFyIGxhIGNhcGFjaWRhZCBwcmVkaWN0aXZhIGRlbCBtb2RlbG8gZmluYWwsIGFkZW1hcywgc2Vydmlyw6EgcGFyYSBpciBjb21wYXJhbmRvIGxvcyBtb2RlbG9zIGdlbmVyYWRvcyBhc8OtIGNvbW8gc3UgY2FsaWJyYWNpw7NuLg0KDQpDb21vIGVzdGUgcHJveWVjdG8gYWJvcmRhIHVuIHByb2JsZW1hIGRlIGNsYXNpZmljYWNpw7NuLCB5YSBxdWUgbGEgdmFyaWFibGUgYSBwcmVkZWNpciBlcyBjYXRlZ8OzcmljYSwgeSBsbyBxdWUgc2UgcHJldGVuZGUgZXMgcXVlIGVsIG1vZGVsbyBjbGFzaWZpcXVlIGNvcnJlY3RhbWVudGUgbGFzIG9ic2VydmFjaW9uZXMgZGUgbGEgY2xhc2UgcG9zaXRpdmEsIGVzIGRlY2lyLCBsb3MgY2xpZW50ZXMgcXVlIGFiYW5kb25hbiwgcG9yIHNlciBtw6FzIGNyw610aWNvIHBhcmEgZWwgYmFuY28sIGxhIG3DqXRyaWNhIGRlIGV2YWx1YWNpw7NuIGVsZWdpZGEgc2Vyw6EgbGEgKipTZW5zaWJpbGlkYWQgbyBSZWNhbGwqKiwgcG9ycXVlIGVzdGEgbcOpdHJpY2EgcmVwcmVzZW50YSBsYSBmcmFjY2nDs24gZGUgdmVyZGFkZXJvcyBwb3NpdGl2b3MsIHBhcmEgdGVuZXJsb3MgYmllbiBpZGVudGlmaWNhZG9zIHkgZGlyaWdpciBjYW1wYcOxYXMgZGUgZmlkZWxpemFjacOzbiBjb24gZWwgb2JqZXRpdm8gZGUgcXVlIG5vIHNlIHZheWFuLiBFbiBvdHJhcyBwYWxhYnJhcywgcXVlcmVtb3MgZXZpdGFyIGNsYXNpZmljYXIgYSB1biBjbGllbnRlIHF1ZSBhYmFuZG9uYSBlbCBiYW5jbyBlbiBsYSBjbGFzZSBkZSBsb3MgcXVlIHBlcm1hbmVjZXJhbi4NCjwvZGl2Pg0KDQpMYSBmw7NybXVsYSBlczoNCg0KKipTZW5zaXRpdml0eSA9IFZQLyhWUCArIEZOKSoqDQoNClZQOiBWZXJkYWRlcm9zIHBvc2l0aXZvcw0KDQpGTjogRmFsc29zIG5lZ2F0aXZvcw0KDQo8YnI+DQoNCiMgPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigwLCAwLCAyMDUpIj5DcmVhY2nDs24gZGUgbW9kZWxvcyBkZSBhcHJlbmRpemFqZSBhdXRvbcOhdGljbzwvc3Bhbj4NCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpTaWVtcHJlIHF1ZSBzZSB2YSBhIGhhY2VyIHVuIG1vZGVsbyBkZSBtYWNoaW5lIGxlYXJuaW5nIHkgZW4gZXNwZWPDrWZpY28gdW4gbW9kZWxvIGRlIGFwcmVuZGl6YWplIHN1cGVydmlzYWRvIGVzIGFsdGFtZW50ZSByZWNvbWVuZGFibGUgaW5pY2lhciBjcmVhbmRvIHVuIG1vZGVsbyBiYXNlIGEgcGFydGlyIGRlbCBjdWFsIHNlIHRyYXRhcsOhIGRlIG1lam9yYXIgbG9zIHJlc3VsdGFkb3MgeWEgc2VhIGNhbGlicmFuZG8gc3VzIGhpcGVycGFyw6FtZXRyb3MgKHNpIGxvcyB0aWVuZSkgbyBjcmVhbmRvIG90cm9zIGFsZ29yaXRtb3MuDQoNClBhcmEgdGVuZXIgdW5hIG1lZGlkYSBkZSByZWZlcmVuY2lhIHkgc29icmUgbGEgY3VhbCBtZWpvcmFyIGVzb3MgcmVzdWx0YWRvcywgc2UgY3JlYXLDoSB1biBtb2RlbG8gcXVlIHNlcsOhOiBOYWl2ZSBCYXllcy4NCjwvZGl2Pg0KDQojIyAgPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigwLCAwLCAyMDUpIj5Nb2RlbG8gYmFzZTogTmFpdmUgQmF5ZXM8L3NwYW4+DQoNCkNvbW8gc2UgdmEgYSB1dGlsaXphciBlbCBwYXF1ZXRlICoqY2FyZXQqKiwgcG9yIGNvbnZlbmllbmNpYSB2YW1vcyBhIGNyZWFyIGVsIG9iamV0byByZWNpcGUsIHBhcmEgcG9kZXIgZWplY3V0YXIgbG9zIGFsZ29yaXRtb3M6DQoNCmBgYHtyfQ0KIyBTZSBjcmVhIHVuIG9iamV0byByZWNpcGUNCm9iamV0by5yZWNpcGUgPC0gcmVjaXBlKGZvcm11bGEgPSBFeGl0ZWQgfiBDcmVkaXRTY29yZSArIEdlb2dyYXBoeSArIEdlbmRlciArIEFnZSArDQogICAgICAgICAgICAgICAgICAgICAgICAgIEJhbGFuY2UgKyBOdW1PZlByb2R1Y3RzICsgSXNBY3RpdmVNZW1iZXIsDQogICAgICAgICAgICAgICAgICAgICAgICBkYXRhID0gY3JlZGl0b3MuYmFsKQ0KDQpvYmpldG8ucmVjaXBlDQoNCmBgYA0KDQpgYGB7cn0NCiMgU2UgZW50cmVuYSBlbCBvYmpldG8gcmVjaXBlDQp0cmFpbmVkLnJlY2lwZSA8LSBwcmVwKG9iamV0by5yZWNpcGUsIHRyYWluaW5nID0gY3JlZGl0b3MuYmFsKQ0KDQp0cmFpbmVkLnJlY2lwZQ0KYGBgDQoNCmBgYHtyfQ0KIyBTZSBwcmVwYXJhbiBsb3Mgb2JqZXRvcw0KY3JlZGl0b3MudHJhaW4ucHJlcCA8LSBiYWtlKHRyYWluZWQucmVjaXBlLCBuZXdfZGF0YSA9IGNyZWRpdG9zLmJhbCkNCmNyZWRpdG9zLnZhbGlkYWNpb24ucHJlcCA8LSBiYWtlKHRyYWluZWQucmVjaXBlLCBuZXdfZGF0YSA9IGNyZWRpdG9zLnZhbGlkYWNpb24pDQpjcmVkaXRvcy50ZXN0LnByZXAgPC0gYmFrZSh0cmFpbmVkLnJlY2lwZSwgbmV3X2RhdGEgPSBjcmVkaXRvcy50ZXN0KQ0KDQpnbGltcHNlKGNyZWRpdG9zLnRyYWluLnByZXApDQpnbGltcHNlKGNyZWRpdG9zLnZhbGlkYWNpb24ucHJlcCkNCmdsaW1wc2UoY3JlZGl0b3MudGVzdC5wcmVwKQ0KDQpgYGANCg0KPGJyPg0KDQo8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDIwNCwgMTUzLCAwKSI+TmFpdmUgQmF5ZXM8L3NwYW4+DQoNCmBgYHtyIGV2YWw9RkFMU0V9DQojIHBhcmFsZWxpemFjaW9uIGRlbCBwcm9jZXNvDQpjb3JlcyA8LSBkZXRlY3RDb3JlcygpLTENCmNsIDwtIG1ha2VDbHVzdGVyKGNvcmVzLCB0eXBlID0gIlNPQ0siKQ0KcmVnaXN0ZXJEb1NOT1coY2wpDQoNCiMgaGlwZXJwYXJhbWV0cm9zLCBuby4gcmVwZXRpY2lvbmVzIHkgc2VtaWxsYSBwYXJhIGNhZGEgcmVwZXRpY2lvbg0KcGFydGljaW9uZXMgPC0gMTANCnJlcGV0aWNpb25lcyA8LSA1DQoNCmhpcGVycGFyYW1ldHJvcyA8LSBkYXRhLmZyYW1lKHVzZWtlcm5lbCA9IEZBTFNFLCBmTCA9IDAgLCBhZGp1c3QgPSAwKQ0KDQpzZXQuc2VlZCgxMjMpDQoNCnNlZWRzIDwtIHZlY3Rvcihtb2RlID0gImxpc3QiLCBsZW5ndGggPSAocGFydGljaW9uZXMgKiByZXBldGljaW9uZXMpICsgMSkgDQoNCmZvciAoaSBpbiAxOihwYXJ0aWNpb25lcyAqIHJlcGV0aWNpb25lcykpIHsNCiAgc2VlZHNbW2ldXSA8LSBzYW1wbGUuaW50KDEwMDAsIG5yb3coaGlwZXJwYXJhbWV0cm9zKSkgDQogIH0gDQoNCnNlZWRzW1socGFydGljaW9uZXMgKiByZXBldGljaW9uZXMpICsgMV1dIDwtIHNhbXBsZS5pbnQoMTAwMCwgMSkNCg0KIyBkZWZpbmljaW9uIGRlbCBlbnRyZW5hbWllbnRvDQpjb250cm9sLnRyYWluIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbnVtYmVyID0gcGFydGljaW9uZXMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cyA9IHJlcGV0aWNpb25lcywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWVkcyA9IHNlZWRzLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJldHVyblJlc2FtcCA9ICJmaW5hbCIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmVyYm9zZUl0ZXIgPSBGQUxTRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsbG93UGFyYWxsZWwgPSBUUlVFKQ0KDQojIGFqdXN0ZSBkZWwgbW9kZWxvDQpzZXQuc2VlZCgzNDIpDQoNCm1vZGVsb19uYiA8LSB0cmFpbihFeGl0ZWQgfiAuLCANCiAgICAgICAgICAgICAgICAgICBkYXRhID0gY3JlZGl0b3MudHJhaW4ucHJlcFstNl0sDQogICAgICAgICAgICAgICAgICAgbWV0aG9kID0gIm5iIiwNCiAgICAgICAgICAgICAgICAgICB0dW5lR3JpZCA9IGhpcGVycGFyYW1ldHJvcywNCiAgICAgICAgICAgICAgICAgICBtZXRyaWMgPSAiQWNjdXJhY3kiLA0KICAgICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGNvbnRyb2wudHJhaW4pDQoNCiMgc2UgZ3VhcmRhIGVsIG1vZGVsbw0Kc2F2ZVJEUyhtb2RlbG9fbmIsIGZpbGUgPSAibW9kZWxvLm5iLnJkcyIpDQoNCmBgYA0KDQpgYGB7cn0NCm1vZGVsb19uYiA8LSByZWFkUkRTKGZpbGUgPSAibW9kZWxvLm5iLnJkcyIpDQpgYGANCg0KRWwgQWNjdXJhY3kgZW4gZWwgY29uanVudG8gZGUgZW50cmVuYW1pZW50byBkZWwgbW9kZWxvIE5haXZlIEJheWVzIGZpbmFsIGZ1ZSBkZTogKipgciBtb2RlbG9fbmJbWyJyZXN1bHRzIl1dW1siQWNjdXJhY3kiXV0qMTAwYCUqKg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0KY20ubmIgPC0gY29uZnVzaW9uTWF0cml4KHByZWRpY3QobW9kZWxvX25iKSwgY3JlZGl0b3MudHJhaW4ucHJlcCRFeGl0ZWQsIHBvc2l0aXZlID0gImNodXJuIikNCg0KZHJhd19jb25mdXNpb25fbWF0cml4KGNtLm5iKQ0KDQpgYGANCg0KTGEgU2Vuc2l0aXZpdHkgZW4gZWwgc2V0IGRlIHRyYWluIGZ1ZSBkZTogKipgciBjbS5uYltbImJ5Q2xhc3MiXV1bWyJTZW5zaXRpdml0eSJdXSoxMDBgJSoqDQoNCkFob3JhIGVzdGFtb3MgbGlzdG9zIHBhcmEgY29tZW56YXIgYSBjb25zdHJ1aXIgbW9kZWxvcyBkZSBhcHJlbmRpemFqZSBhdXRvbcOhdGljby4gTG9zIGFsZ29yaXRtb3MgY2xhc2lmaWNhZG9yZXMgcXVlIHNlIGhhbiBzZWxlY2Npb25hZG8gc29uIGxvcyBzaWd1aWVudGVzOg0KDQoqIExvZ2lzdGljIFJlZ3Jlc3Npb24gKExPR0lUKQ0KKiBTdXBwb3J0IFZlY3RvciBNYWNoaW5lIChTVk0pDQoqIFJhbmRvbSBGb3Jlc3QgKFJGKQ0KKiBFeHRyZW1lIEdyYWRpZW50IEJvb3N0aW5nIChYR0Jvb3N0KQ0KKiBOZXVyb25hbCBOZXR3b3JrIChOTkVUKQ0KKiBHcmFkaWVudCBCb29zdGluZyBNYWNoaW5lIChHQk0pDQoNCjxicj4NCg0KPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigyMDQsIDE1MywgMCkiPlJlZ3Jlc2nDs24gTG9nw61zdGljYTwvc3Bhbj4NCg0KYGBge3IgZXZhbD1GQUxTRX0NCiMgcGFyYWxlbGl6YWNpb24gZGVsIHByb2Nlc28NCmNvcmVzIDwtIGRldGVjdENvcmVzKCktMQ0KY2wgPC0gbWFrZUNsdXN0ZXIoY29yZXMsIHR5cGUgPSAiU09DSyIpDQpyZWdpc3RlckRvU05PVyhjbCkNCg0KIyBoaXBlcnBhcmFtZXRyb3MsIG5vLiByZXBldGljaW9uZXMgeSBzZW1pbGxhIHBhcmEgY2FkYSByZXBldGljaW9uDQpwYXJ0aWNpb25lcyA8LSAxMA0KcmVwZXRpY2lvbmVzIDwtIDUNCg0KaGlwZXJwYXJhbWV0cm9zIDwtIGRhdGEuZnJhbWUocGFyYW1ldGVyID0gIm5vbmUiKQ0KDQpzZXQuc2VlZCgxMjMpDQoNCnNlZWRzIDwtIHZlY3Rvcihtb2RlID0gImxpc3QiLCBsZW5ndGggPSAocGFydGljaW9uZXMgKiByZXBldGljaW9uZXMpICsgMSkgDQoNCmZvciAoaSBpbiAxOihwYXJ0aWNpb25lcyAqIHJlcGV0aWNpb25lcykpIHsNCiAgc2VlZHNbW2ldXSA8LSBzYW1wbGUuaW50KDEwMDAsIG5yb3coaGlwZXJwYXJhbWV0cm9zKSkgDQogIH0gDQoNCnNlZWRzW1socGFydGljaW9uZXMgKiByZXBldGljaW9uZXMpICsgMV1dIDwtIHNhbXBsZS5pbnQoMTAwMCwgMSkNCg0KIyBkZWZpbmljaW9uIGRlbCBlbnRyZW5hbWllbnRvDQpjb250cm9sLnRyYWluIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbnVtYmVyID0gcGFydGljaW9uZXMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cyA9IHJlcGV0aWNpb25lcywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWVkcyA9IHNlZWRzLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJldHVyblJlc2FtcCA9ICJmaW5hbCIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmVyYm9zZUl0ZXIgPSBGQUxTRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsbG93UGFyYWxsZWwgPSBUUlVFKQ0KDQojIGFqdXN0ZSBkZWwgbW9kZWxvDQpzZXQuc2VlZCgzNDIpDQoNCm1vZGVsb19sb2dpc3RpYyA8LSB0cmFpbihFeGl0ZWQgfiAuLCANCiAgICAgICAgICAgICAgICAgICBkYXRhID0gY3JlZGl0b3MudHJhaW4ucHJlcCwNCiAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAiZ2xtIiwNCiAgICAgICAgICAgICAgICAgICB0dW5lR3JpZCA9IGhpcGVycGFyYW1ldHJvcywNCiAgICAgICAgICAgICAgICAgICBtZXRyaWMgPSAiQWNjdXJhY3kiLA0KICAgICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGNvbnRyb2wudHJhaW4sDQogICAgICAgICAgICAgICAgICAgZmFtaWx5ID0gImJpbm9taWFsIikNCg0KIyBzZSBndWFyZGEgZWwgbW9kZWxvDQpzYXZlUkRTKG1vZGVsb19sb2dpc3RpYywgZmlsZSA9ICJtb2RlbG8ubG9naXQucmRzIikNCmBgYA0KDQpgYGB7cn0NCm1vZGVsb19sb2dpc3RpYyA8LSByZWFkUkRTKGZpbGUgPSAibW9kZWxvLmxvZ2l0LnJkcyIpDQoNCmBgYA0KDQpFbCBBY2N1cmFjeSBlbiBlbCBjb25qdW50byBkZSBlbnRyZW5hbWllbnRvIGRlbCBtb2RlbG8gaGVjaG8gY29uIGxhIFJlZ3Jlc2nDs24gTG9nw61zdGljYSBmdWUgZGU6ICoqYHIgbW9kZWxvX2xvZ2lzdGljW1sicmVzdWx0cyJdXVtbIkFjY3VyYWN5Il1dKjEwMGAlKioNCg0KYGBge3J9DQpjbS5sZyA8LSBjb25mdXNpb25NYXRyaXgocHJlZGljdChtb2RlbG9fbG9naXN0aWMpLCBjcmVkaXRvcy50cmFpbi5wcmVwJEV4aXRlZCwgcG9zaXRpdmUgPSAiY2h1cm4iKQ0KDQpkcmF3X2NvbmZ1c2lvbl9tYXRyaXgoY20ubGcpDQoNCmBgYA0KDQpMYSBTZW5zaXRpdml0eSBlbiBlbCBzZXQgZGUgdHJhaW4gZnVlIGRlOiAqKmByIGNtLmxnW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dKjEwMGAlKioNCg0KPGJyPg0KDQo8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDIwNCwgMTUzLCAwKSI+TcOhcXVpbmFzIGRlIFZlY3RvciBTb3BvcnRlPC9zcGFuPg0KDQpgYGB7ciBldmFsPUZBTFNFfQ0KIyBwYXJhbGVsaXphY2lvbiBkZWwgcHJvY2Vzbw0KY29yZXMgPC0gZGV0ZWN0Q29yZXMoKS0xDQpjbCA8LSBtYWtlQ2x1c3Rlcihjb3JlcywgdHlwZSA9ICJTT0NLIikNCnJlZ2lzdGVyRG9TTk9XKGNsKQ0KDQojIGhpcGVycGFyYW1ldHJvcywgbm8uIHJlcGV0aWNpb25lcyB5IHNlbWlsbGEgcGFyYSBjYWRhIHJlcGV0aWNpb24NCnBhcnRpY2lvbmVzIDwtIDEwDQpyZXBldGljaW9uZXMgPC0gNQ0KDQpoaXBlcnBhcmFtZXRyb3MgPC0gZXhwYW5kLmdyaWQoc2lnbWEgPSBjKDAuMDAxLCAwLjAxLCAwLjEsIDAuNSwgMSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgQyA9IGMoMSAsIDIwLCA1MCwgMTAwLCAyMDAsIDUwMCwgNzAwKSkNCg0Kc2V0LnNlZWQoMTIzKQ0KDQpzZWVkcyA8LSB2ZWN0b3IobW9kZSA9ICJsaXN0IiwgbGVuZ3RoID0gKHBhcnRpY2lvbmVzICogcmVwZXRpY2lvbmVzKSArIDEpIA0KDQpmb3IgKGkgaW4gMToocGFydGljaW9uZXMgKiByZXBldGljaW9uZXMpKSB7DQogIHNlZWRzW1tpXV0gPC0gc2FtcGxlLmludCgxMDAwLCBucm93KGhpcGVycGFyYW1ldHJvcykpIA0KICB9IA0KDQpzZWVkc1tbKHBhcnRpY2lvbmVzICogcmVwZXRpY2lvbmVzKSArIDFdXSA8LSBzYW1wbGUuaW50KDEwMDAsIDEpDQoNCiMgZGVmaW5pY2lvbiBkZWwgZW50cmVuYW1pZW50bw0KY29udHJvbC50cmFpbiA8LSB0cmFpbkNvbnRyb2wobWV0aG9kID0gInJlcGVhdGVkY3YiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlciA9IHBhcnRpY2lvbmVzLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcGVhdHMgPSByZXBldGljaW9uZXMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VlZHMgPSBzZWVkcywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm5SZXNhbXAgPSAiZmluYWwiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZlcmJvc2VJdGVyID0gRkFMU0UsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhbGxvd1BhcmFsbGVsID0gVFJVRSkNCg0KIyBhanVzdGUgZGVsIG1vZGVsbw0Kc2V0LnNlZWQoMzQyKQ0KDQptb2RlbG9fc3ZtIDwtIHRyYWluKEV4aXRlZCB+IC4sIA0KICAgICAgICAgICAgICAgICAgIGRhdGEgPSBjcmVkaXRvcy50cmFpbi5wcmVwLA0KICAgICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJzdm1SYWRpYWwiLA0KICAgICAgICAgICAgICAgICAgIHR1bmVHcmlkID0gaGlwZXJwYXJhbWV0cm9zLA0KICAgICAgICAgICAgICAgICAgIG1ldHJpYyA9ICJBY2N1cmFjeSIsDQogICAgICAgICAgICAgICAgICAgdHJDb250cm9sID0gY29udHJvbC50cmFpbikNCg0KIyBzZSBndWFyZGEgZWwgbW9kZWxvDQpzYXZlUkRTKG1vZGVsb19zdm0sIGZpbGUgPSAibW9kZWxvLnN2bS5yZHMiKQ0KYGBgDQoNCmBgYHtyfQ0KbW9kZWxvX3N2bSA8LSByZWFkUkRTKGZpbGUgPSAibW9kZWxvLnN2bS5yZHMiKQ0KDQpgYGANCg0KRWwgYWNjdXJhY3kgcHJvbWVkaW8gZGVsIG1vZGVsbyBTdXBwb3J0IFZlY3RvciBNYWNoaW5lIGVuIGVsIGNvbmp1bnRvIGRlIGVudHJlbmFtaWVudG8gZnVlIGRlOiAqKmByIG1lYW4obW9kZWxvX3N2bVtbInJlc3VsdHMiXV1bWyJBY2N1cmFjeSJdXSkqMTAwYCUqKg0KDQpgYGB7cn0NCmNtLnN2bSA8LSBjb25mdXNpb25NYXRyaXgocHJlZGljdChtb2RlbG9fc3ZtKSwgY3JlZGl0b3MudHJhaW4ucHJlcCRFeGl0ZWQsIHBvc2l0aXZlID0gImNodXJuIikNCg0KZHJhd19jb25mdXNpb25fbWF0cml4KGNtLnN2bSkNCg0KYGBgDQoNCkxhIFNlbnNpdGl2aXR5IGVuIGVsIHNldCBkZSB0cmFpbiBmdWUgZGU6ICoqYHIgY20uc3ZtW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dKjEwMGAlKioNCg0KYGBge3J9DQojIHJlcHJlc2VudGFjaW9uIGdyw6FmaWNhDQpnZ3Bsb3QobW9kZWxvX3N2bSwgaGlnaGxpZ2h0ID0gVFJVRSkgKyANCiAgbGFicyh0aXRsZSA9ICJFdm9sdWNpw7NuIGRlbCBhY2N1cmFjeSBkZWwgbW9kZWxvIFNWTSBSYWRpYWwiKSArDQogIHRoZW1lX2J3KCkNCg0KYGBgDQoNCjxicj4NCg0KPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigyMDQsIDE1MywgMCkiPlJhbmRvbSBGb3Jlc3Q8L3NwYW4+DQoNCmBgYHtyIGV2YWw9RkFMU0V9DQojIHBhcmFsZWxpemFjaW9uIGRlbCBwcm9jZXNvDQpjb3JlcyA8LSBkZXRlY3RDb3JlcygpLTENCmNsIDwtIG1ha2VDbHVzdGVyKGNvcmVzLCB0eXBlID0gIlNPQ0siKQ0KcmVnaXN0ZXJEb1NOT1coY2wpDQoNCiMgaGlwZXJwYXJhbWV0cm9zLCBuby4gcmVwZXRpY2lvbmVzIHkgc2VtaWxsYSBwYXJhIGNhZGEgcmVwZXRpY2lvbg0KcGFydGljaW9uZXMgPC0gMTANCnJlcGV0aWNpb25lcyA8LSA1DQoNCmhpcGVycGFyYW1ldHJvcyA8LSBleHBhbmQuZ3JpZChtdHJ5ID0gYygzLCA0LCA1LCA3KSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWluLm5vZGUuc2l6ZSA9IGMoMiwgMywgNCwgNSwgMTAsIDE1LCAyMCwgMzApLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNwbGl0cnVsZSA9ICJnaW5pIikNCg0Kc2V0LnNlZWQoMTIzKQ0KDQpzZWVkcyA8LSB2ZWN0b3IobW9kZSA9ICJsaXN0IiwgbGVuZ3RoID0gKHBhcnRpY2lvbmVzICogcmVwZXRpY2lvbmVzKSArIDEpIA0KDQpmb3IgKGkgaW4gMToocGFydGljaW9uZXMgKiByZXBldGljaW9uZXMpKSB7DQogIHNlZWRzW1tpXV0gPC0gc2FtcGxlLmludCgxMDAwLCBucm93KGhpcGVycGFyYW1ldHJvcykpIA0KICB9IA0KDQpzZWVkc1tbKHBhcnRpY2lvbmVzICogcmVwZXRpY2lvbmVzKSArIDFdXSA8LSBzYW1wbGUuaW50KDEwMDAsIDEpDQoNCiMgZGVmaW5pY2lvbiBkZWwgZW50cmVuYW1pZW50bw0KY29udHJvbC50cmFpbiA8LSB0cmFpbkNvbnRyb2wobWV0aG9kID0gInJlcGVhdGVkY3YiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlciA9IHBhcnRpY2lvbmVzLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcGVhdHMgPSByZXBldGljaW9uZXMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VlZHMgPSBzZWVkcywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm5SZXNhbXAgPSAiZmluYWwiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZlcmJvc2VJdGVyID0gRkFMU0UsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhbGxvd1BhcmFsbGVsID0gVFJVRSkNCg0KIyBhanVzdGUgZGVsIG1vZGVsbw0Kc2V0LnNlZWQoMzQyKQ0KDQptb2RlbG9fcmYgPC0gdHJhaW4oRXhpdGVkIH4gLiwgDQogICAgICAgICAgICAgICAgICAgZGF0YSA9IGNyZWRpdG9zLnRyYWluLnByZXAsDQogICAgICAgICAgICAgICAgICAgbWV0aG9kID0gInJhbmdlciIsDQogICAgICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBoaXBlcnBhcmFtZXRyb3MsDQogICAgICAgICAgICAgICAgICAgbWV0cmljID0gIkFjY3VyYWN5IiwNCiAgICAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSBjb250cm9sLnRyYWluLA0KICAgICAgICAgICAgICAgICAgIG51bS50cmVlcyA9IDUwMCkNCg0KIyBzZSBndWFyZGEgZWwgbW9kZWxvDQpzYXZlUkRTKG1vZGVsb19yZiwgZmlsZSA9ICJtb2RlbG8ucmYucmRzIikNCmBgYA0KDQpgYGB7cn0NCm1vZGVsb19yZiA8LSByZWFkUkRTKGZpbGUgPSAibW9kZWxvLnJmLnJkcyIpDQoNCmBgYA0KDQpFbCBBY2N1cmFjeSBwcm9tZWRpbyBkZWwgbW9kZWxvIFJhbmRvbSBGb3Jlc3QgZW4gZWwgY29uanVudG8gZGUgZW50cmVuYW1pZW50byBmdWUgZGU6ICoqYHIgbWVhbihtb2RlbG9fcmZbWyJyZXN1bHRzIl1dW1siQWNjdXJhY3kiXV0pKjEwMGAlKioNCg0KYGBge3J9DQpjbS5yZiA8LSBjb25mdXNpb25NYXRyaXgocHJlZGljdChtb2RlbG9fcmYpLCBjcmVkaXRvcy50cmFpbi5wcmVwJEV4aXRlZCwgcG9zaXRpdmUgPSAiY2h1cm4iKQ0KDQpkcmF3X2NvbmZ1c2lvbl9tYXRyaXgoY20ucmYpDQoNCmBgYA0KDQpMYSBTZW5zaXRpdml0eSBlbiBlbCBzZXQgZGUgdHJhaW4gZnVlIGRlOiAqKmByIGNtLnJmW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dKjEwMGAlKioNCg0KYGBge3J9DQojIHJlcHJlc2VudGFjacOzbiBncsOhZmljYQ0KZ2dwbG90KG1vZGVsb19yZiwgaGlnaGxpZ2h0ID0gVFJVRSkgKyANCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IDE6MzApICsgDQogIGxhYnModGl0bGUgPSAiRXZvbHVjacOzbiBkZWwgYWNjdXJhY3kgZGVsIG1vZGVsbyBSYW5kb20gRm9yZXN0IikgKyANCiAgZ3VpZGVzKGNvbG9yID0gZ3VpZGVfbGVnZW5kKHRpdGxlID0gIm10cnkiKSwgDQogICAgICAgICBzaGFwZSA9IGd1aWRlX2xlZ2VuZCh0aXRsZSA9ICJtdHJ5IikpICsNCiAgdGhlbWVfYncoKQ0KDQpgYGANCg0KPGJyPg0KDQo8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDIwNCwgMTUzLCAwKSI+WEdCb29zdDwvc3Bhbj4NCg0KYGBge3IgZXZhbD1GQUxTRX0NCmNvcmVzIDwtIGRldGVjdENvcmVzKCktMQ0KY2wgPC0gbWFrZUNsdXN0ZXIoY29yZXMsIHR5cGUgPSAiU09DSyIpDQpyZWdpc3RlckRvU05PVyhjbCkNCg0KIyBoaXBlcnBhcmFtZXRyb3MsIG5vLiByZXBldGljaW9uZXMgeSBzZW1pbGxhIHBhcmEgY2FkYSByZXBldGljaW9uDQpwYXJ0aWNpb25lcyA8LSAxMA0KcmVwZXRpY2lvbmVzIDwtIDUNCg0KaGlwZXJwYXJhbWV0cm9zIDwtIGV4cGFuZC5ncmlkKGV0YSA9IGMoMC4wNSwgMC4wNzUsIDAuMSksDQogICAgICAgICAgICAgICAgICAgICAgICAgbnJvdW5kcyA9IGMoNTAsIDc1LCAxMDApLA0KICAgICAgICAgICAgICAgICAgICAgICAgIG1heF9kZXB0aCA9IDY6OCwNCiAgICAgICAgICAgICAgICAgICAgICAgICBtaW5fY2hpbGRfd2VpZ2h0ID0gYygyLjAsIDIuMjUsIDIuNSksDQogICAgICAgICAgICAgICAgICAgICAgICAgY29sc2FtcGxlX2J5dHJlZSA9IGMoMC4zLCAwLjQsIDAuNSksDQogICAgICAgICAgICAgICAgICAgICAgICAgZ2FtbWEgPSAwLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHN1YnNhbXBsZSA9IDEpDQoNCnNldC5zZWVkKDEyMykNCg0Kc2VlZHMgPC0gdmVjdG9yKG1vZGUgPSAibGlzdCIsIGxlbmd0aCA9IChwYXJ0aWNpb25lcyAqIHJlcGV0aWNpb25lcykgKyAxKSANCg0KZm9yIChpIGluIDE6KHBhcnRpY2lvbmVzICogcmVwZXRpY2lvbmVzKSkgew0KICBzZWVkc1tbaV1dIDwtIHNhbXBsZS5pbnQoMTAwMCwgbnJvdyhoaXBlcnBhcmFtZXRyb3MpKSANCiAgfSANCg0Kc2VlZHNbWyhwYXJ0aWNpb25lcyAqIHJlcGV0aWNpb25lcykgKyAxXV0gPC0gc2FtcGxlLmludCgxMDAwLCAxKQ0KDQojIGRlZmluaWNpb24gZGVsIGVudHJlbmFtaWVudG8NCmNvbnRyb2wudHJhaW4gPC0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJyZXBlYXRlZGN2IiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBudW1iZXIgPSBwYXJ0aWNpb25lcywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXBlYXRzID0gcmVwZXRpY2lvbmVzLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlZWRzID0gc2VlZHMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuUmVzYW1wID0gImZpbmFsIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2ZXJib3NlSXRlciA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VhcmNoID0gImdyaWQiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxsb3dQYXJhbGxlbCA9IFRSVUUpDQoNCiMgYWp1c3RlIGRlbCBtb2RlbG8NCnNldC5zZWVkKDM0MikNCg0KbW9kZWxvX2Jvb3N0IDwtIHRyYWluKEV4aXRlZCB+IC4sIA0KICAgICAgICAgICAgICAgICAgICAgIGRhdGEgPSBjcmVkaXRvcy50cmFpbi5wcmVwLCANCiAgICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAieGdiVHJlZSIsIA0KICAgICAgICAgICAgICAgICAgICAgIHR1bmVHcmlkID0gaGlwZXJwYXJhbWV0cm9zLA0KICAgICAgICAgICAgICAgICAgICAgIG1ldHJpYyA9ICJBY2N1cmFjeSIsIA0KICAgICAgICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGNvbnRyb2wudHJhaW4sIA0KICAgICAgICAgICAgICAgICAgICAgIGRpc3RyaWJ1dGlvbiA9ICJhZGFib29zdCIsDQogICAgICAgICAgICAgICAgICAgICAgdmVyYm9zZSA9IEZBTFNFKQ0KDQojIHNlIGd1YXJkYSBlbCBtb2RlbG8NCnNhdmVSRFMobW9kZWxvX2Jvb3N0LCBmaWxlID0gIm1vZGVsby54Z2IucmRzIikNCmBgYA0KDQpgYGB7cn0NCm1vZGVsb194Z2IgPC0gcmVhZFJEUyhmaWxlID0gIm1vZGVsby54Z2IucmRzIikNCmBgYA0KDQpFbCBhY2N1cmFjeSBwcm9tZWRpbyBkZWwgbW9kZWxvIHhnYm9vc3QgZW4gZWwgY29uanVudG8gZGUgZW50cmVuYW1pZW50byBmdWUgZGU6ICoqYHIgbWVhbihtb2RlbG9feGdiW1sicmVzdWx0cyJdXVtbIkFjY3VyYWN5Il1dKSoxMDBgJSoqDQoNCmBgYHtyfQ0KY20ueGdiIDwtIGNvbmZ1c2lvbk1hdHJpeChwcmVkaWN0KG1vZGVsb194Z2IpLCBjcmVkaXRvcy50cmFpbi5wcmVwJEV4aXRlZCwgcG9zaXRpdmUgPSAiY2h1cm4iKQ0KDQpkcmF3X2NvbmZ1c2lvbl9tYXRyaXgoY20ueGdiKQ0KDQpgYGANCg0KTGEgU2Vuc2l0aXZpdHkgZW4gZWwgc2V0IGRlIHRyYWluIGZ1ZSBkZTogKipgciBjbS54Z2JbWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0qMTAwYCUqKg0KDQo8YnI+DQoNCjxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMjA0LCAxNTMsIDApIj5SZWRlcyBOZXVyb25hbGVzIChSTk4pPC9zcGFuPg0KDQpgYGB7ciBldmFsPUZBTFNFfQ0KY29yZXMgPC0gZGV0ZWN0Q29yZXMoKS0xDQpjbCA8LSBtYWtlQ2x1c3Rlcihjb3JlcywgdHlwZSA9ICJTT0NLIikNCnJlZ2lzdGVyRG9TTk9XKGNsKQ0KDQojIGhpcGVycGFyYW1ldHJvcywgbm8uIHJlcGV0aWNpb25lcyB5IHNlbWlsbGEgcGFyYSBjYWRhIHJlcGV0aWNpb24NCnBhcnRpY2lvbmVzIDwtIDEwDQpyZXBldGljaW9uZXMgPC0gNQ0KDQpoaXBlcnBhcmFtZXRyb3MgPC0gZXhwYW5kLmdyaWQoc2l6ZSA9IGMoMTAsIDIwLCA1MCwgODAsIDEwMCwgMTIwKSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGVjYXkgPSBjKDAuMDAwMSwgMC4xLCAwLjUpKQ0KDQpzZXQuc2VlZCgxMjMpDQoNCnNlZWRzIDwtIHZlY3Rvcihtb2RlID0gImxpc3QiLCBsZW5ndGggPSAocGFydGljaW9uZXMgKiByZXBldGljaW9uZXMpICsgMSkgDQoNCmZvciAoaSBpbiAxOihwYXJ0aWNpb25lcyAqIHJlcGV0aWNpb25lcykpIHsNCiAgc2VlZHNbW2ldXSA8LSBzYW1wbGUuaW50KDEwMDAsIG5yb3coaGlwZXJwYXJhbWV0cm9zKSkgDQogIH0gDQoNCnNlZWRzW1socGFydGljaW9uZXMgKiByZXBldGljaW9uZXMpICsgMV1dIDwtIHNhbXBsZS5pbnQoMTAwMCwgMSkNCg0KIyBkZWZpbmljaW9uIGRlbCBlbnRyZW5hbWllbnRvDQpjb250cm9sLnRyYWluIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbnVtYmVyID0gcGFydGljaW9uZXMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cyA9IHJlcGV0aWNpb25lcywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWVkcyA9IHNlZWRzLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJldHVyblJlc2FtcCA9ICJmaW5hbCIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmVyYm9zZUl0ZXIgPSBGQUxTRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsbG93UGFyYWxsZWwgPSBUUlVFKQ0KDQojIGFqdXN0ZSBkZWwgbW9kZWxvDQpzZXQuc2VlZCgzNDIpDQoNCm1vZGVsb19ubmV0IDwtIHRyYWluKEV4aXRlZCB+IC4sIA0KICAgICAgICAgICAgICAgICAgICAgIGRhdGEgPSBjcmVkaXRvcy50cmFpbi5wcmVwLCANCiAgICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAibm5ldCIsIA0KICAgICAgICAgICAgICAgICAgICAgIHR1bmVHcmlkID0gaGlwZXJwYXJhbWV0cm9zLA0KICAgICAgICAgICAgICAgICAgICAgIG1ldHJpYyA9ICJBY2N1cmFjeSIsIA0KICAgICAgICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGNvbnRyb2wudHJhaW4sDQogICAgICAgICAgICAgICAgICAgICAgcmFuZyA9YygtMC43LCAwLjcpLA0KICAgICAgICAgICAgICAgICAgICAgIE1heE5XdHMgPSAyMDAwLA0KICAgICAgICAgICAgICAgICAgICAgIHRyYWNlID0gRkFMU0UpDQoNCiMgc2UgZ3VhcmRhIGVsIG1vZGVsbw0Kc2F2ZVJEUyhtb2RlbG9fbm5ldCwgZmlsZSA9ICJtb2RlbG8ubm5ldC5yZHMiKQ0KYGBgDQoNCmBgYHtyfQ0KbW9kZWxvX25uZXQgPC0gcmVhZFJEUyhmaWxlID0gIm1vZGVsby5ubmV0LnJkcyIpDQoNCmBgYA0KDQpFbCBhY2N1cmFjeSBwcm9tZWRpbyBkZWwgbW9kZWxvIG5uZXQgZW4gZWwgY29uanVudG8gZGUgZW50cmVuYW1pZW50byBmdWUgZGU6ICoqYHIgbWVhbihtb2RlbG9fbm5ldFtbInJlc3VsdHMiXV1bWyJBY2N1cmFjeSJdXSkqMTAwYCUqKg0KDQpgYGB7cn0NCmNtLm5uZXQgPC0gY29uZnVzaW9uTWF0cml4KHByZWRpY3QobW9kZWxvX25uZXQpLCBjcmVkaXRvcy50cmFpbi5wcmVwJEV4aXRlZCwgcG9zaXRpdmUgPSAiY2h1cm4iKQ0KDQpkcmF3X2NvbmZ1c2lvbl9tYXRyaXgoY20ubm5ldCkNCg0KYGBgDQoNCkxhIFNlbnNpdGl2aXR5IGVuIGVsIHNldCBkZSB0cmFpbiBmdWUgZGU6ICoqYHIgY20ubm5ldFtbImJ5Q2xhc3MiXV1bWyJTZW5zaXRpdml0eSJdXSoxMDBgJSoqDQoNCmBgYHtyfQ0KIyByZXByZXNlbnRhY2nDs24gZ3LDoWZpY2ENCmdncGxvdChtb2RlbG9fbm5ldCwgaGlnaGxpZ2h0ID0gVFJVRSkgKyANCiAgbGFicyh0aXRsZSA9ICJFdm9sdWNpw7NuIGRlbCBBY2N1cmFjeSBOTkVUIikgKw0KICB0aGVtZV9idygpDQpgYGANCg0KPGJyPg0KDQo8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDIwNCwgMTUzLCAwKSI+R3JhZGllbnQgQm9vc3RpbmcgTWFjaGluZSAoR0JNKTwvc3Bhbj4NCg0KYGBge3IgZXZhbD1GQUxTRX0NCmNvcmVzIDwtIGRldGVjdENvcmVzKCktMQ0KY2wgPC0gbWFrZUNsdXN0ZXIoY29yZXMsIHR5cGUgPSAiU09DSyIpDQpyZWdpc3RlckRvU05PVyhjbCkNCg0KIyBoaXBlcnBhcmFtZXRyb3MsIG5vLiByZXBldGljaW9uZXMgeSBzZW1pbGxhIHBhcmEgY2FkYSByZXBldGljaW9uDQpwYXJ0aWNpb25lcyA8LSAxMA0KcmVwZXRpY2lvbmVzIDwtIDUNCg0KaGlwZXJwYXJhbWV0cm9zIDwtIGV4cGFuZC5ncmlkKGludGVyYWN0aW9uLmRlcHRoID0gYygxLCAyKSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbi50cmVlcyA9IGMoNTAwLCAxMDAwLCAyMDAwKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaHJpbmthZ2UgPSBjKDAuMDAxLCAwLjAxLCAwLjEpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG4ubWlub2JzaW5ub2RlID0gYygyLCA1LCAxNSkpDQoNCnNldC5zZWVkKDEyMykNCg0Kc2VlZHMgPC0gdmVjdG9yKG1vZGUgPSAibGlzdCIsIGxlbmd0aCA9IChwYXJ0aWNpb25lcyAqIHJlcGV0aWNpb25lcykgKyAxKSANCg0KZm9yIChpIGluIDE6KHBhcnRpY2lvbmVzICogcmVwZXRpY2lvbmVzKSkgew0KICBzZWVkc1tbaV1dIDwtIHNhbXBsZS5pbnQoMTAwMCwgbnJvdyhoaXBlcnBhcmFtZXRyb3MpKSANCiAgfSANCg0Kc2VlZHNbWyhwYXJ0aWNpb25lcyAqIHJlcGV0aWNpb25lcykgKyAxXV0gPC0gc2FtcGxlLmludCgxMDAwLCAxKQ0KDQojIGRlZmluaWNpb24gZGVsIGVudHJlbmFtaWVudG8NCmNvbnRyb2wudHJhaW4gPC0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJyZXBlYXRlZGN2IiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBudW1iZXIgPSBwYXJ0aWNpb25lcywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXBlYXRzID0gcmVwZXRpY2lvbmVzLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlZWRzID0gc2VlZHMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuUmVzYW1wID0gImZpbmFsIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2ZXJib3NlSXRlciA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxsb3dQYXJhbGxlbCA9IFRSVUUpDQoNCiMgYWp1c3RlIGRlbCBtb2RlbG8NCnNldC5zZWVkKDM0MikNCg0KbW9kZWxvX2dibSA8LSB0cmFpbihFeGl0ZWQgfiAuLCANCiAgICAgICAgICAgICAgICAgICAgICBkYXRhID0gY3JlZGl0b3MudHJhaW4ucHJlcCwgDQogICAgICAgICAgICAgICAgICAgICAgbWV0aG9kID0gImdibSIsIA0KICAgICAgICAgICAgICAgICAgICAgIHR1bmVHcmlkID0gaGlwZXJwYXJhbWV0cm9zLA0KICAgICAgICAgICAgICAgICAgICAgIG1ldHJpYyA9ICJBY2N1cmFjeSIsIA0KICAgICAgICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGNvbnRyb2wudHJhaW4sDQogICAgICAgICAgICAgICAgICAgICAgZGlzdHJpYnV0aW9uID0gImFkYWJvb3N0IiwNCiAgICAgICAgICAgICAgICAgICAgICB2ZXJib3NlID0gRkFMU0UpDQoNCiMgc2UgZ3VhcmRhIGVsIG1vZGVsbw0Kc2F2ZVJEUyhtb2RlbG9fZ2JtLCBmaWxlID0gIm1vZGVsby5nYm0ucmRzIikNCmBgYA0KDQpgYGB7cn0NCm1vZGVsb19nYm0gPC0gcmVhZFJEUyhmaWxlID0gIm1vZGVsby5nYm0ucmRzIikNCmBgYA0KDQpFbCBhY2N1cmFjeSBwcm9tZWRpbyBkZWwgbW9kZWxvIGdibSBlbiBlbCBjb25qdW50byBkZSBlbnRyZW5hbWllbnRvIGZ1ZSBkZTogKipgciBtZWFuKG1vZGVsb19nYm1bWyJyZXN1bHRzIl1dW1siQWNjdXJhY3kiXV0pKjEwMGAlKioNCg0KYGBge3J9DQpjbS5nYm0gPC0gY29uZnVzaW9uTWF0cml4KHByZWRpY3QobW9kZWxvX2dibSksIGNyZWRpdG9zLnRyYWluLnByZXAkRXhpdGVkLCBwb3NpdGl2ZSA9ICJjaHVybiIpDQoNCmRyYXdfY29uZnVzaW9uX21hdHJpeChjbS5nYm0pDQoNCmBgYA0KDQpMYSBTZW5zaXRpdml0eSBlbiBlbCBzZXQgZGUgdHJhaW4gZnVlIGRlOiAqKmByIGNtLmdibVtbImJ5Q2xhc3MiXV1bWyJTZW5zaXRpdml0eSJdXSoxMDBgJSoqDQoNCmBgYHtyfQ0KIyByZXByZXNlbnRhY2nDs24gZ3LDoWZpY2ENCmdncGxvdChtb2RlbG9fZ2JtLCBoaWdobGlnaHQgPSBUUlVFKSArIA0KICBsYWJzKHRpdGxlID0gIkV2b2x1Y2nDs24gZGVsIEFjY3VyYWN5IEdyYWRpZW50IEJvb3N0aW5nIikgKw0KICBndWlkZXMoY29sb3IgPSBndWlkZV9sZWdlbmQodGl0bGUgPSAic2hyaW5rYWdlIiksDQogICAgICAgICBzaGFwZSA9IGd1aWRlX2xlZ2VuZCh0aXRsZSA9ICJzaHJpbmthZ2UiKSkgKw0KICB0aGVtZV9idygpICsNCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gImJvdHRvbSIpDQoNCmBgYA0KDQo8YnI+DQoNCiMgIDxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMCwgMCwgMjA1KSI+Q29tcGFyYWNpw7NuIGRlIG1vZGVsb3M8L3NwYW4+IA0KDQpGaW5hbG1lbnRlLCB5YSB0ZW5lbW9zIGxvcyBtb2RlbG9zIGVudHJlbmFkb3MsIHBvciBsbyBxdWUsIGRlYmVtb3MgaWRlbnRpZmljYXIgZWwgcXVlIGNvbnNpZ3VlIG1lam9yZXMgcmVzdWx0YWRvcyBwYXJhIGVsIHByb2JsZW1hIHF1ZSB0ZW5lbW9zLCBxdWUgZXMgZWwgZGUgcHJlZGVjaXIgbGEgZnVnYSBkZSBjbGllbnRlcy4NCg0KQ29uIGFwb3lvIGRlIGdyw6FmaWNvcyB2ZXJlbW9zIGVsIEFjY3VyYWN5IHkgbGEgU2Vuc2l0aXZpdHkgZGUgY2FkYSBtb2RlbG8sIHBhcmEgZmFjaWxpdGFyIGxhIGlkZW50aWZpY2FjacOzbi4NCg0KPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigyMDQsIDE1MywgMCkiPkFjY3VyYWN5PC9zcGFuPg0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkFudGVzIGRlIGVsYWJvcmFyIGxhIHByaW1lciBncsOhZmljYSBjb3JyZXNwb25kaWVudGUgYWwgQWNjdXJhY3kgcHJvbWVkaW8sIGRlYmVtb3MgaWRlbnRpZmljYXIgZWwgKipBY2N1cmFjeSBiYXNhbCoqLiBFbiBwcm9ibGVtYXMgZGUgY2xhc2lmaWNhY2nDs24sIGVzIGxhIG1vZGEgZGUgbGEgdmFyaWFibGUgb2JqZXRpdm8sIHF1ZSBlbiBlc3RlIGNhc28gZXMgRXhpdGVkLiBFbiBsYSBiYXNlIHF1ZSBzZSBlc3TDoSB0cmFiYWphbmRvIMOpc3RlIG5pdmVsIGJhc2FsIGVzdMOhIGRhZG8gcG9yIGxhIGNsYXNlIG1heW9yaXRhcmlhICIwIiwgcXVlIHJlcHJlc2VudGEgZWwgMC43OTYzLCBlbCBjdWFsIGVzIGVsIG5pdmVsIG3DrW5pbW8gcXVlIGRlYmVuIHN1cGVyYXIgbG9zIG1vZGVsb3MuIFBhcmEgcXVlIHVuIG1vZGVsbyBwcmVkaWN0aXZvIHNlYSDDunRpbCwgZGViZSBkZSB0ZW5lciB1biBwb3JjZW50YWplIGRlIGFjaWVydG8gc3VwZXJpb3IgYSBsbyBlc3BlcmFkbyBwb3IgYXphci4NCjwvZGl2Pg0KDQpgYGB7cn0NCnByb3AudGFibGUodGFibGUoY3JlZGl0b3MkRXhpdGVkKSkNCmBgYA0KDQpgYGB7cn0NCm1vZGVscyA8LSBsaXN0KG5iID0gbW9kZWxvX25iLCANCiAgICAgICAgICAgICAgIGxvZ2lzdGljID0gbW9kZWxvX2xvZ2lzdGljLA0KICAgICAgICAgICAgICAgcmYgPSBtb2RlbG9fcmYsDQogICAgICAgICAgICAgICB4Z2IgPSBtb2RlbG9feGdiLA0KICAgICAgICAgICAgICAgc3ZtID0gbW9kZWxvX3N2bSwNCiAgICAgICAgICAgICAgIG5uZXQgPSBtb2RlbG9fbm5ldCwNCiAgICAgICAgICAgICAgIGdibSA9IG1vZGVsb19nYm0NCiAgICAgICAgICAgICAgICApDQoNCnJlc3VsdGFkb3MgPC0gcmVzYW1wbGVzKG1vZGVscykNCg0KbWV0cmljYXMgPC0gcmVzdWx0YWRvcyR2YWx1ZXMgJT4lIA0KICBnYXRoZXIoa2V5ID0gIm1vZGVsbyIsIHZhbHVlID0gInZhbG9yIiwgLVJlc2FtcGxlKSAlPiUgDQogIHNlcGFyYXRlKGNvbCA9ICJtb2RlbG8iLCBpbnRvID0gYygibW9kZWxvIiwgIm1ldHJpY2EiKSwgc2VwID0gIn4iLCByZW1vdmUgPSBUUlVFKQ0KDQptZXRyaWNhcyAlPiUgDQogIGZpbHRlcihtZXRyaWNhID09ICJBY2N1cmFjeSIpICU+JSANCiAgZ3JvdXBfYnkobW9kZWxvKSAlPiUgDQogIHN1bW1hcmlzZShtZWRpYSA9IG1lYW4odmFsb3IpKSAlPiUgDQogIGdncGxvdChhZXMoeCA9IHJlb3JkZXIobW9kZWxvLCBtZWRpYSksIA0KICAgICAgICAgICAgIHkgPSBtZWRpYSwgDQogICAgICAgICAgICAgbGFiZWwgPSByb3VuZChtZWRpYSwgMikpKSArDQogIGdlb21fc2VnbWVudChhZXMoeCA9IHJlb3JkZXIobW9kZWxvLCBtZWRpYSksDQogICAgICAgICAgICAgICAgICAgeSA9IDAsDQogICAgICAgICAgICAgICAgICAgbHdkID0gMiwNCiAgICAgICAgICAgICAgICAgICB4ZW5kID0gbW9kZWxvLCANCiAgICAgICAgICAgICAgICAgICB5ZW5kID0gbWVkaWEpLCANCiAgICAgICAgICAgICAgIGNvbG9yID0gImRhcmtibHVlIiwNCiAgICAgICAgICAgICAgIGx3ZCA9IDIpICsNCiAgZ2VvbV9wb2ludChzaXplID0gOC41LCBwY2ggPSAyMSwgYmcgPSAiZmlyZWJyaWNrIiwgY29sb3IgPSAicmVkIikgKw0KICBnZW9tX3RleHQoY29sb3IgPSAid2hpdGUiLCBzaXplID0gMykgKyANCiAgc2NhbGVfeV9jb250aW51b3VzKGxpbWl0cyA9IGMoMCwgMSkpICsgDQogIGdlb21faGxpbmUoeWludGVyY2VwdCA9IDAuNzksIGxpbmV0eXBlID0gImRhc2hlZCIpICsNCiAgYW5ub3RhdGUoZ2VvbSA9ICJ0ZXh0IiwgeSA9IDAuODQsIHggPSAxLjAsIGxhYmVsID0gIkFjY3VyYWN5IGJhc2FsIikgKw0KICBsYWJzKHRpdGxlID0gIlZhbGlkYWNpw7NuOiBBY2N1cmFjeSBQcm9tZWRpbyIsIA0KICAgICAgIHN1YnRpdGxlID0gIk1vZGVsb3Mgb3JkZW5hZG9zIHBvciBtZWRpYSIsIA0KICAgICAgIHggPSAibW9kZWxvIikgKyANCiAgY29vcmRfZmxpcCgpICsNCiAgdGhlbWVfYncoKQ0KDQpgYGANCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpEZSBsb3MgNyBtb2RlbG9zIGdlbmVyYWRvcywgc29sbyB0cmVzIHNvbiBsb3MgcXVlIG5vIHN1cGVyYW4gZWwgQWNjdXJhY3kgYmFzYWw6IE5haXZlIEJheWVzLCBSZWdyZXNpw7NuIExvZ8Otc3RpY2EgeSBHcmFkaWVudCBCb29zdGluZyBNYWNoaW5lLiBIYXkgcXVlIHJlY29yZGFyIHF1ZSBsYXMgY2xhc2VzIGVzdMOhbiBtdXkgZGVzYmFsYW5jZWFkYXMsIHBvciBsbyBxdWUsIHNvbiBidWVuYXMgbm90aWNpYXMgcXVlIGxhIG1heW9yw61hIGxvcyBtb2RlbG9zIHBhc2VuIGVzZSBuaXZlbCBtaW5pbW8uIEVsIG1vZGVsbyB4Z2Jvb3N0IGVzdMOhIGEgMC4wMDYgZGUgbGxlZ2FyIGFsIEFjY3VyYWN5IGJhc2FsLCBwb3IgbG8gcXVlLCBzZSBjb25zaWRlcmEgcXVlIHBhc2EgZXN0YSBldmFsdWFjacOzbi4NCjwvZGl2Pg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0KbWV0cmljYXMgJT4lIA0KICBncm91cF9ieShtb2RlbG8sIG1ldHJpY2EpICU+JSANCiAgc3VtbWFyaXNlKG1lZGlhID0gbWVhbih2YWxvcikpICU+JQ0KICBzcHJlYWQoa2V5ID0gbWV0cmljYSwgdmFsdWUgPSBtZWRpYSkgJT4lDQogIGFycmFuZ2UoZGVzYyhBY2N1cmFjeSkpICU+JSANCiAga2FibGUoYWxpZ24gPSAiYyIsIA0KICAgICAgICBjYXB0aW9uID0gIlByb21lZGlvOiBBY2N1cmFjeSAtIEthcHBhIiwNCiAgICAgICAgZm9ybWF0LmFyZ3MgPSBsaXN0KGJpZy5tYXJrID0gIiwiKSwgDQogICAgICAgICkgJT4lDQogIGthYmxlX3BhcGVyKCJob3ZlciIsIGZ1bGxfd2lkdGggPSBGKQ0KDQpgYGANCg0KYGBge3IgbWVzc2FnZT1GQUxTRX0NCm1ldHJpY2FzICU+JSANCiAgZmlsdGVyKG1ldHJpY2EgPT0gIkFjY3VyYWN5IikgJT4lIA0KICBncm91cF9ieShtb2RlbG8pICU+JQ0KICBtdXRhdGUobWVkaWEgPSBtZWFuKHZhbG9yKSkgJT4lIA0KICB1bmdyb3VwKCkgJT4lDQogIGdncGxvdChhZXMoeCA9IHJlb3JkZXIobW9kZWxvLCBtZWRpYSksIHkgPSB2YWxvciwgY29sb3IgPSBtb2RlbG8pKSArIA0KICBnZW9tX2JveHBsb3QoYWxwaGEgPSAwLjYsIG91dGxpZXIuc2hhcGUgPSBOQSkgKw0KICBnZW9tX2ppdHRlcih3aWR0aCA9IDAuMSwgYWxwaGEgPSAwLjYpICsNCiAgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gMC43OSwgbGluZXR5cGUgPSAiZGFzaGVkIikgKw0KICBhbm5vdGF0ZShnZW9tID0gInRleHQiLCB5ID0gMC44LCB4ID0gMS4wLCBsYWJlbCA9ICJBY2N1cmFjeSBiYXNhbCIpICsgDQogIHRoZW1lX2J3KCkgKyANCiAgbGFicyh0aXRsZSA9ICJWYWxpZGFjacOzbjogQWNjdXJhY3kgUHJvbWVkaW8iLA0KICAgICAgIHN1YnRpdGxlID0gIk1vZGVsb3Mgb3JkZW5hZG9zIHBvciBtZWRpYSIpICsNCiAgY29vcmRfZmxpcCgpICsNCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKQ0KICANCmBgYA0KDQo8YnI+DQoNCjxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMjA0LCAxNTMsIDApIj5TZW5zaXRpdml0eTwvc3Bhbj4NCg0KYGBge3J9DQoNCnMubmIgPC0gcm91bmQoY20ubmJbWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0sIGRpZ2l0cyA9IDIpDQpzLmxnIDwtIHJvdW5kKGNtLmxnW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCBkaWdpdHMgPSAyKQ0Kcy5zdm0gPC0gcm91bmQoY20uc3ZtW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCBkaWdpdHMgPSAyKQ0Kcy5yZiA8LSByb3VuZChjbS5yZltbImJ5Q2xhc3MiXV1bWyJTZW5zaXRpdml0eSJdXSwgZGlnaXRzID0gMikNCnMueGdiIDwtIHJvdW5kKGNtLnhnYltbImJ5Q2xhc3MiXV1bWyJTZW5zaXRpdml0eSJdXSwgZGlnaXRzID0gMikNCnMubm5ldCA8LSByb3VuZChjbS5ubmV0W1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCBkaWdpdHMgPSAyKQ0Kcy5nYm0gPC0gcm91bmQoY20uZ2JtW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCBkaWdpdHMgPSAyKQ0KDQpjb21wYXJhY2lvbi50cmFpbiA8LSB0cmliYmxlKA0KICB+TW9kZWxvLH5TZW5zaWJpbGlkYWQsDQogICduYicsIHMubmIsDQogICdsZycsIHMubGcsDQogICdzdm0nLCBzLnN2bSwNCiAgJ3JmJywgcy5yZiwNCiAgJ3hnYicsIHMueGdiLA0KICAnbm5ldCcsIHMubm5ldCwNCiAgJ2dibScsIHMuZ2JtDQogICkNCg0KY29tcGFyYWNpb24udHJhaW4kTW9kZWxvIDwtIGZhY3Rvcihjb21wYXJhY2lvbi50cmFpbiRNb2RlbG8sIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxldmVscyA9IGNvbXBhcmFjaW9uLnRyYWluJE1vZGVsbw0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFtvcmRlcihjb21wYXJhY2lvbi50cmFpbiRTZW5zaWJpbGlkYWQpXSkNCg0KY29tcGFyYWNpb24udHJhaW4gJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBNb2RlbG8sIHkgPSBTZW5zaWJpbGlkYWQpKSArDQogIGdlb21fc2VnbWVudChhZXMoeCA9IE1vZGVsbywgeGVuZCA9IE1vZGVsbywgeSA9IDAsIHllbmQgPSBTZW5zaWJpbGlkYWQpLA0KICAgICAgICAgICAgICAgY29sb3IgPSAiZGFya2JsdWUiLCBsd2QgPSAyKSArDQogIGdlb21fcG9pbnQoc2l6ZSA9IDkuNSwgcGNoID0gMjEsIGJnID0gInN0ZWVsYmx1ZSIsIGNvbCA9ICJyZWQiKSArDQogIGdlb21fdGV4dChhZXMobGFiZWwgPSBTZW5zaWJpbGlkYWQpLCBjb2xvciA9ICJ3aGl0ZSIsIHNpemUgPSAzLjUpICsNCiAgbGFicyh0aXRsZSA9ICJDb21wYXJhY2nDs24gcG9yIFNlbnNpYmlsaWRhZCIsIA0KICAgICAgIHkgPSAiTW9kZWxvIiwNCiAgICAgICB4ID0gIlNlbnNpYmlsaWRhZCIpICsNCiAgY29vcmRfZmxpcCgpDQoNCmBgYA0KDQpOdWV2YW1lbnRlLCBsb3MgbW9kZWxvcyBxdWUgbm8gc3VwZXJhcm9uIGVsIEFjY3VyYWN5IGJhc2FsIHByZXNlbnRhbiBsYSBTZW5zaXRpdml0eSBtw6FzIGJhamFzLg0KDQo8YnI+DQoNCiMgIDxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMCwgMCwgMjA1KSI+RXZhbHVhY2nDs24gY29uIGRhdG9zIGRlIHZhbGlkYWNpw7NuPC9zcGFuPiANCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpBIHBlc2FyIGRlIHF1ZSBhbGd1bm9zIG1vZGVsb3MgdHV2aWVyb24gYnVlbiBkZXNlbXBlw7FvLCBsYSBldmFsdWFjacOzbiBmaW5hbCBzZSBkZWJlIGRlIGhhY2VyIGNvbiB1biBjb25qdW50byBkZSBvYnNlcnZhY2lvbmVzIGRpZmVyZW50ZSwgcG9ycXVlIHNvbiBvYnNlcnZhY2lvbmVzIHF1ZSBsb3MgYWxnb3JpdG1vcyBubyBoYW4gdmlzdG8sIGFkZW3DoXMsIGVzdG8gbm9zIHBlcm1pdGlyw6EgY29ub2NlciBzaSBsb3MgbW9kZWxvcyBnZW5lcmFyb24gc29icmVhanVzdGUuDQo8L2Rpdj4NCg0KYGBge3J9DQojIE5haXZlIEJheWVzDQoNCnByZWRzLm5iIDwtIHByZWRpY3QobW9kZWxvX25iLCBjcmVkaXRvcy52YWxpZGFjaW9uLnByZXApDQpjbS5uYi52YWwgPC0gY29uZnVzaW9uTWF0cml4KHByZWRzLm5iLCBjcmVkaXRvcy52YWxpZGFjaW9uLnByZXAkRXhpdGVkLCBwb3NpdGl2ZSA9ICJjaHVybiIpDQpkcmF3X2NvbmZ1c2lvbl9tYXRyaXgoY20ubmIudmFsKQ0KDQojIExvZ2lzdGljIFJlZ3Jlc3Npb24NCg0KcHJlZHMubG9nIDwtIHByZWRpY3QobW9kZWxvX2xvZ2lzdGljLCBjcmVkaXRvcy52YWxpZGFjaW9uLnByZXApDQpjbS5sZy52YWwgPC0gY29uZnVzaW9uTWF0cml4KHByZWRzLmxvZywgY3JlZGl0b3MudmFsaWRhY2lvbi5wcmVwJEV4aXRlZCwgcG9zaXRpdmUgPSAiY2h1cm4iKQ0KZHJhd19jb25mdXNpb25fbWF0cml4KGNtLmxnLnZhbCkNCg0KIyBTdXBvcnQgVmVjdG9yIE1hY2hpbmUNCg0KcHJlZHMuc3ZtIDwtIHByZWRpY3QobW9kZWxvX3N2bSwgY3JlZGl0b3MudmFsaWRhY2lvbi5wcmVwKQ0KY20uc3ZtLnZhbCA8LSBjb25mdXNpb25NYXRyaXgocHJlZHMuc3ZtLCBjcmVkaXRvcy52YWxpZGFjaW9uLnByZXAkRXhpdGVkLCBwb3NpdGl2ZSA9ICJjaHVybiIpDQpkcmF3X2NvbmZ1c2lvbl9tYXRyaXgoY20uc3ZtLnZhbCkNCg0KIyBSYW5kb20gRm9yZXN0DQoNCnByZWRzLnJmIDwtIHByZWRpY3QobW9kZWxvX3JmLCBjcmVkaXRvcy52YWxpZGFjaW9uLnByZXApDQpjbS5yZi52YWwgPC0gY29uZnVzaW9uTWF0cml4KHByZWRzLnJmLCBjcmVkaXRvcy52YWxpZGFjaW9uLnByZXAkRXhpdGVkLCBwb3NpdGl2ZSA9ICJjaHVybiIpDQpkcmF3X2NvbmZ1c2lvbl9tYXRyaXgoY20ucmYudmFsKQ0KDQojIEV4dHJlbWUgR3JhZGllbnQgQm9vc3RpbmcNCg0KcHJlZHMueGdiIDwtIHByZWRpY3QobW9kZWxvX3hnYiwgY3JlZGl0b3MudmFsaWRhY2lvbi5wcmVwKQ0KY20ueGdiLnZhbCA8LSBjb25mdXNpb25NYXRyaXgocHJlZHMueGdiLCBjcmVkaXRvcy52YWxpZGFjaW9uLnByZXAkRXhpdGVkLCBwb3NpdGl2ZSA9ICJjaHVybiIpDQpkcmF3X2NvbmZ1c2lvbl9tYXRyaXgoY20ueGdiLnZhbCkNCg0KIyBOZXVyb25hbCBOZXR3b3JrDQpwcmVkcy5ubmV0IDwtIHByZWRpY3QobW9kZWxvX25uZXQsIGNyZWRpdG9zLnZhbGlkYWNpb24ucHJlcCkNCmNtLm5uZXQudmFsIDwtIGNvbmZ1c2lvbk1hdHJpeChwcmVkcy5ubmV0LCBjcmVkaXRvcy52YWxpZGFjaW9uLnByZXAkRXhpdGVkLCBwb3NpdGl2ZSA9ICJjaHVybiIpDQpkcmF3X2NvbmZ1c2lvbl9tYXRyaXgoY20ubm5ldC52YWwpDQoNCiMgR3JhZGllbnQgQm9vc3RpbmcgTWFjaGluZQ0KDQpwcmVkcy5nYm0gPC0gcHJlZGljdChtb2RlbG9fZ2JtLCBjcmVkaXRvcy52YWxpZGFjaW9uLnByZXApDQpjbS5nYm0udmFsIDwtIGNvbmZ1c2lvbk1hdHJpeChwcmVkcy5nYm0sIGNyZWRpdG9zLnZhbGlkYWNpb24ucHJlcCRFeGl0ZWQsIHBvc2l0aXZlID0gImNodXJuIikNCmRyYXdfY29uZnVzaW9uX21hdHJpeChjbS5nYm0udmFsKQ0KDQpBY2N1cmFjeS52YWwgPC0gdHJpYmJsZSgNCiAgfk1vZGVsbyx+QWNjdXJhY3ksfkthcHBhLA0KICAnbmInLCBjbS5uYi52YWxbWyJvdmVyYWxsIl1dW1siQWNjdXJhY3kiXV0sIGNtLm5iLnZhbFtbIm92ZXJhbGwiXV1bWyJLYXBwYSJdXSwNCiAgJ2xvZ2lzdGljJywgY20ubGcudmFsW1sib3ZlcmFsbCJdXVtbIkFjY3VyYWN5Il1dLCBjbS5sZy52YWxbWyJvdmVyYWxsIl1dW1siS2FwcGEiXV0sDQogICdzdm0nLCBjbS5zdm0udmFsW1sib3ZlcmFsbCJdXVtbIkFjY3VyYWN5Il1dLCBjbS5zdm0udmFsW1sib3ZlcmFsbCJdXVtbIkthcHBhIl1dLA0KICAncmYnLCBjbS5yZi52YWxbWyJvdmVyYWxsIl1dW1siQWNjdXJhY3kiXV0sIGNtLnJmLnZhbFtbIm92ZXJhbGwiXV1bWyJLYXBwYSJdXSwNCiAgJ3hnYicsIGNtLnhnYi52YWxbWyJvdmVyYWxsIl1dW1siQWNjdXJhY3kiXV0sIGNtLnhnYi52YWxbWyJvdmVyYWxsIl1dW1siS2FwcGEiXV0sDQogICdubmV0JywgY20ubm5ldC52YWxbWyJvdmVyYWxsIl1dW1siQWNjdXJhY3kiXV0sIGNtLm5uZXQudmFsW1sib3ZlcmFsbCJdXVtbIkthcHBhIl1dLA0KICAnZ2JtJywgY20uZ2JtLnZhbFtbIm92ZXJhbGwiXV1bWyJBY2N1cmFjeSJdXSwgY20uZ2JtLnZhbFtbIm92ZXJhbGwiXV1bWyJLYXBwYSJdXQ0KICApDQoNCg0KQWNjdXJhY3kudmFsICU+JSANCiAgYXJyYW5nZShkZXNjKEFjY3VyYWN5KSkgJT4lIA0KICBrYWJsZShhbGlnbiA9ICJjIiwgDQogICAgICAgIGNhcHRpb24gPSAiUHJvbWVkaW86IEFjY3VyYWN5IC0gS2FwcGEiDQogICAgICAgICkgJT4lDQogIGthYmxlX3BhcGVyKCJob3ZlciIsIGZ1bGxfd2lkdGggPSBGKQ0KDQpBY2N1cmFjeS52YWwgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSByZW9yZGVyKE1vZGVsbywgQWNjdXJhY3kpLCANCiAgICAgICAgICAgICB5ID0gQWNjdXJhY3ksIA0KICAgICAgICAgICAgIGxhYmVsID0gcm91bmQoQWNjdXJhY3ksIDIpKSkgKw0KICBnZW9tX3NlZ21lbnQoYWVzKHggPSByZW9yZGVyKE1vZGVsbywgQWNjdXJhY3kpLA0KICAgICAgICAgICAgICAgICAgIHkgPSAwLA0KICAgICAgICAgICAgICAgICAgIGx3ZCA9IDIsDQogICAgICAgICAgICAgICAgICAgeGVuZCA9IE1vZGVsbywgDQogICAgICAgICAgICAgICAgICAgeWVuZCA9IEFjY3VyYWN5KSwgDQogICAgICAgICAgICAgICBjb2xvciA9ICJkYXJrYmx1ZSIsDQogICAgICAgICAgICAgICBsd2QgPSAyKSArDQogIGdlb21fcG9pbnQoc2l6ZSA9IDguNSwgcGNoID0gMjEsIGJnID0gImZpcmVicmljayIsIGNvbG9yID0gInJlZCIpICsNCiAgZ2VvbV90ZXh0KGNvbG9yID0gIndoaXRlIiwgc2l6ZSA9IDMpICsgDQogIHNjYWxlX3lfY29udGludW91cyhsaW1pdHMgPSBjKDAsIDEpKSArIA0KICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSAwLjc5LCBsaW5ldHlwZSA9ICJkYXNoZWQiKSArDQogIGFubm90YXRlKGdlb20gPSAidGV4dCIsIHkgPSAwLjg0LCB4ID0gMS4wLCBsYWJlbCA9ICJBY2N1cmFjeSBiYXNhbCIpICsNCiAgbGFicyh0aXRsZSA9ICJWYWxpZGFjacOzbiBBY2N1cmFjeSIsIA0KICAgICAgIHN1YnRpdGxlID0gIk1vZGVsb3Mgb3JkZW5hZG9zIHBvciBBY2N1cmFjeSIsIA0KICAgICAgIHggPSAibW9kZWxvIikgKyANCiAgY29vcmRfZmxpcCgpICsNCiAgdGhlbWVfYncoKQ0KDQpgYGANCg0KRW4gZWwgY29uanVudG8gZGUgdmFsaWRhY2nDs24gZWwgbW9kZWxvIHF1ZSBnZW5lcsOzIGVsIG3DoXMgYWx0byBBY2N1cmFjeSBmdWUgZWwgU1ZNIHNlZ3VpZG8gcG9yIGVsIFJGLiBFbCBtb2RlbG8gcXVlIG5vIG1lam9yw7MgZGljaGEgbWVkaWRhIGZ1ZSBlbCBOYWl2ZSBCYXllcy4NCg0KYGBge3IgbWVzc2FnZT1GQUxTRX0NCnJlc3VsdC5hY2MgPC0gbWV0cmljYXMgJT4lIA0KICBncm91cF9ieShtb2RlbG8sIG1ldHJpY2EpICU+JSANCiAgc3VtbWFyaXNlKG1lZGlhID0gbWVhbih2YWxvcikpICU+JQ0KICBzcHJlYWQoa2V5ID0gbWV0cmljYSwgdmFsdWUgPSBtZWRpYSkgJT4lDQogIHJlbmFtZShNb2RlbG8gPSBtb2RlbG8sDQogICAgICAgICBBY2N1cmFjeS5UcmFpbiA9IEFjY3VyYWN5KSAlPiUgDQogIGRwbHlyOjpzZWxlY3QoLUthcHBhKSAlPiUgDQogIGlubmVyX2pvaW4oQWNjdXJhY3kudmFsLCBieSA9ICJNb2RlbG8iKSAlPiUgDQpkcGx5cjo6c2VsZWN0KC1LYXBwYSkgJT4lIA0KICByZW5hbWUoQWNjdXJhY3kuVmFsaWRhY2lvbiA9IEFjY3VyYWN5KSAlPiUgDQogIGFycmFuZ2UoZGVzYyhBY2N1cmFjeS5WYWxpZGFjaW9uKSkNCg0KYnJrcyA8LSBzZXEoMC43LCAwLjk5LCAwLjAxKQ0KY2xycyA8LSBjb2xvclJhbXBQYWxldHRlKGMoIndoaXRlIiwgIiM2YmFlZDYiKSkobGVuZ3RoKGJya3MpICsgMSkNCg0KZGF0YXRhYmxlKHJlc3VsdC5hY2MsDQogICAgICAgICAgIGVzY2FwZSA9IEZBTFNFLA0KICAgICAgICAgICByb3duYW1lcyA9IEZBTFNFLA0KICAgICAgICAgIG9wdGlvbnMgPSBsaXN0KGF1dG9XaWR0aCA9IEZBTFNFKSkgJT4lDQogICAgICAgZm9ybWF0U3R5bGUoYygiQWNjdXJhY3kuVmFsaWRhY2lvbiIpLCANCiAgICAgICAgICAgICAgICAgICBiYWNrZ3JvdW5kQ29sb3IgPSBzdHlsZUludGVydmFsKGJya3MsIGNscnMpKQ0KDQpgYGANCg0KYGBge3J9DQpyZXN1bHQuYWNjICU+JQ0KICBwaXZvdF9sb25nZXIoY29scyA9IHN0YXJ0c193aXRoKCdBY2MnKSwNCiAgICAgICAgICAgICAgIG5hbWVzX3RvID0nVGlwbycsDQogICAgICAgICAgICAgICB2YWx1ZXNfdG8gPSAnQWNjdXJhY3knDQogICAgICAgICAgICAgICApICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gcmVvcmRlcihNb2RlbG8sIEFjY3VyYWN5KSwNCiAgICAgICAgICAgICB5ID0gQWNjdXJhY3ksIA0KICAgICAgICAgICAgIGNvbG9yID0gVGlwbywNCiAgICAgICAgICAgICBsYWJlbCA9IHJvdW5kKEFjY3VyYWN5LCAyKSkpICsNCiAgZ2VvbV9wb2ludChzaXplID0gOCkgKw0KICBzY2FsZV9jb2xvcl9tYW51YWwodmFsdWVzID0gYygiZ3JheTUwIiwgImZpcmVicmljayIpKSArIA0KICBnZW9tX3RleHQoY29sb3IgPSAid2hpdGUiLCBzaXplID0gMykgKw0KICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSAwLjc5LCBsaW5ldHlwZSA9ICJkYXNoZWQiKSArIA0KICBhbm5vdGF0ZShnZW9tID0gInRleHQiLCB5ID0gMC43OSwgeCA9IDEuMiwgbGFiZWwgPSAiQWNjdXJhY3kgYmFzYWwiKSArDQogIGNvb3JkX2ZsaXAoKSArDQogIGxhYnModGl0bGUgPSAiQWNjdXJhY3kgZGUgRW50cmVuYW1pZW50byB5IFZhbGlkYWNpw7NuIiwgeCA9ICJNb2RlbG8iKSArDQogIGdlb21fcGF0aChhcnJvdyA9IGFycm93KGxlbmd0aCA9IHVuaXQoMS41LCdtbScpLCB0eXBlID0gJ2Nsb3NlZCcpLCBjb2xvciA9ICdncmF5MzAnKSArDQogIHRoZW1lX2J3KCkgKw0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIikNCg0KYGBgDQoNCkFob3JhLCB2ZWFtb3MgcXXDqSBzdWNlZGnDsyBjb24gbGEgbcOpdHJpY2EgcXVlIHNlIGVsaWdpw7MgcGFyYSBldmFsdWFyIGxvcyBtb2RlbG9zOiBTZW5zaWJpbGlkYWQuDQoNCmBgYHtyfQ0KIyBTZW5zaWJpbGlkYWQNCg0Kcy5uYi52YWwgPC0gcm91bmQoY20ubmIudmFsW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCBkaWdpdHMgPSAyKQ0Kcy5sZy52YWwgPC0gcm91bmQoY20ubGcudmFsW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCBkaWdpdHMgPSAyKQ0Kcy5zdm0udmFsIDwtIHJvdW5kKGNtLnN2bS52YWxbWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0sIGRpZ2l0cyA9IDIpDQpzLnJmLnZhbCA8LSByb3VuZChjbS5yZi52YWxbWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0sIGRpZ2l0cyA9IDIpDQpzLnhnYi52YWwgPC0gcm91bmQoY20ueGdiLnZhbFtbImJ5Q2xhc3MiXV1bWyJTZW5zaXRpdml0eSJdXSwgZGlnaXRzID0gMikNCnMubm5ldC52YWwgPC0gcm91bmQoY20ubm5ldC52YWxbWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0sIGRpZ2l0cyA9IDIpDQpzLmdibS52YWwgPC0gcm91bmQoY20uZ2JtLnZhbFtbImJ5Q2xhc3MiXV1bWyJTZW5zaXRpdml0eSJdXSwgZGlnaXRzID0gMikNCg0KU2Vuc2liaWxpZGFkLnZhbCA8LSB0cmliYmxlKA0KICB+TW9kZWxvLH5UcmFpbix+VmFsaWRhY2lvbiwNCiAgJ25iJywgcy5uYiwgcy5uYi52YWwsDQogICdsZycsIHMubGcsIHMubGcudmFsLA0KICAnc3ZtJywgcy5zdm0sIHMuc3ZtLnZhbCwNCiAgJ3JmJywgcy5yZiwgcy5yZi52YWwsDQogICd4Z2InLCBzLnhnYiwgcy54Z2IudmFsLA0KICAnbm5ldCcsIHMubm5ldCwgcy5ubmV0LnZhbCwNCiAgJ2dibScsIHMuZ2JtLCBzLmdibS52YWwNCiAgKQ0KDQpTZW5zaWJpbGlkYWQudmFsICU+JSANCiAgYXJyYW5nZShkZXNjKFZhbGlkYWNpb24pKSAlPiUgDQogIGthYmxlKGFsaWduID0gImMiLCANCiAgICAgICAgY2FwdGlvbiA9ICJTZW5zaWJpbGlkYWQiDQogICAgICAgICkgJT4lDQogIGthYmxlX3BhcGVyKCJob3ZlciIsIGZ1bGxfd2lkdGggPSBGKQ0KDQpTZW5zaWJpbGlkYWQudmFsICU+JQ0KICBwaXZvdF9sb25nZXIoY29scyA9IGVuZHNfd2l0aCgnbicpLA0KICAgICAgICAgICAgICAgbmFtZXNfdG8gPSdUaXBvJywNCiAgICAgICAgICAgICAgIHZhbHVlc190byA9ICdTZW5zaWJpbGlkYWQnDQogICAgICAgICAgICAgICApICU+JQ0KICBtdXRhdGUoTW9kZWxvID0gYXMuZmFjdG9yKE1vZGVsbyksDQogICAgICAgICBNb2RlbG8gPSBmY3RfcmVvcmRlcihNb2RlbG8sIFNlbnNpYmlsaWRhZCkpICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gTW9kZWxvLA0KICAgICAgICAgICAgIHkgPSBTZW5zaWJpbGlkYWQsIA0KICAgICAgICAgICAgIGNvbG9yID0gVGlwbywNCiAgICAgICAgICAgICBsYWJlbCA9IHJvdW5kKFNlbnNpYmlsaWRhZCwgMikpKSArDQogIGdlb21fcG9pbnQoc2l6ZSA9IDgpICsNCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcyA9IGMoImdyYXk1MCIsICJmaXJlYnJpY2siKSkgKyANCiAgZ2VvbV90ZXh0KGNvbG9yID0gIndoaXRlIiwgc2l6ZSA9IDMpICsNCiAgY29vcmRfZmxpcCgpICsNCiAgbGFicyh0aXRsZSA9ICJTZW5zaWJpbGlkYWQgZGUgRW50cmVuYW1pZW50byB5IFZhbGlkYWNpw7NuIiwgeCA9ICJNb2RlbG8iKSArDQogIGdlb21fcGF0aChhcnJvdyA9IGFycm93KGxlbmd0aCA9IHVuaXQoMS41LCdtbScpLCB0eXBlID0gJ2Nsb3NlZCcpLCBjb2xvciA9ICdncmF5MzAnKSArDQogIHRoZW1lX2J3KCkgKw0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIikNCg0KYGBgDQoNCkxhIFNlbnNpdGl2aXR5IGRlbCBtb2RlbG8gTmFpdmUgQmF5ZXMgZW4gZWwgY29uanVudG8gZGUgdmFsaWRhY2nDs24gZnVlIGRlOiAqKmByIHJvdW5kKGNtLm5iLnZhbFtbImJ5Q2xhc3MiXV1bWyJTZW5zaXRpdml0eSJdXSwgZGlnaXRzID0gMikqMTAwYCUqKg0KDQpMYSBTZW5zaXRpdml0eSBkZWwgbW9kZWxvIGRlIFJlZ3Jlc2nDs24gTG9nw61zdGljYSBlbiBlbCBjb25qdW50byBkZSB2YWxpZGFjacOzbiBmdWUgZGU6ICoqYHIgcm91bmQoY20ubGcudmFsW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCBkaWdpdHMgPSAyKSoxMDBgJSoqDQoNCkxhIFNlbnNpdGl2aXR5IGRlbCBtb2RlbG8gZGUgTcOhcXVpbmEgZGUgVmVjdG9yIFNvcG9ydGUgZW4gZWwgY29uanVudG8gZGUgdmFsaWRhY2nDs24gZnVlIGRlOiAqKmByIHJvdW5kKGNtLnN2bS52YWxbWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0sIGRpZ2l0cyA9IDIpKjEwMGAlKioNCg0KTGEgU2Vuc2l0aXZpdHkgZGVsIG1vZGVsbyBSYW5kb20gRm9yZXN0IGVuIGVsIGNvbmp1bnRvIGRlIHZhbGlkYWNpw7NuIGZ1ZSBkZTogKipgciByb3VuZChjbS5yZi52YWxbWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0sIGRpZ2l0cyA9IDIpKjEwMGAlKioNCg0KTGEgU2Vuc2l0aXZpdHkgZGVsIG1vZGVsbyBYR0Jvb3N0IGVuIGVsIGNvbmp1bnRvIGRlIHZhbGlkYWNpw7NuIGZ1ZSBkZTogKipgciByb3VuZChjbS54Z2IudmFsW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCBkaWdpdHMgPSAyKSoxMDBgJSoqDQoNCkxhIFNlbnNpdGl2aXR5IGRlbCBtb2RlbG8gZGUgUmVkZXMgTmV1cm9uYWxlcyBlbiBlbCBjb25qdW50byBkZSB2YWxpZGFjacOzbiBmdWUgZGU6ICoqYHIgcm91bmQoY20ubm5ldC52YWxbWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0sIGRpZ2l0cyA9IDIpKjEwMGAlKioNCg0KTGEgU2Vuc2l0aXZpdHkgZGVsIG1vZGVsbyBHcmFkaWVudCBCb29zdGluZyBlbiBlbCBjb25qdW50byBkZSB2YWxpZGFjacOzbiBmdWUgZGU6ICoqYHIgcm91bmQoY20uZ2JtLnZhbFtbImJ5Q2xhc3MiXV1bWyJTZW5zaXRpdml0eSJdXSwgZGlnaXRzID0gMikqMTAwYCUqKg0KDQpQcsOhY3RpY2FtZW50ZSwgbmluZ8O6biBtb2RlbG8gbWVqb3LDsyBsYSBTZW5zaWJpbGlkYWQgYSBleGNlcGNpw7NuIGRlIGxhIHJlZCBuZXVyb25hbC4gUG9yIGxvIHRhbnRvLCBoYXJlbW9zIHVuIGVuc2FtYmxlIGNvbiBhbGd1bm9zIG1vZGVsb3MgcGFyYSB2ZXIgc2kgc2Ugb2J0aWVuZW4gbWVqb3JlcyByZXN1bHRhZG9zIHF1ZSBlbCBtZWpvciBtb2RlbG86IG5uZXQuDQoNCjxicj4NCg0KIyAgPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigwLCAwLCAyMDUpIj5JbXBvcnRhbmNpYSBkZSBsYXMgdmFyaWFibGVzPC9zcGFuPg0KDQpWZWFtb3MgY3XDoWxlcyBzb24gbGFzIHZhcmlhYmxlcyBtw6FzIGltcG9ydGFudGVzIHNlZ8O6biBlbCBtb2RlbG8gcXVlIG3DoXMgYWx0byBSZWNhbGwgb2J0dXZvOg0KDQpgYGB7cn0NCnZhckltcChtb2RlbG9fbm5ldCkgJT4lIHBsb3QobWFpbiA9ICJNb2RlbG8gTk5FVCIpDQoNCmBgYA0KDQpFbiBsYSBncsOhZmljYSBzZSBvYnNlcnZhIHF1ZSBlbCB0b3AgMyBkZSBsYXMgdmFyaWFibGVzIHF1ZSBtw6FzIGluZmx1eWVuIGVuIGRpY2hvIG1vZGVsbyBzb246IEFnZSBzZWd1aWRhIHBvciBOdW1PZlByb2R1Y3RzIHF1ZSB0aWVuZSAyIHkgR2VvZ3JhcGh5MS4NCg0KPGJyPg0KDQojICA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPkVuc2FtYmxlPC9zcGFuPg0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkFob3JhLCB2YW1vcyBhIGhhY2VyIHVuIGVuc2FtYmxlIGNvbiB0cmVzIG1vZGVsb3MgcGFyYSB2ZXIgc2kgcG9kZW1vcyBtZWpvcmFyIGxvcyByZXN1bHRhZG9zIG9idGVuaWRvcyBwb3IgZWwgbW9kZWxvIG5uZXQuDQoNCkVsIGVuc2FtYmxlIGNvbnNpc3RlIGVuIGNvbWJpbmFyIGxhcyBwcmVkaWNjaW9uZXMgZGUgZG9zIG8gbcOhcyBtb2RlbG9zIGNvbiBlbCBvYmpldGl2byBkZSBzdXBlcmFyIGxvcyByZXN1bHRhZG9zIG9idGVuaWRvcyBwb3IgZWwgbWVqb3IgbW9kZWxvIGluZGl2aWR1YWwuIEVzdGEgZXN0cmF0w6lnaWEgc2UgcmVjb21pZW5kYSBhcGxpY2FybGEgY3VhbmRvIGhheSBkaWZlcmVuY2lhcyBlbnRyZSBsb3MgZGlzdGludG9zIG1vZGVsb3MgZ2VuZXJhZG9zLCB5YSBxdWUgbGEgaWRlYSBlcyBjb21wZW5zYXIgbG9zIGZhbGxvcyBkZSB1bm9zIGNvbiBsb3MgYWNpZXJ0b3MgZGUgb3Ryb3MuIEFwcmlvcmkgbm8gc2UgZXNwZXJhbiBtZWpvcmVzIHJlc3VsdGFkb3MsIHlhIHF1ZSBsb3MgbW9kZWxvcyBhIGVuc2FtYmxhciBubyBwcmVzZW50YW4gZGlmZXJlbmNpYXMgc2lnbmlmaWNhdGl2YXMsIHNpbiBlbWJhcmdvLCBwYXJhIGZpbmVzIGRlIG1vc3RyYXIgZWwgcHJvY2VkaW1pZW50byBzZSBhcGxpY2Fyw6EgZXN0YSBlc3RyYXTDqWdpYS4NCjwvZGl2Pg0KDQpFbiBlc3RlIGNhc28sIGNvbW8gbGEgdmFyaWFibGUgYSBwcmVkZWNpciBlcyBjYXRlZ8OzcmljYSBzZSB1dGlsaXphcsOhIGxhIG1vZGEgcGFyYSBlbCBlbnNhbWJsZS4NCg0KTG9zIG1vZGVsb3MgYSBjb21iaW5hciBzb246DQoNCiogKipTVk0qKg0KKiAqKlhHQm9vc3QqKg0KKiAqKlJGKioNCg0KYGBge3J9DQojIGRhdGEgZnJhbWUNCkVuc2FtYmxlIDwtIGRhdGEuZnJhbWUoDQogIHByZWRzLnN2bSwNCiAgcHJlZHMueGdiLA0KICBwcmVkcy5yZg0KKQ0KDQpjb2xuYW1lcyhFbnNhbWJsZSkgPC0gYygiU1ZNIiwgIlhHQiIsICJSRiIpDQoNCm1vZGEgPC0gZnVuY3Rpb24oeCl7DQogIHJldHVybihuYW1lcyh3aGljaC5tYXgodGFibGUoeCkpKSkNCiAgfQ0KDQpFbnNhbWJsZS5wcmVkcyA8LSBFbnNhbWJsZSAlPiUgDQogIG11dGF0ZShNb2RhID0gYXBwbHkoWCA9IEVuc2FtYmxlLCBNQVJHSU4gPSAxLCBGVU4gPSBtb2RhKSkNCg0KRW5zYW1ibGUucHJlZHMkTW9kYSA8LSBhcy5mYWN0b3IoRW5zYW1ibGUucHJlZHMkTW9kYSkNCg0KYGBgDQoNCkVsIEFjY3VyYWN5IHByb21lZGlvIG9idGVuaWRvIGRlbCBlbnNhbWJsZSBlcyBkZTogKipgciBtZWFuKGNyZWRpdG9zLnZhbGlkYWNpb24ucHJlcCRFeGl0ZWQgPT0gRW5zYW1ibGUucHJlZHMkTW9kYSlgKioNCg0KYGBge3IgbWVzc2FnZT1GQUxTRX0NCmNtLmVuc2FtYmxlIDwtIGNvbmZ1c2lvbk1hdHJpeChFbnNhbWJsZS5wcmVkcyRNb2RhLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjcmVkaXRvcy52YWxpZGFjaW9uLnByZXAkRXhpdGVkLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBvc2l0aXZlID0gImNodXJuIikNCg0KZHJhd19jb25mdXNpb25fbWF0cml4KGNtLmVuc2FtYmxlKQ0KDQpgYGANCg0KQ29tbyBzZSBlc3BlcmFiYSwgbm8gaHVibyBtZWpvcmFzIHNpZ25pZmljYXRpdmFzIGVuIGNvbXBhcmFjacOzbiBjb24gZWwgbWVqb3IgbW9kZWxvLCBwdWVzIHNvbG8gc2UgcmVkdWplcm9uIDcgZmFsc29zIHBvc2l0aXZvcyBjb24gZWwgZXNhbWJsZS4gU2luIGVtYmFyZ28sIG5vIGVzdMOhIGRlbcOhcyBjb21vY2VyIGVzdGEgdMOpY25pY2EuDQoNCjxicj4NCg0KIyAgPHNwYW4gc3R5bGU9ImNvbG9yOnJnYigwLCAwLCAyMDUpIj5FbGVjY2nDs24gZGVsIHZhbG9yIGRlIGNvcnRlIGFkZWN1YWRvPC9zcGFuPg0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NClRvZG9zIGxvcyBhbGdvcml0bW9zIGN1YW5kbyBnZW5lcmFuIGxhcyBwcmVkaWNjaW9uZXMgbG8gaGFjZW4gY29uc2lkZXJhbmRvIHVuIGN1dG9mZiBvIHB1bnRvIGRlIGNvcnRlIGRlbCAwLjUsIGVzIGRlY2lyLCBwcm9iYWJpbGlkYWRlcyBtYXlvcmVzIGEgZXNlIG5pdmVsIGxhcyBldGlxdWV0YW4gY29tbyAxIHkgMCBlbiBvdHJvIGNhc28uIFNpbiBlbWJhcmdvLCBlc3RlIHVtYnJhbCBzZSBwdWVkZSBvcHRpbWl6YXIgcGFyYSB0cmF0YXIgZGUgbWluaW1pemFyIGVsIHRpcG8gZGUgZXJyb3IgcXVlIHNlIGRlc2VhIHJlZHVjaXIuICoqRW4gZXN0ZSBhbsOhbGlzaXMgbG8gcXVlIHNlIGJ1c2NhcsOhIGVzIG1pbmltaXphciBlbCBlcnJvciB0aXBvIElJLCBsb3MgZmFsc29zIG5lZ2F0aXZvcy4qKg0KDQpUcmFiYWphcmVtb3MgY29uIGVsIG1vZGVsbyBubmV0LCB5YSBxdWUgZnVlIHVubyBkZSBsb3MgZG9zIG1vZGVsb3MgcXVlIGdlbmVyw7MgbGEgc2Vuc2liaWxpZGFkIG3DoXMgYWx0YSB5LCBhZGVtw6FzLCB0dXZvIHBvY2EgZGlmZXJlbmNpYSBlbnRyZSBsYSBzZW5zaWJpbGlkYWQgZGUgZW50cmVuYW1pZW50byB5IGRlIHZhbGlkYWNpw7NuLg0KDQpTZSB2dWVsdmUgYSBnZW5lcmFyIGVsIG1vZGVsbyBwZXJvIGFob3JhIGFncmVnYW5kbyBlbCBhcmd1bWVudG8gKipjbGFzc1Byb2JzKiosIHBhcmEgcXVlIGNhbGN1bGUgbGFzIHByb2JhYmlsaWRhZGVzIGRlIGFtYmFzIGNsYXNlcy4NCjwvZGl2Pg0KDQpgYGB7ciBldmFsPUZBTFNFfQ0KIyBwYXJhbGVsaXphY2lvbiBkZWwgcHJvY2Vzbw0KY29yZXMgPC0gZGV0ZWN0Q29yZXMoKS0xDQpjbCA8LSBtYWtlQ2x1c3Rlcihjb3JlcywgdHlwZSA9ICJTT0NLIikNCnJlZ2lzdGVyRG9TTk9XKGNsKQ0KDQojIGhpcGVycGFyYW1ldHJvcywgbm8uIHJlcGV0aWNpb25lcyB5IHNlbWlsbGEgcGFyYSBjYWRhIHJlcGV0aWNpb24NCnBhcnRpY2lvbmVzIDwtIDEwDQpyZXBldGljaW9uZXMgPC0gNQ0KDQpoaXBlcnBhcmFtZXRyb3MgPC0gZXhwYW5kLmdyaWQoc2l6ZSA9IGMoMTAsIDIwLCA1MCwgODAsIDEwMCwgMTIwKSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGVjYXkgPSBjKDAuMDAwMSwgMC4xLCAwLjUpKQ0KDQpzZXQuc2VlZCgxMjMpDQoNCnNlZWRzIDwtIHZlY3Rvcihtb2RlID0gImxpc3QiLCBsZW5ndGggPSAocGFydGljaW9uZXMgKiByZXBldGljaW9uZXMpICsgMSkgDQoNCmZvciAoaSBpbiAxOihwYXJ0aWNpb25lcyAqIHJlcGV0aWNpb25lcykpIHsNCiAgc2VlZHNbW2ldXSA8LSBzYW1wbGUuaW50KDEwMDAsIG5yb3coaGlwZXJwYXJhbWV0cm9zKSkgDQogIH0gDQoNCnNlZWRzW1socGFydGljaW9uZXMgKiByZXBldGljaW9uZXMpICsgMV1dIDwtIHNhbXBsZS5pbnQoMTAwMCwgMSkNCg0KIyBkZWZpbmljaW9uIGRlbCBlbnRyZW5hbWllbnRvDQpjb250cm9sLnRyYWluIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbnVtYmVyID0gcGFydGljaW9uZXMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cyA9IHJlcGV0aWNpb25lcywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWVkcyA9IHNlZWRzLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJldHVyblJlc2FtcCA9ICJmaW5hbCIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmVyYm9zZUl0ZXIgPSBGQUxTRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzUHJvYnMgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxsb3dQYXJhbGxlbCA9IFRSVUUpDQoNCg0KIyBhanVzdGUgZGVsIG1vZGVsbw0Kc2V0LnNlZWQoMzQyKQ0KDQptb2RlbG9fbm5ldF9wcm9icyA8LSB0cmFpbihFeGl0ZWQgfiAuLCANCiAgICAgICAgICAgICAgICAgICAgICBkYXRhID0gY3JlZGl0b3MudHJhaW4ucHJlcCwgDQogICAgICAgICAgICAgICAgICAgICAgbWV0aG9kID0gIm5uZXQiLCANCiAgICAgICAgICAgICAgICAgICAgICB0dW5lR3JpZCA9IGhpcGVycGFyYW1ldHJvcywNCiAgICAgICAgICAgICAgICAgICAgICBtZXRyaWMgPSAiQWNjdXJhY3kiLCANCiAgICAgICAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSBjb250cm9sLnRyYWluLA0KICAgICAgICAgICAgICAgICAgICAgIHJhbmcgPWMoLTAuNywgMC43KSwNCiAgICAgICAgICAgICAgICAgICAgICBNYXhOV3RzID0gMjAwMCwNCiAgICAgICAgICAgICAgICAgICAgICB0cmFjZSA9IEZBTFNFKQ0KDQojIHNlIGd1YXJkYSBlbCBtb2RlbG8NCnNhdmVSRFMobW9kZWxvX25uZXRfcHJvYnMsIGZpbGUgPSAibW9kZWxvLm5uZXQucHJvYnMucmRzIikNCmBgYA0KDQpgYGB7cn0NCm1vZGVsb19ubmV0X3Byb2JzIDwtIHJlYWRSRFMoZmlsZSA9ICJtb2RlbG8ubm5ldC5wcm9icy5yZHMiKQ0KYGBgDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0V9DQojIHNlIGdlbmVyYSB1bmEgY29waWEgZGUgbG9zIGRhdGFzZXRzDQpjLnRyYWluIDwtIGNyZWRpdG9zLnRyYWluLnByZXANCmMudmFsIDwtIGNyZWRpdG9zLnZhbGlkYWNpb24ucHJlcA0KDQpgYGANCg0KYGBge3J9DQojIHNlIG9idGllbmVuIGxhcyBwcm9iYWJpbGlkYWRlcw0KcHJlZGljdC50cmFpbiA8LSBwcmVkaWN0KG1vZGVsb19ubmV0X3Byb2JzLCBjcmVkaXRvcy50cmFpbi5wcmVwLCB0eXBlID0gInByb2IiKVsyXQ0KcHJlZGljdC52YWwgPC0gcHJlZGljdChtb2RlbG9fbm5ldF9wcm9icywgY3JlZGl0b3MudmFsaWRhY2lvbi5wcmVwLCB0eXBlID0gInByb2IiKVsyXQ0KDQojIGp1bnRhciBsYXMgcHJvYmFiaWxpZGFkZXMNCmMudHJhaW4kcHJlZGljdGlvbiA8LSBwcmVkaWN0LnRyYWluJGNodXJuDQpjLnZhbCRwcmVkaWN0aW9uIDwtIHByZWRpY3QudmFsJGNodXJuDQoNCmMudHJhaW4gPC0gYXMuZGF0YS50YWJsZShjLnRyYWluKQ0KYy52YWwgPC0gYXMuZGF0YS50YWJsZShjLnZhbCkNCg0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KRWwgb2JqZXRpdm8gZmluYWwgZGVsIG1vZGVsbyBlcyBjbGFzaWZpY2FyIGEgbG9zIG51ZXZvcyBjbGllbnRlcyBlbiB1bmEgZGUgZG9zIGNsYXNlczogcXVlIHBlcm1hbmV6Y2EgbyBhYmFuZG9uZSwgdmFtb3MgYSBxdWVyZXIgcXVlIGVsIG1vZGVsbyBvdG9yZ3VlIHByb2JhYmlsaWRhZGVzIGFsdGFzIGEgbGFzIG9ic2VydmFjaW9uZXMgcG9zaXRpdmFzIChlbCBjbGllbnRlIGFiYW5kb25lKSB5IHByb2JhYmlsaWRhZGVzIGJhamFzIGEgbGFzIG9ic2VydmFjaW9uZXMgbmVnYXRpdmFzIChjbGllbnRlcyBxdWUgbm8gYWJhbmRvbmVuKS4NCjwvZGl2Pg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0KIyBncsOhZmljYXMgZGUgZGVuc2lkYWRlcw0KZ2dwbG90KGMudHJhaW4sIGFlcyhwcmVkaWN0aW9uLCBjb2xvciA9IGFzLmZhY3RvcihFeGl0ZWQpKSkgKyANCmdlb21fZGVuc2l0eShzaXplID0gMSkgKw0KZ2d0aXRsZSgiUHJvYmFiaWxpZGFkZXMgZW4gZWwgQ29uanVudG8gZGUgRW50cmVuYW1pZW50byIpICsgDQpzY2FsZV9jb2xvcl9lY29ub21pc3QobmFtZSA9ICJDbGFzZSIpDQoNCmBgYA0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkVuIGxhIGdyw6FmaWNhIGFudGVyaW9yLCBzZSBtdWVzdGFuIGxhcyBkZW5zaWRhZGVzIGRlIGFtYmFzIGNsYXNlcywgbGFzIGN1YWxlcyBlc3TDoW4gY2xhcmFtZW50ZSBzZXBhcmFkYXMgZGUgbWFuZXJhIGNvcnJlY3RhOiBsYSBjbGFzZSBuZWdhdGl2YSBhIGxhIGl6cXVpZXJkYSAobm8gYWJhbmRvbmEpIHkgbGEgY2xhc2UgcG9zaXRpdmEgYSBsYSBkZXJlY2hhIChhYmFuZG9uYSkuIExhIGRpdmlzacOzbiBzZSBhcHJlY2lhIGJhc3RhbnRlIGNsYXJhLCBwb3JxdWUgaGF5IHF1ZSByZWNvcmRhciBxdWUgc2UgaGl6byB1biBiYWxhbmNlbyBkZSBjbGFzZXMuDQoNCkN1YW5kbyBsYSB2YXJpYWJsZSBhIHByZWRlY2lyIGVzIGNhdGVnw7NyaWNhLCBsb3MgYWxnb3JpdG1vcyBkZSBjbGFzaWZpY2FjacOzbiBlbiBlbCBmb25kbyBnZW5lcmFuIHByb2JhYmlsaWRhZGVzLCBsYXMgY3VhbGVzLCBjb21vIHNlIG1lbmNpb27DsyBhbnRlcmlvcm1lbnRlLCBzZSBiYXNhbiBlbiB1biBwdW50byBkZSBjb3J0ZSBwYXJhIHJlYWxpemFyIGxhIGFzaWduYWNpw7NuIGRlIGNsYXNlcy4NCg0KVXNhcmVtb3MgdW5hIGZ1bmNpw7NuIHBhcmEgcmVjb3JyZXIgdmFyaW9zIHB1bnRvcyBkZSBjb3J0ZSB5IGNhbGN1bGFyIGxhIHByZWNpc2nDs24gZGVsIG1vZGVsbyB0YW50byBlbiBlbCBjb25qdW50byBkZSBlbnRyZW5hbWllbnRvIGNvbW8gZW4gZWwgY29uanVudG8gZGUgdmFsaWRhY2nDs24uDQo8L2Rpdj4NCg0KYGBge3J9DQpjLnRyYWluJEV4aXRlZCA8LSBpZmVsc2UoYy50cmFpbiRFeGl0ZWQgPT0gJ2NodXJuJywgJzEnLCAnMCcpDQpjLnRyYWluJEV4aXRlZCA8LSBhcy5mYWN0b3IoYy50cmFpbiRFeGl0ZWQpDQoNCmMudmFsJEV4aXRlZCA8LSBpZmVsc2UoYy52YWwkRXhpdGVkID09ICdjaHVybicsICcxJywgJzAnKQ0KYy52YWwkRXhpdGVkIDwtIGFzLmZhY3RvcihjLnZhbCRFeGl0ZWQpDQoNCmFjY3VyYWN5X2luZm8gPC0gQWNjdXJhY3lDdXRvZmZJbmZvKHRyYWluID0gYy50cmFpbiwgdmFsaWQgPSBjLnZhbCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHJlZGljdCA9ICJwcmVkaWN0aW9uIiwgYWN0dWFsID0gIkV4aXRlZCIpDQoNCmFjY3VyYWN5X2luZm8kcGxvdA0KDQpgYGANCg0KU2UgYXByZWNpYSBjb21vIGVuIGVsIGNvbmp1bnRvIGRlIGVudHJlbmFtaWVudG8gZWwgQWNjdXJhY3kgZXMgbWF5b3IgaGFzdGEgY2FzaSBsYSBtaXRhZCBkZWwgcHVudG8gZGUgY29ydGUsIHlhIHF1ZSBhIHBhcnRpciBkZSBlc2Ugbml2ZWwgZWwgQWNjdXJhY3kgZW4gZWwgY29uanVudG8gZGUgdmFsaWRhY2nDs24gbG8gcmViYXNhLiBTZSBpZGVudGlmaWNhcsOhIGN1w6FsIGVzIGVsIHB1bnRvIGRlIGNvcnRlIGRvbmRlIGxhIGRpZmVyZW5jaWEgZXMgbWVub3IgZW50cmUgYW1ib3MgY29uanVudG9zOg0KDQpgYGB7cn0NCmNvcnRlIDwtIGZ1bmN0aW9uKHgpew0KICBkaWZmKHJhbmdlKHhbMV0sIHhbMl0pKQ0KfQ0KDQpSYW5nb3MgPC0gYWNjdXJhY3lfaW5mbyRkYXRhDQoNCkNvcnRlcyA8LSBSYW5nb3MgJT4lIA0KICBtdXRhdGUoUmFuZ28gPSBhcHBseShhY2N1cmFjeV9pbmZvJGRhdGFbLC0xXSwgTUFSR0lOID0gMSwgRlVOID0gY29ydGUpKQ0KDQptaW4oQ29ydGVzJFJhbmdvKQ0Kd2hpY2gubWluKENvcnRlcyRSYW5nbykNCg0KQ29ydGVzW3doaWNoLm1pbihDb3J0ZXMkUmFuZ28pLF0NCg0KY3V0Lm9mZiA8LSBDb3J0ZXNbd2hpY2gubWluKENvcnRlcyRSYW5nbyksXSRjdXRvZmYNCg0KYGBgDQoNCkVuIGVsIHB1bnRvIGRlIGNvcnRlIGVuIGByIENvcnRlc1t3aGljaC5taW4oQ29ydGVzJFJhbmdvKSxdJGN1dG9mZmAgZXMgZW4gZG9uZGUgZWwgY29uanVudG8gZGUgdmFsaWRhY2nDs24gc3VwZXJhIGVsIEFjY3VyYWN5IGRlbCBjb25qdW50byBkZSBlbnRyZW5hbWllbnRvLiBBIGNvbnRpbnVhY2nDs24sIHNlIG11ZXN0cmFuIGxvcyByZXN1bHRhZG9zIGRlIGVsZWdpciBlc2UgcHVudG8gZGUgY29ydGUuDQoNCmBgYHtyfQ0KY21faW5mbyA8LSBDb25mdXNpb25NYXRyaXhJbmZvKCBkYXRhID0gYy50cmFpbiwgcHJlZGljdCA9ICJwcmVkaWN0aW9uIiwgDQoJCQkJCSAJCQlhY3R1YWwgPSAiRXhpdGVkIiwgY3V0b2ZmID0gY3V0Lm9mZikNCg0KY21faW5mbyRwbG90DQoNCmBgYA0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkxhIGdyw6FmaWNhIGFudGVyaW9yLCBtdWVzdHJhIGxhcyBvYnNlcnZhY2lvbmVzIGNvbiBydWlkbyBwYXJhIHBvZGVyIGRpc3Rpbmd1aXJsYXMgbcOhcyBmw6FjaWxtZW50ZS4gU2UgbXVlc3RyYSBsYSBjb21wZW5zYWNpw7NuIGFsIGVsZWdpciBlc2UgcHVudG8gZGUgY29ydGUuDQoNClNpIGF1bWVudGFtb3MgZWwgcHVudG8gZGUgY29ydGUsIGVsIG7Dum1lcm8gZGUgdmVyZGFkZXJvcyBuZWdhdGl2b3MgKFROKSBhdW1lbnRhIHkgZWwgbsO6bWVybyBkZSB2ZXJkYWRlcm9zIHBvc2l0aXZvcyAoVFApIGRpc21pbnV5ZSwgZXMgZGVjaXIsIHNpIGF1bWVudGFtb3MgZWwgdmFsb3IgZGVsIHB1bnRvIGRlIGNvcnRlLCBlbCBuw7ptZXJvIGRlIGZhbHNvcyBwb3NpdGl2b3MgKEZQKSBzZSByZWR1Y2UsIG1pZW50cmFzIHF1ZSBlbCBuw7ptZXJvIGRlIGZhbHNvcyBuZWdhdGl2b3MgKEZOKSBhdW1lbnRhLg0KDQpBbCBlc3RhciBtb2RpZmljYW5kbyBlbCBwdW50byBkZSBjb3J0ZSBzZSBlc3TDoSBlcXVpbGlicmFuZG8gZW50cmUgbGEgdGFzYSBkZSBmYWxzb3MgcG9zaXRpdm9zIHkgbGEgdGFzYSBkZSBmYWxzb3MgbmVnYXRpdm9zIHksIHBhcmEgdHJhdGFyIGRlIG1pbmltaXphciBlbCBuw7ptZXJvIGRlIGVycm9yZXMgcXVlIGVsIG1vZGVsbyBlc3TDoSBjb21ldGllbmRvLCBzZSBzdWVsZW4gYXNpZ25hciBjb3N0b3MuIExhIGN1cnZhICoqUk9DKiogc2UgdXRpbGl6YSBwYXJhIHZpc3VhbGl6YXIgeSBjdWFudGlmaWNhciBsYSBjb21wZW5zYWNpw7NuIHF1ZSBlc3RhbW9zIGhhY2llbmRvIGVudHJlIGxhcyBkb3MgbWVkaWRhcy4NCg0KRXN0YSBjdXJ2YSBzZSBjcmVhIGNvbiBsYSB0YXNhIGRlIHZlcmRhZGVyb3MgcG9zaXRpdm9zIChUUFIpIHkgbGEgdGFzYSBkZSBmYWxzb3MgcG9zaXRpdm9zIChGUFIpLCBlbiB2YXJpb3MgY29ydGVzIGVudHJlIDAgeSAxLg0KDQpDb21vIGVsIG9iamV0aXZvIGVzIHF1ZSB0ZW5nYW1vcyBwb2NvcyBmYWxzb3MgbmVnYXRpdm9zLCBlcyBkZWNpciwgcG9jb3MgZXJyb3JlcyBlbiBsb3MgcXVlIGVsIG1vZGVsbyBwcmVkaWNlIHF1ZSB1biBjbGllbnRlIHBlcm1hbmVjZSBjdWFuZG8gZW4gcmVhbGlkYWQgc2UgZnVlLCBzZSBhc2lnbmFyw6EgdW4gY29zdG8gbWF5b3IgKGVsIGRvYmxlKSBjb24gcmVzcGVjdG8gYSBsb3MgZmFsc29zIHBvc2l0aXZvcy4NCjwvZGl2Pg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0KY29zdF9mcCA8LSA1MA0KY29zdF9mbiA8LSAxMDANCg0Kcm9jX2luZm8gPC0gUk9DSW5mbyhkYXRhID0gY21faW5mbyRkYXRhLCBwcmVkaWN0ID0gInByZWRpY3QiLCANCiAgICAgICAgICAgICAgICAgICAgIGFjdHVhbCA9ICJhY3R1YWwiLCBjb3N0LmZwID0gY29zdF9mcCwgY29zdC5mbiA9IGNvc3RfZm4pDQoNCmdyaWQuZHJhdyhyb2NfaW5mbyRwbG90KQ0KDQpgYGANCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpBbCBvYnNlcnZhciBsYXMgZ3LDoWZpY2FzIHNlIHB1ZWRlIGNvbmNsdWlyIGxvIHNpZ3VpZW50ZToNCg0KKiBBbCBhc2lnbmFyIHVuIGNvc3RvIGEgdW4gZmFsc28gcG9zaXRpdm8geSBhIHVuIGZhbHNvIG5lZ2F0aXZvIGRlIDUwIHkgMTAwLCByZXNwZWN0aXZhbWVudGUsIGVsIHB1bnRvIGRlIGNvcnRlIMOzcHRpbW8gZXMgZGUgMC4zMyB5IGVsIGNvc3RvIHRvdGFsIGRlIGVsZWdpciBlc2Ugbml2ZWwgZXMgZGUgMTAyLDUwMC4NCiogTGEgY3VydmEgUk9DIG11ZXN0cmEgbGEgY29tcGVuc2FjacOzbiBlbnRyZSBsYSB0YXNhIGEgbGEgcXVlIHB1ZWRlIHByZWRlY2lyIGNvcnJlY3RhbWVudGUgYWxnbyBjb24gbGEgdGFzYSBkZSBwcmVkaWNjacOzbiBpbmNvcnJlY3RhIGFsIGVsZWdpciBkaWZlcmVudGVzIHB1bnRvcyBkZSBjb3J0ZS4gRWwgw6FyZWEgYmFqbyBsYSBjdXJ2YSBST0MgKEFVQykgZXMgZGUgMC44NjcsIGN1YW50byBtw6FzIGFsdG8gZXMgZXN0ZSByZXN1bHRhZG8gbWVqb3Igc2Vyw6EgZWwgbW9kZWxvLg0KKiBFbCBncsOhZmljbyBkZSBjb3N0b3MgY2FsY3VsYSBlbCBjb3N0byBhc29jaWFkbyBhbCBlbGVnaXIgZGlmZXJlbnRlcyBwdW50b3MgZGUgY29ydGUuDQoqIFBhcmEgYW1iYXMgZ3LDoWZpY2FzLCBsYSBsw61uZWEgcHVudGVhZGEgaW5kaWNhIGTDs25kZSBzZSBlbmN1ZW50cmEgZXNlIHB1bnRvIGRlIGNvcnRlIMOzcHRpbW8sIHBhcmEgbGEgZ3LDoWZpY2EgZGUgY29zdG9zLCBtdWVzdHJhIGVsIHZhbG9yIGRlIGNvcnRlIMOzcHRpbW8uIEVuIGN1YW50byBhIGxhIGdyw6FmaWNhIGRlIGxhIGN1cnZhIFJPQywgZXN0YSBpbmRpY2EgbGEgdWJpY2FjacOzbiBkZSBsYSB0YXNhIGRlIGZhbHNvcyBwb3NpdGl2b3MgKEZQUikgeSBsYSB0YXNhIGRlIHZlcmRhZGVyb3MgcG9zaXRpdm9zIChUUFIpIGNvcnJlc3BvbmRpZW50ZXMgYWwgdmFsb3IgZGUgY29ydGUgw7NwdGltby4gRWwgY29sb3IgZW4gbGFzIGN1cnZhcyBkZW5vdGEgZWwgY29zdG8gYXNvY2lhZG8gY29uIGVzZSBwdW50bywgIm3DoXMgdmVyZGUiIHNpZ25pZmljYSBxdWUgZWwgY29zdG8gZXMgbWVub3IsIG1pZW50cmFzIHF1ZSAibcOhcyBuZWdybyIgc2lnbmlmaWNhIGxvIGNvbnRyYXJpby4NCg0KVm9sdmVtb3MgYSBnZW5lcmFyIGxhIG1hdHJpeiBkZSBjb25mdXNpw7NuIGNvbW8gZ3LDoWZpY28gZGUgZGlzcGVyc2nDs24gY29uIGVsIHB1bnRvIGRlIGNvcnRlIMOzcHRpbW8gcGFyYSB2ZXIgbG9zIHJlc3VsdGFkb3Mgb2J0ZW5pZG9zLg0KPC9kaXY+DQoNCmBgYHtyfQ0KY21faW5mbyA8LSBDb25mdXNpb25NYXRyaXhJbmZvKCBkYXRhID0gYy50cmFpbiwgcHJlZGljdCA9ICJwcmVkaWN0aW9uIiwgDQoJCQkJCSAJCQlhY3R1YWwgPSAiRXhpdGVkIiwgY3V0b2ZmID0gcm9jX2luZm8kY3V0b2ZmKQ0KDQpjbV9pbmZvJHBsb3QNCg0KYGBgDQoNCkVsIGdyw6FmaWNvIGRlIGxhIG1hdHJpeiBkZSBjb25mdXNpw7NuIG11ZXN0cmEgY2xhcmFtZW50ZSBxdWUgYWwgY2FtYmlhciBlbCB2YWxvciBkZSBjb3J0ZSBhIDAuMzMgZWwgbW9kZWxvIGRlIGNsYXNpZmljYWNpw7NuIGVzdMOhIGNvbWV0aWVuZG8gbWVub3MgZXJyb3IgZmFsc28gbmVnYXRpdm8gKEZOKSwgeWEgcXVlIGVsIGNvc3RvIGFzb2NpYWRvIGNvbiDDqWwgZXMgMiB2ZWNlcyBtYXlvciBxdWUgdW4gZmFsc28gcG9zaXRpdm8gKEZQKS4NCg0KQWhvcmEsIHZlYW1vcyBsYSBjdXJ2YSBST0MNCg0KYGBge3IgbWVzc2FnZT1GQUxTRX0NCnByZWQgPC0gcHJlZGljdGlvbihjLnRyYWluJHByZWRpY3Rpb24sIGMudHJhaW4kRXhpdGVkKQ0KcGVyZiA8LSBwZXJmb3JtYW5jZShwcmVkLCAidHByIiwgImZwciIpDQoNCnBsb3QocGVyZiwgY29sb3JpemUgPSBUUlVFLCBtYWluID0gIkN1cnZhIFJPQyIsIHhsYWIgPSAiVGFzYSBkZSBmYWxzb3MgcG9zaXRpdm9zIiwNCiAgICAgeWxhYiA9ICJUYXNhIGRlIHZlcmRhZGVyb3MgcG9zaXRpdm9zIikNCmFibGluZShhID0gMCwgYiA9IDEsIGNvbCA9ICJibHVlIiwgbHR5ID0gMikNCmdyaWQoKQ0KYXVjIDwtIGFzLm51bWVyaWMocGVyZm9ybWFuY2UocHJlZCwiYXVjIilAeS52YWx1ZXMpDQpsZWdlbmQoImJvdHRvbXJpZ2h0IiwgbGVnZW5kID0gcGFzdGUoIiBBVUMgPSIsIHJvdW5kKGF1Yyw0KSkpDQoNCmBgYA0KDQpQb2RlbW9zLCB0YW1iacOpbiwgZ2VuZXJhciBsb3MgcmVzdWx0YWRvcyBwYXJhIGNhZGEgcHVudG8gZGUgY29ydGU6DQoNCmBgYHtyfQ0KY3V0b2ZmcyA8LSBkYXRhLmZyYW1lKGN1dCA9IHBlcmZAYWxwaGEudmFsdWVzW1sxXV0sIGZwciA9IHBlcmZAeC52YWx1ZXNbWzFdXSwgDQogICAgICAgICAgICAgICAgICAgICAgdHByID0gcGVyZkB5LnZhbHVlc1tbMV1dKQ0KaGVhZChjdXRvZmZzKQ0KDQoNCmN1dG9mZnMgPC0gY3V0b2Zmc1tvcmRlcihjdXRvZmZzJHRwciwgZGVjcmVhc2luZyA9IFRSVUUpLF0NCmhlYWQoc3Vic2V0KGN1dG9mZnMsIGZwciA8IDAuMzMpKQ0KDQpgYGANCg0KPGJyPg0KDQojICA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPkV2YWx1YWNpw7NuIGZpbmFsOiBkYXRvcyBkZSB0ZXN0PC9zcGFuPg0KDQpGaW5hbG1lbnRlLCB2YW1vcyBhIHJldmlzYXIgZWwgY29uanVudG8gZGUgdGVzdCBwYXJhIGNvbm9jZXIgbG9zIHJlc3VsdGFkb3MgY29uIGVsIHB1bnRvIGRlIGNvcnRlIMOzcHRpbW8uDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0V9DQpwcmVkaWN0LnRlc3QgPC0gcHJlZGljdChtb2RlbG9fbm5ldF9wcm9icywgY3JlZGl0b3MudGVzdC5wcmVwLCB0eXBlID0gInByb2IiKVsyXSAlPiUgDQogIGFzLmRhdGEuZnJhbWUoKQ0KDQpjcmVkaXRvcy50ZXN0LnByZXAkcHJlZGljdCA8LSBwcmVkaWN0LnRlc3QkY2h1cm4NCg0KdGVzdC5maW5hbCA8LSBjcmVkaXRvcy50ZXN0LnByZXAgJT4lIA0KICBtdXRhdGUocHJlZGljY2lvbiA9IGlmX2Vsc2UocHJlZGljdCA+PSByb2NfaW5mbyRjdXRvZmYsICJjaHVybiIsICJzdGF5IikpDQoNCnRlc3QuZmluYWwkcHJlZGljY2lvbiA8LSBhcy5mYWN0b3IodGVzdC5maW5hbCRwcmVkaWNjaW9uKQ0KDQpjbS5maW5hbCA8LSBjb25mdXNpb25NYXRyaXgodGVzdC5maW5hbCRwcmVkaWNjaW9uLCBjcmVkaXRvcy50ZXN0LnByZXAkRXhpdGVkLCBwb3NpdGl2ZSA9ICJjaHVybiIpDQoNCmRyYXdfY29uZnVzaW9uX21hdHJpeChjbS5maW5hbCkNCg0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KRGVzcHXDqXMgZGUgZ2VuZXJhciBsYXMgcHJvYmFiaWxpZGFkZXMgeSBkZSBjb25zaWRlcmFyIGVsIHB1bnRvIGRlIGNvcnRlIMOzcHRpbW8gcmVzdWx0YWRvIGRlIGFwbGljYXJsZSBjb3N0b3MgYSBsb3MgZXJyb3Jlcywgc2Ugb2J0dXZvIHVuYSAqKlNlbnNpYmlsaWRhZCBkZSBgciBjbS5maW5hbFtbImJ5Q2xhc3MiXV1bWyJTZW5zaXRpdml0eSJdXWAqKiwgYWRlbcOhcywgc29sbyBzZSBvYnR1dmllcm9uICoqYHIgY20uZmluYWxbWyJ0YWJsZSJdXVsxLDJdYCBmYWxzb3MgbmVnYXRpdm9zKiosIGVzIGRlY2lyLCBjbGllbnRlcyBxdWUgZWwgbW9kZWxvIHByZWRpam8gcXVlIG5vIHNlIGliYW4gYSBpciBkZWwgYmFuY28geSBlbiByZWFsaWRhZCBzZSBmdWVyb24uIFNlIGxvZ3LDsyBtaW5pbWl6YXIgZXN0ZSBkYXRvLCBzaW4gZW1iYXJnbywgZnVlIGFjb3N0YSBkZSBhdW1lbnRhciBsb3MgZmFsc29zIHBvc2l0aXZvcywgeWEgcXVlIMOpc3RvcyBhdW1lbnRhcm9uLCBwZXJvIGVzIHByZWZlcmlibGUgcGFyYSBlc3RlIGNhc28uDQo8L2Rpdj4NCg0KPGJyPg0KDQojICA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPklkZW50aWZpY2FuZG8gZWwgdGllbXBvIGRlIGFiYW5kb25vIGRlbCBjbGllbnRlPC9zcGFuPg0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCllhIHBhcmEgdGVybWluYXIgY29uIGVsIHByZXNlbnRlIHByb3llY3RvLCB2YW1vcyBhIGlkZW50aWZpY2FyIGVsIHRpZW1wbyBlbiBlbCBxdWUgbG9zIGNsaWVudGVzIGFiYW5kb25hbiBlbCBiYW5jbywgc2l0dWFjacOzbiBxdWUgZXMgZGUgc3VtYSBpbXBvcnRhbmNpYSBwYXJhIHRyYXRhciBkZSBkZXRlY3RhciBlbCBtb21lbnRvIGVuIGVsIHF1ZSBwb2RlbW9zIGltcGxlbWVudGFyIGVzdHJhdGVnaWFzIHBhcmEgZXZpdGFyIHN1IGFiYW5kb25vLg0KDQpBcGxpY2FyZW1vcyB1biAqKmFuw6FsaXNpcyBkZSBzdXBlcnZpdmVuY2lhKiosIHlhIHF1ZSBlcyB1biBjb25qdW50byBkZSBtw6l0b2RvcyBlc3RhZMOtc3RpY29zIHV0aWxpemFkb3MgcGFyYSBjb25vY2VyIGVsIHRpZW1wbyBxdWUgdGFyZGEgZW4gb2N1cnJpciB1biBldmVudG8gcXVlLCBlbiBlc3RlIGNhc28sIHNlcsOhIGVsIGFiYW5kb25vIGRlIGNsaWVudGVzLg0KDQpQYXJhIGVzdG8gdmFtb3MgYSB1dGlsaXphciBsYSBjb3BpYSBkZSBsYSBiYXNlIG9yaWdpbmFsIHF1ZSBzZSBnZW5lcsOzIGFsIGluaWNpbyBkZWwgYW7DoWxpc2lzLCB5YSBxdWUgaGF5IHF1ZSByZWNvcmRhciBxdWUgbGEgYmFzZSBxdWUgc2UgaGEgZXN0YWRvIHRyYWJhamFkbyBoYSBzdWZyaWRvIG1vZGlmaWNhY2lvbmVzIHkgZWxpbWluYWNpb25lcyBkZSB2YXJpYWJsZXMgbm8gc2lnbmlmaWNhdGl2YXMgcGFyYSBsb3MgYWxnb3JpdG1vcyB1dGlsaXphZG9zLg0KPC9kaXY+DQoNCmBgYHtyfQ0KY3JlZGl0b3MucmF3JHN0YXR1cyA8LSBpZmVsc2UoY3JlZGl0b3MucmF3JEV4aXRlZCA9PSAxLCJDaHVybnMiLCJTdGF5cyIpDQpjcmVkaXRvcy5yYXckc3RhdHVzIDwtIGFzLmZhY3RvcihjcmVkaXRvcy5yYXckc3RhdHVzKQ0KDQpgYGANCg0KVmVhbW9zIGVuIHVuYSB0YWJsYSBsYXMgdmFyaWFibGVzIGRlIGludGVyw6lzOiAqKlRlbnVyZSoqIHkgKipFeGl0ZWQqKiwgY29uIGVsIHByb3DDs3N0byBkZSBpZGVudGlmaWNhciBsYXMgZGlzdHJpYnVjaW9uZXM6DQoNCmBgYHtyfQ0KdGFiX3h0YWIoY3JlZGl0b3MucmF3JFRlbnVyZSwgY3JlZGl0b3MucmF3JHN0YXR1cywgbWFyZ2luID0gInJvdyIsIGJhci5wb3MgPSAic3RhY2siLA0Kc2hvdy5zdW1tYXJ5ID0gRikNCg0KYGBgDQoNCjxicj4NCg0KUGFyYSBmYWNpbGl0YXIgbGEgaWRlbnRpZmljYWNpw7NuIGRlIGxhcyBkaXN0cmlidWNpb25lcyBlbnRyZSBsb3MgY2xpZW50ZXMgcXVlIGFiYW5kb25hcm9uIGVsIGJhbmNvIHkgbG9zIHF1ZSBhw7puIHBlcm1hbmVjZW4sIHNlIGhhcsOhIHVuYSBncsOhZmljYSBkZSBiYXJyYXMgYXBpbGFkYXMgY29uIHN1cyByZXNwZWN0aXZhcyBwcm9wb3JjaW9uZXMgZW4gcG9yY2VudGFqZXMgeSBlbiBuaXZlbDoNCg0KYGBge3J9DQpwbG90X3h0YWIoY3JlZGl0b3MucmF3JFRlbnVyZSwgY3JlZGl0b3MucmF3JHN0YXR1cywgbWFyZ2luID0gInJvdyIsIGJhci5wb3MgPSAic3RhY2siLA0Kc2hvdy5zdW1tYXJ5ID0gRiwgY29vcmQuZmxpcCA9IFQsDQpheGlzLnRpdGxlcyA9ICIiLCBsZWdlbmQudGl0bGUgPSAiU3RhdHVzIiwNCmdlb20uY29sb3JzID0gYygiI2YzYzZmMyIsImxpZ2h0Ymx1ZSIpKSArDQogIHRoZW1lX21pbmltYWwoKSArDQogICAgICAgICAgdGhlbWUodGV4dCA9IGVsZW1lbnRfdGV4dChzaXplID0gMTIpLCANCiAgICAgICAgICBwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41KSwgdGl0bGUgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDEyKSkgKw0KICBsYWJzKHRpdGxlID0gIlByb3BvcmNpb25lcyBkZSBBYmFuZG9ubyBwb3IgVGllbXBvIGRlIFBlcm1hbmVuY2lhIiwNCiAgICAgICB4ID0gIlBlcm1hbmVuY2lhIChhw7FvcykiLCANCiAgICAgICB5ID0gIlBvcmNlbnRhamUgJSIpDQoNCmBgYA0KDQo8ZGl2IGNsYXNzPXRleHQtanVzdGlmeT4NCkVuIGxhIGdyw6FmaWNhIGFudGVyaW9yLCBzZSBhcHJlY2lhIGNsYXJhbWVudGUgY8OzbW8gZXMgZW4gbG9zIGRvcyBwcmltZXJvcyBhw7FvcyBkb25kZSBlbCBwb3JjZW50YWplIGRlIGFiYW5kb25vIGVzIGxpZ2VyYW1lbnRlIG1heW9yIGNvbiByZXNwZWN0byBhbCByZXN0byBkZWwgcGVyaW9kby4gQWRlbcOhcywgc2UgcHVlZGUgaWRlbnRpZmljYXIgcXVlIG5vIHNlIGFwcmVjaWEgdW5hIHRlbmRlbmNpYSBkZWZpbmlkYSBjb24gZWwgcGFzbyBkZWwgdGllbXBvLg0KPC9kaXY+DQoNCjxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoMjA0LCAxNTMsIDApIj5Fc3RpbWFjacOzbiBkZWwgdGllbXBvIGRlIHN1cGVydml2ZW5jaWEgbyBwZXJtYW5lbmNpYSBlbiBlbCBiYW5jbzwvc3Bhbj4NCg0KYGBge3J9DQpmaXRzdiA8LSBzdXJ2Zml0KFN1cnYoY3JlZGl0b3MucmF3JFRlbnVyZSwgY3JlZGl0b3MucmF3JEV4aXRlZCkgfiAxLCANCiAgICAgICAgICAgICAgICAgdHlwZSA9ICJrYXBsYW4tbWVpZXIiKQ0KZml0c3YNCg0KYGBgDQoNCjxkaXYgY2xhc3M9dGV4dC1qdXN0aWZ5Pg0KQSBwYXJ0aXIgZGUgbGEgc2FsaWRhIGFudGVyaW9yLCBzZSBwdWVkZSB2ZXIgcXVlIGRlIGxvcyAxMCwwMDAgY2xpZW50ZXMgYW5hbGl6YWRvcywgZnVlcm9uIDIsMDM3IGxvcyBxdWUgYWJhbmRvbmFyb24gZWwgYmFuY28gZHVyYW50ZSBlbCBwZXJpb2RvLiBMYSBtZWRpYW5hIGRlbCB0aWVtcG8gZGUgc3VwZXJ2aXZlbmNpYSBlcyBkZSAxMCBhw7FvcywgbG8gcXVlIHNpZ25pZmljYSBxdWUgYWxyZWRlZG9yIGRlbCA1MCUgZGUgbG9zIGNsaWVudGVzIG5vIGFiYW5kb25hbiBhbnRlcyBkZSBhbGNhbnphciB1bmEgZHVyYWNpw7NuIGRlIHBlcm1hbmVuY2lhIGRlIDEwIGHDsW9zLg0KPC9kaXY+DQoNClZlYW1vcyBsbyBxdWUgc3VjZWRlIHBvciBnw6luZXJvOg0KDQpgYGB7cn0NCmZpdHN2MSA8LSBzdXJ2Zml0KFN1cnYoVGVudXJlLCBFeGl0ZWQpIH4gR2VuZGVyLCBkYXRhID0gY3JlZGl0b3MucmF3KQ0KZml0c3YxDQoNCmBgYA0KDQpFbCByZXN1bWVuIGFudGVyaW9yIG11ZXN0cmEgcXVlIHBhcmEgZWwgZ3J1cG8gZGUgbGFzIG11amVyZXMgZWwgMjUlIG5vIGFiYW5kb27DsyBlbCBiYW5jbyBoYXN0YSBsb3MgMTAgYcOxb3MsIGFwcm94aW1hZGFtZW50ZS4gUGFyYSBlbCBncnVwbyBkZSBsb3MgaG9tYnJlcyBsYSBtZWRpYW5hIGRlIHN1cGVydml2ZW5jaWEgZnVlIG1heW9yIChwb3IgZXNvIGFwYXJlY2UgTkEpLg0KDQpgYGB7cn0NCmdnc3VydnBsb3QoZml0c3YxLA0KICAgICAgICAgICBwdmFsID0gVFJVRSwgY29uZi5pbnQgPSBUUlVFLA0KICAgICAgICAgICByaXNrLnRhYmxlLmNvbCA9ICJzdHJhdGEiLA0KICAgICAgICAgICBsaW5ldHlwZSA9ICJzdHJhdGEiLA0KICAgICAgICAgICBnZ3RoZW1lID0gdGhlbWVfYncoKSwNCiAgICAgICAgICAgcGFsZXR0ZSA9IGMoIiNFN0I4MDAiLCAiIzJFOUZERiIpKQ0KDQpgYGANCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQpFbiBsYSBncsOhZmljYSBhbnRlcmlvciwgc2UgYXByZWNpYSBjbGFyYW1lbnRlIGNvbW8gbGEgcHJvYmFiaWxpZGFkIGRlIHN1cGVydml2ZW5jaWEgZXMgbWF5b3IgcGFyYSBsb3MgaG9tYnJlcyByZXNwZWN0byBhbCBncnVwbyBkZSBsYXMgbXVqZXJlcy4gUG9yIGVqZW1wbG8sIGVuIGVsIHRpZW1wbyA1LCBsYSBwcm9iYWJpbGlkYWQgcGFyYSBsb3MgaG9tYnJlcyBlcyBkZSAwLjg4MiwgbWllbnRyYXMgcXVlIHBhcmEgbGFzIG11amVyZXMgZXMgZGUgMC44MjExLCBhcHJveGltYWRhbWVudGUuIEVsIHAtdmFsdWUgYWwgc2VyIG1lbm9yIGFsIDAuMDUgbm9zIGluZGljYSBxdWUgaGF5IGRpZmVyZW5jaWFzIHNpZ25pZmljYXRpdmFzIGVudHJlIGFtYm9zIGdydXBvcy4gTG8gYW50ZXJpb3IsIHRhbWJpw6luIHNlIGNvbmZpcm1hIGNvbiBsYSBwcnVlYmEgTG9nLVJhbmssIHF1ZSBjb21wYXJhIGN1cnZhcyBkZSBzdXBlcnZpdmVuY2lhIHkgY3V5YSBoaXDDs3Rlc2lzIG51bGEgZXMgcXVlIG5vIGhheSBkaWZlcmVuY2lhIGVuIGxhIHN1cGVydml2ZW5jaWEgZW50cmUgbG9zIGdydXBvczoNCjwvZGl2Pg0KDQpgYGB7cn0NCnN1cnZkaWZmKFN1cnYoVGVudXJlLCBFeGl0ZWQpIH4gR2VuZGVyLCBkYXRhID0gY3JlZGl0b3MucmF3KQ0KDQpgYGANCg0KYGBge3J9DQpyZXMgPC0gZGF0YS5mcmFtZSh0aW1lID0gZml0c3YxJHRpbWUsDQogICAgICAgICAgICAgICAgbi5yaXNrID0gZml0c3YxJG4ucmlzaywNCiAgICAgICAgICAgICAgICBuLmV2ZW50ID0gZml0c3YxJG4uZXZlbnQsDQogICAgICAgICAgICAgICAgbi5jZW5zb3IgPSBmaXRzdjEkbi5jZW5zb3IsDQogICAgICAgICAgICAgICAgc3VydiA9IGZpdHN2MSRzdXJ2LA0KICAgICAgICAgICAgICAgIHVwcGVyID0gZml0c3YxJHVwcGVyLA0KICAgICAgICAgICAgICAgIGxvd2VyID0gZml0c3YxJGxvd2VyDQopDQoNCnJlcyAlPiUgDQogIGZpbHRlcih0aW1lID09IDUpDQoNCmBgYA0KDQpFc3RpbWFjacOzbiBkZWwgdGllbXBvIGRlIHN1cGVydml2ZW5jaWEgcG9yIFBhw61zDQoNCmBgYHtyfQ0KZml0c3YyIDwtIHN1cnZmaXQoU3VydihUZW51cmUsIEV4aXRlZCkgfiBHZW9ncmFwaHksIA0KICAgICAgICAgICAgICAgICAgICBkYXRhID0gY3JlZGl0b3MucmF3KQ0KZml0c3YyDQoNCmBgYA0KDQpgYGB7cn0NCmdnc3VydnBsb3QoZml0c3YyLA0KICAgICAgICAgICBwdmFsID0gVFJVRSwgY29uZi5pbnQgPSBUUlVFLA0KICAgICAgICAgICByaXNrLnRhYmxlLmNvbCA9ICJzdHJhdGEiLA0KICAgICAgICAgICBsaW5ldHlwZSA9ICJzdHJhdGEiLA0KICAgICAgICAgICBzdXJ2Lm1lZGlhbi5saW5lID0gImh2IiwNCiAgICAgICAgICAgZ2d0aGVtZSA9IHRoZW1lX2J3KCksDQogICAgICAgICAgIHBhbGV0dGUgPSBjKCIjRTdCODAwIiwgIiMyRTlGREYiLCAiZGFya3JlZCIpKQ0KDQpgYGANCg0KRW4gbGEgc3VwZXJ2aXZlbmNpYSBwb3IgZ3J1cG8gZ2VvZ3LDoWZpY28sIGNhc2kgbGEgdGVyY2VyYSBwYXJ0ZSBkZSBsb3MgY2xpZW50ZXMgZGUgQWxlbWFuaWEgbm8gYWJhbmRvbmFuIGhhc3RhIGxvcyA5IGHDsW9zIGRlIGR1cmFjacOzbi4gU2luIGVtYmFyZ28sIGxvcyBjbGllbnRlcyBkZSBGcmFuY2lhIHkgRXNwYcOxYSB0aWVuZW4gdW5hIG1lZGlhbmEgZGUgc3VwZXJ2aXZlbmNpYSBzdXBlcmlvciBhIGxvcyAxMCBhw7FvcyAocG9yIGVzbyBoYXkgTkEpLg0KDQpTdSByZXNwZWN0aXZhIGdyw6FmaWNhIG11ZXN0cmEgY8OzbW8gbG9zIGFsZW1hbmVzIHRpZW5lbiBwcm9iYWJpbGlkYWRlcyBtw6FzIGJhamFzIGNvbiByZXNwZWN0byBhIGxvcyBjbGllbnRlcyBkZSBsb3Mgb3Ryb3MgZG9zIHBhw61zZXMuIEFxdcOtIHRhbWJpZW4gZXhpc3RlbiBkaWZlcmVuY2lhcyBzaWduaWZpY2F0aXZhcy4NCg0KYGBge3J9DQpzdXJ2ZGlmZihTdXJ2KFRlbnVyZSwgRXhpdGVkKSB+IEdlb2dyYXBoeSwgZGF0YSA9IGNyZWRpdG9zLnJhdykNCg0KYGBgDQoNCkVzdGltYWNpw7NuIGRlbCB0aWVtcG8gZGUgc3VwZXJ2aXZlbmNpYSBwb3IgdGlwbyBkZSBtaWVtYnJvIGRlbCBzaXN0ZW1hIGJhbmNhcmlvDQoNCmBgYHtyfQ0KZml0c3YzIDwtIHN1cnZmaXQoU3VydihUZW51cmUsIEV4aXRlZCkgfiBJc0FjdGl2ZU1lbWJlciwgDQogICAgICAgICAgICAgICAgICAgIGRhdGEgPSBjcmVkaXRvcy5yYXcpDQpmaXRzdjMNCg0KYGBgDQoNCkRlIG1hbmVyYSBzaW1pbGFyLCBzZSBwdWVkZSBvYnNlcnZhciBxdWUgZW50cmUgbG9zIGNsaWVudGVzIHF1ZSBubyBzb24gbWllbWJyb3MgYWN0aXZvcyBkZWwgc2lzdGVtYSBiYW5jYXJpbywgZWwgdGllbXBvIG1lZGlvIGRlIHN1cGVydml2ZW5jaWEgZXMgZGUgMTAgYcOxb3MsIG1pZW50cmFzIHF1ZSBwYXJhIGxvcyBxdWUgc29uIG1pZW1icm9zIGFjdGl2b3MsIGVsIHRpZW1wbyBtZWRpbyBkZSBzdXBlcnZpdmVuY2lhIGVzIHN1cGVyaW9yIGEgMTAuDQoNCmBgYHtyfQ0KZ2dzdXJ2cGxvdChmaXRzdjMsDQogICAgICAgICAgIHB2YWwgPSBUUlVFLCBjb25mLmludCA9IFRSVUUsDQogICAgICAgICAgIHJpc2sudGFibGUuY29sID0gInN0cmF0YSIsDQogICAgICAgICAgIGxpbmV0eXBlID0gInN0cmF0YSIsDQogICAgICAgICAgIGdndGhlbWUgPSB0aGVtZV9idygpLA0KICAgICAgICAgICBwYWxldHRlID0gYygiI0U3QjgwMCIsICIjMkU5RkRGIikpDQoNCmBgYA0KDQpNdWVzdHJhIGxhIG1pc21hIHRlbmRlbmNpYSBxdWUgZWwgYW7DoWxpc2lzIHBvciBnw6luZXJvIHksIHRhbWJpw6luLCBoYXkgZGlmZXJlbmNpYXMgc2lnbmlmaWNhdGl2YXMgZW50cmUgZXN0YXIgYWN0aXZvIG8gbm8gZGVudHJvIGRlbCBzaXN0ZW1hIGJhbmNhcmlvLg0KDQpgYGB7cn0NCnN1cnZkaWZmKFN1cnYoVGVudXJlLCBFeGl0ZWQpIH4gSXNBY3RpdmVNZW1iZXIsIGRhdGEgPSBjcmVkaXRvcy5yYXcpDQpgYGANCg0KPGJyPg0KDQpWYW1vcyBhIHByZWRlY2lyIGFsZ3VuYXMgcHJvYmFiaWxpZGFkZXMgZGUgc3VwZXJ2aXZlbmNpYToNCg0KKiBIb21icmUgZW4gZWwgcGVyaW9kbyA3Og0KDQpgYGB7cn0NCnRpbWUxIDwtIDcNCg0KIyBTdXBlcnZpdmVuY2lhDQpwcmVkaWN0U3VydlByb2IoZml0c3YxLCBuZXdkYXRhID0gZGF0YS5mcmFtZShHZW5kZXIgPSAiTWFsZSIpLCB0aW1lID0gdGltZTEpDQoNCiMgTm8gU3VwZXJ2aXZlbmNpYQ0KMS1wcmVkaWN0U3VydlByb2IoZml0c3YxLCBuZXdkYXRhID0gZGF0YS5mcmFtZShHZW5kZXIgPSAiTWFsZSIpLCB0aW1lID0gdGltZTEpDQoNCmBgYA0KDQo8YnI+DQoNCiogTXVqZXIgZW4gZWwgcGVyaW9kbyA3Og0KDQpgYGB7cn0NCnRpbWUxIDwtIDcNCg0KIyBTdXBlcnZpdmVuY2lhDQpwcmVkaWN0U3VydlByb2IoZml0c3YxLCBuZXdkYXRhID0gZGF0YS5mcmFtZShHZW5kZXIgPSAiRmVtYWxlIiksIHRpbWUgPSB0aW1lMSkNCg0KIyBObyBTdXBlcnZpdmVuY2lhDQoxLXByZWRpY3RTdXJ2UHJvYihmaXRzdjEsIG5ld2RhdGEgPSBkYXRhLmZyYW1lKEdlbmRlciA9ICJGZW1hbGUiKSwgdGltZSA9IHRpbWUxKQ0KDQpgYGANCg0KPGJyPg0KDQoqIEhvbWJyZSBkZSBBbGVtYW5pYSBlbiBlbCBwZXJpb2RvIDg6DQoNCmBgYHtyfQ0KdGltZTEgPC0gOA0KDQojIFN1cGVydml2ZW5jaWENCnByZWRpY3RTdXJ2UHJvYihmaXRzdjEsIG5ld2RhdGEgPSBkYXRhLmZyYW1lKEdlbmRlciA9ICJNYWxlIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEdlb2dyYXBoeSA9ICJHZXJtYW55IiksIHRpbWUgPSB0aW1lMSkNCg0KIyBObyBTdXBlcnZpdmVuY2lhDQoxLXByZWRpY3RTdXJ2UHJvYihmaXRzdjEsIG5ld2RhdGEgPSBkYXRhLmZyYW1lKEdlbmRlciA9ICJNYWxlIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEdlb2dyYXBoeSA9ICJHZXJtYW55IiksIHRpbWUgPSB0aW1lMSkNCg0KDQpgYGANCg0KPGJyPg0KDQojICA8c3BhbiBzdHlsZT0iY29sb3I6cmdiKDAsIDAsIDIwNSkiPkNvbmNsdXNpb25lczwvc3Bhbj4NCg0KPGRpdiBjbGFzcz10ZXh0LWp1c3RpZnk+DQoNClNlIHB1ZG8gY29tcHJvYmFyIHF1ZSBwYXJhIHByZWRlY2lyIGVsIGFiYW5kb25vIGRlIGNsaWVudGVzIGVsIG1vZGVsbyBxdWUgbWVqb3JlcyByZXN1bHRhZG9zIG9idHV2byBmdWUgbGEgcmVkIG5ldXJvbmFsLCBzaW4gZW1iYXJnbywgYWwgaGFjZXIgZWwgZW5zYW1ibGUgY29uIGxvcyBtb2RlbG9zIFNWTSwgWEdCIHkgUkYgc2Ugb2J0dXZvIHVuYSBtZWpvcmEgbWFyZ2luYWwsIHBlcm8gc2lndWllbmRvIGVsIHByaW5jaXBpbyBkZSBwYXJzaW1vbmlhIChzaW1wbGljaWRhZCkgc2UgdHJhYmFqbyBjb24gZWwgbW9kZWxvIE5ORVQuDQoNClNlIGxvZ3LDsyByZWR1Y2lyIGxvcyBmYWxzb3MgbmVnYXRpdm9zIHBlcm8gYSBjb3N0YSBkZSBhdW1lbnRhciBsb3MgZmFsc29zIHBvc2l0aXZvcy4gTG8gYW50ZXJpb3IsIGZ1ZSByZXN1bHRhZG8gZGVsIG9iamV0aXZvIGJ1c2NhZG86IHJlZHVjaXIgZWwgZXJyb3IgdGlwbyBJSSAocHJvbm9zdGljYXIgYSB1biBjbGllbnRlIGVuIGxhIGNsYXNlIGRlIGxvcyBxdWUgbm8gc2UgdmFuIGEgaXIgeSBhbCBmaW5hbCB0ZXJtaW5hbiBhYmFuZG9uYW5kbyBlbCBiYW5jbykuDQoNCkVuIGN1YW50byBhIGxhcyB2YXJpYWJsZXMgbcOhcyBpbXBvcnRhbnRlcyBkZSBhYmFuZG9ubywgc2Vnw7puIGVsIG1vZGVsbyBOTkVULCBzZSBwdWVkZW4gbm9tYnJhciBsb3Mgc2lndWllbnRlczogZWRhZCwgbsO6bWVybyBkZSBwcm9kdWN0b3MsIHBhw61zIGRlIHJlc2lkZW5jaWEsIGFjdGl2aWRhZCBjb21vIHVzdWFyaW8gZGVsIHNpc3RlbWEgYmFuY2FyaW8sIGJhbGFuY2UgbyBzYWxkbyB5IGfDqW5lcm8uDQoNCkhhYmxhbmRvIHNvYnJlIGVsIHRpZW1wbyBkZSBwZXJtYW5lbmNpYSwgYWxyZWRlZG9yIGRlbCA1MCUgZGUgbG9zIGNsaWVudGVzIHByZXN1bWlibGVtZW50ZSBubyB2YW4gYSBhYmFuZG9uYXIgZGVudHJvIGRlIGxvcyAxMCBhw7Fvcy4gQcO6biBhc8OtLCBzaSBzZSB0b21hbiBlbiBjb25zaWRlcmFjacOzbiBsYXMgY292YXJpYWJsZXMsIHNlciBjaXVkYWRhbm8gYWxlbcOhbiBhdW1lbnRhIGxhIHByb2JhYmlsaWRhZCBkZSBhYmFuZG9ubywgbG8gcXVlIHJlZHVjZSBlbCB0aWVtcG8gZGUgc3VwZXJ2aXZlbmNpYSBhIHNvbG8gOSBhw7Fvcy4gU2luIGVtYmFyZ28sIHNlciBjaXVkYWRhbm8gZGUgb3RybyBwYcOtcyB5L28gc2VyIG1pZW1icm8gYWN0aXZvIGRlbCBzaXN0ZW1hIGJhbmNhcmlvIGRpc21pbnV5ZSBsYSBwcm9iYWJpbGlkYWQgZGUgYWJhbmRvbm8sIGxvIHF1ZSBnZW5lcmEgbcOhcyBkZSAxMCBhw7FvcyBlbiBsb3MgcXVlIG5vIHNlIGVzcGVyYSBxdWUgbG9zIGNsaWVudGVzIHNlIHZheWFuLg0KDQpQb3IgdGFudG8sIGhhYnLDrWEgcXVlIHRyYWJhamFyIGVuIGNvbmp1bnRvIGNvbiBlbCDDoXJlYSBkZSBNYXJrZXRpbmcgcGFyYSBlbmZvY2FyIGVzdHJhdMOpZ2lhcyB5IGNhbXBhw7FhcyBkZSBmaWRlbGl6YWNpw7NuIGVuIGxhcyBtdWplcmVzIGFsZW1hbmFzIHF1ZSBubyBzb24gbWllbWJyb3MgYWN0aXZvcyBkZWwgc2lzdGVtYSBiYW5jYXJpby4NCg0KPC9kaXY+DQoNCg0K