Ir al contenido

Windows

Windows es una plataforma publicada, no algo añadido después: cada cambio ejecuta la suite unitaria entera y la de extremo a extremo en un runner de Windows, y el código fuente lleva partes solo de Windows allí donde el sistema se comporta distinto. Lo que sigue está escrito para PowerShell, con la forma de cmd al lado siempre que las dos difieran.

Ventana de terminal
irm https://raw.githubusercontent.com/jmrplens/ghchronicle/main/install.ps1 | iex

Deduce la arquitectura, coge la release más nueva, comprueba el archivo contra la suma de verificación que esa release publicó, y deja ghchronicle.exe en %LOCALAPPDATA%\Programs\ghchronicle. Una suma que no cuadra lo detiene sin instalar nada, y no hay parámetro para saltarse ese paso. Si cosign 2.4.2 o posterior ya está en tu PATH, verifica además que el propio fichero de sumas viene del workflow de release, la misma comprobación que hace install.sh, y una firma que cosign no confirma lo detiene, con lo que dijo cosign, para que una máquina que no llega a Sigstore no se tome por un fichero falsificado. Un cosign más antiguo no sabe leer el bundle de la firma, así que entonces, como sin cosign, dice que solo se verificó la suma.

Después añade ese directorio a tu PATH, el de usuario, para que ghchronicle funcione por nombre. Los programas ya abiertos conservan el PATH con el que arrancaron, así que abre una terminal nueva. Windows lee el PATH de máquina antes que el de usuario, así que otro ghchronicle instalado para todo el mundo, o una compilación vieja de go install, puede seguir respondiendo al nombre cuando esto termina; el script avisa cuando encuentra uno. No escribe nada fuera de tu perfil ni pide elevación: una instalación para toda la máquina es cosa de un instalador con su aviso de UAC, no de un script bajado de la red.

Para fijar una versión, elegir el directorio o no tocar el PATH, el script admite parámetros, y eso pide la forma algo más larga porque iex no tiene dónde ponerlos:

Ventana de terminal
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/jmrplens/ghchronicle/main/install.ps1))) -Version 2.6.5 -BinDir C:\tools -NoPathUpdate

El resto de esta página es esa misma instalación hecha a mano, que es lo que seguir cuando quieres saber exactamente qué ha aterrizado dónde, o cuando prefieres no ejecutar un script que no has escrito.

Después deja que escriba la configuración:

Ventana de terminal
ghchronicle -setup

Pide un token y dónde van los números, comprueba cada respuesta contra aquello que nombra, y escribe un config.yaml en %APPDATA%\ghchronicle y, si lo quieres, una tarea programada que registras con schtasks.

Lee esto primero. Cuatro de las cinco líneas de abajo son la razón de que una orden copiada de una página de Linux no funcione.

  • El programa es ghchronicle.exe. Escribir ghchronicle lo encuentra, porque PATHEXT lista .EXE.
  • Dependencias en ejecución: ninguna. El binario se compila con CGO_ENABLED=0, así que no hay runtime de C que instalar.
  • Pararlo es Ctrl+C en su consola. No hay señal que enviarle, y taskkill /F lo termina sin cerrar sus destinos.
  • Rutas en la configuración: una barra invertida dentro de un escalar YAML entre comillas dobles es un escape. Usa barras normales.
  • Ejecución desatendida: una tarea programada. El binario no es un servicio del Administrador de control de servicios.

Los archivos de Windows son zip y no tar.gz, y se llaman ghchronicle_<versión>_windows_<arq>.zip, donde <arq> es amd64 o arm64.

$env:PROCESSOR_ARCHITECTUREEl archivo que toca
AMD64windows_amd64
ARM64windows_arm64

Esa variable describe el proceso, no la máquina. Un PowerShell de 32 bits en una máquina de 64 informa de x86 y deja la arquitectura real de la máquina en $env:PROCESSOR_ARCHITEW6432; un PowerShell x64 emulado en una máquina ARM64 informa de AMD64 y no pone nada más, que es el caso que te entrega sin ruido el archivo equivocado. Si alguno de los dos puedes ser tú, pregúntale a la máquina y no al intérprete:

Ventana de terminal
(Get-CimInstance Win32_Processor).Architecture # 9 es x64, 12 es ARM64
Ventana de terminal
$version = "2.6.5"
$arch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "amd64" }
$base = "https://github.com/jmrplens/ghchronicle/releases/download/v$version"
$zip = "ghchronicle_${version}_windows_${arch}.zip"
Invoke-WebRequest -UseBasicParsing -Uri "$base/$zip" -OutFile $zip

-UseBasicParsing no es adorno. En Windows PowerShell 5.1 Invoke-WebRequest arma su resultado a través del motor de Internet Explorer salvo que se le diga que no, y falla del todo en una máquina donde Internet Explorer se haya quitado o nunca haya pasado su configuración de primer arranque, que es el caso de Server Core y de casi cualquier imagen endurecida. En PowerShell 7 el modificador se acepta y no hace nada.

checksums.txt lleva el SHA-256 de cada archivo, y checksums.txt.sigstore.json es una firma sobre ese fichero.

  1. Coge el fichero de sumas y compara la única línea que es tuya.

    Ventana de terminal
    Invoke-WebRequest -UseBasicParsing -Uri "$base/checksums.txt" -OutFile checksums.txt
    $expected = (Select-String -Path checksums.txt -Pattern ([regex]::Escape($zip) + '$')).Line.Split(" ")[0]
    $actual = (Get-FileHash -Algorithm SHA256 -Path $zip).Hash
    if ($actual -eq $expected) { "OK" } else { "MISMATCH" }

    Get-FileHash devuelve el resumen en mayúsculas y checksums.txt lo guarda en minúsculas. Aun así comparan igual porque el -eq de PowerShell entre dos cadenas ignora las mayúsculas, que es el único sitio de esta página donde ese comportamiento por omisión resulta cómodo en vez de una trampa.

  2. Comprueba el propio fichero de sumas, si tienes cosign 2.4.2 o posterior; uno más antiguo no sabe leer el bundle y falla diga lo que diga el fichero. La orden es la misma que imprimen las notas de la release, y la misma que ejecuta quien lea la página de Linux o la de macOS.

    Ventana de terminal
    cosign verify-blob `
    --certificate-identity-regexp 'https://github.com/jmrplens/ghchronicle/.github/workflows/release.yml@refs/tags/.*' `
    --certificate-oidc-issuer https://token.actions.githubusercontent.com `
    --bundle checksums.txt.sigstore.json `
    checksums.txt

    El acento grave es la continuación de línea de PowerShell, donde un guion de intérprete usa la barra invertida.

El archivo lleva tres ficheros y ningún directorio, así que descomprímelo en un directorio que hayas hecho.

  • Directorioghchronicle_VERSION_windows_ARCH.zip
    • ghchronicle.exe el binario
    • LICENSE
    • README.md

Un PowerShell elevado, porque tanto el directorio como el Path de máquina piden permisos de administrador.

Ventana de terminal
$dir = "C:\Program Files\ghchronicle"
Expand-Archive -Path $zip -DestinationPath $dir -Force
$machine = [Environment]::GetEnvironmentVariable("Path", "Machine")
[Environment]::SetEnvironmentVariable("Path", "$machine;$dir", "Machine")

Un Path nuevo solo llega a los procesos que arranquen después, así que abre una terminal nueva antes de la comprobación de abajo. La actual se queda con el entorno que le dieron.

Ventana de terminal
ghchronicle -version

Responde con una línea: el número de la versión, y después el commit y la fecha de compilación de esa versión.

ghchronicle 2.6.5 (commit <commit>, built <date>)

Un ${VAR} en una credencial, una dirección o una ruta de fichero de la configuración se lee del entorno cuando arranca el proceso, así que el token nunca tiene que estar en el fichero.

Ventana de terminal
# Solo esta ventana
$env:GITHUB_TOKEN = "github_pat_..."
# Persistido para esta cuenta
[Environment]::SetEnvironmentVariable("GITHUB_TOKEN", "github_pat_...", "User")

El binario necesita un fichero de configuración, y el inicio rápido escribe uno en seis pasos. Guárdalo en UTF-8, y atención a los dos detalles de Windows de debajo.

Ventana de terminal
ghchronicle -config config.yaml -list # qué se recogería
ghchronicle -config config.yaml -once # una pasada y termina

Si el binario está en el directorio actual y no en el PATH, PowerShell necesita que se le nombre como ruta: .\ghchronicle.exe. Un nombre a secas es una orden, y el directorio actual no se busca para órdenes.

YAML trata la barra invertida como un escape dentro de un escalar entre comillas dobles y como un carácter corriente en cualquier otro sitio. Así que una ruta de Windows entre comillas dobles no es la ruta que escribiste, y normalmente tampoco es YAML válido:

state_file: "C:\ghchronicle\state.json" # ghchronicle: config.yaml: yaml: line N: found unknown escape character
state_file: C:\ghchronicle\state.json # correcto, escalar simple
state_file: 'C:\ghchronicle\state.json' # correcto, comillas simples
state_file: C:/ghchronicle/state.json # correcto, y el que conviene

Las barras normales son la respuesta más simple: Windows las acepta en una ruta, y sobreviven a entrecomillarlas de cualquiera de las maneras.

Windows PowerShell 5.1, el que viene de serie, no escribe ninguna de las dos cosas que quiere un analizador de YAML, y escribe una cosa equivocada distinta según cómo se lo pidas. > y Out-File producen UTF-16LE, que el analizador lee como binario. Set-Content produce la página de códigos activa del sistema, normalmente ANSI, que se analiza bien mientras el fichero sea ASCII puro y destroza el primer carácter acentuado que aparezca. PowerShell 7 usa UTF-8 sin marca de orden de bytes por omisión y no tiene ninguno de los dos problemas; en 5.1, sé explícito:

Ventana de terminal
Set-Content -Path config.yaml -Value $text -Encoding utf8

utf8 en 5.1 quiere decir UTF-8 con marca de orden de bytes, cosa que el valor no sabe expresar y de la que el cmdlet no avisa. El analizador del colector la salta, así que el fichero funciona; una herramienta que lea los primeros bytes por su cuenta puede que no.

No hay modo servicio. El colector es un programa de consola: no habla con el Administrador de control de servicios, así que registrarlo con sc.exe create produce un servicio que Windows arranca y luego abandona, informando de que «no respondió a la petición de inicio o control de manera oportuna». Existen envoltorios de servicio de terceros y este proyecto ni distribuye ni prueba ninguno.

La forma corriente de ejecutar algo desatendido en Windows es una tarea programada, y tiene dos formas.

El equivalente de cron en Windows, y el que conviene: no hay nada que parar, y una ejecución perdida cuesta una pasada.

Ventana de terminal
$action = New-ScheduledTaskAction `
-Execute "C:\Program Files\ghchronicle\ghchronicle.exe" `
-Argument '-config "C:\ProgramData\ghchronicle\config.yaml" -once'
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) `
-RepetitionInterval (New-TimeSpan -Hours 1) `
-RepetitionDuration ([TimeSpan]::MaxValue)
Register-ScheduledTask -TaskName ghchronicle -Action $action -Trigger $trigger

-RepetitionDuration ([TimeSpan]::MaxValue) es lo que deja escrito «para siempre». La regla del propio Programador de tareas es que una repetición sin duración se repite indefinidamente, así que omitirlo no es un error, pero el cmdlet no tiene valor propio por omisión y la tarea queda registrada sin ninguna duración.

Esta forma trae dos condiciones, y ninguna es la de cron. Una tarea horaria da a todas las familias una cadencia horaria como mucho, así que las cinco que corren cada quince minutos, actions, events, notifs, activity y ratelimit, corren cuatro veces menos, que es el mismo cambio que hace cron en Linux. Y aquí Register-ScheduledTask no nombra ni -User ni -Principal, así que la tarea queda registrada con la cuenta que la crea y con el tipo de inicio de sesión por omisión, y corre solo mientras esa cuenta tenga la sesión iniciada. Un trabajo de cron no se para al cerrar sesión. Para que esta se comporte igual, regístrala con una entidad de seguridad con «ejecutar tanto si el usuario inició sesión como si no» marcado, que es New-ScheduledTaskPrincipal.

Ventana de terminal
Start-ScheduledTask -TaskName ghchronicle
Get-ScheduledTaskInfo -TaskName ghchronicle # última ejecución, último resultado

Stop-ScheduledTask termina el proceso en vez de pedirle que acabe: es taskkill /F con otro nombre, y no cierra ningún destino al salir. En la forma con temporizador no hay nada que parar, que es buena parte de por qué es la mejor opción por omisión.

Go 1.27.1 o más nuevo, que es la versión que declara go.mod. CGO_ENABLED=0 quiere decir sin MSVC, sin MinGW y sin SDK de Windows.

Ventana de terminal
go install github.com/jmrplens/ghchronicle/v2/cmd/ghchronicle@latest

Cae en $(go env GOPATH)\bin, que es %USERPROFILE%\go\bin salvo que lo hayas movido, y ese directorio tiene que estar en tu Path. Un binario hecho así no tiene commit ni fecha de compilación de los que informar: esas dos las sella la compilación de release, y un módulo descargado por el proxy no lleva copia de trabajo de donde leerlas. -version nombra en su lugar lo que la compilación sí registra, la versión del módulo que descargó la orden go y la versión de Go que lo compiló:

ghchronicle 2.6.5 (module v2.6.5, built with <go version>)

El colector no busca el fichero de configuración en ningún sitio concreto: -config vale por omisión config.yaml relativo al directorio de trabajo, y detrás no hay ninguna ruta de búsqueda. El directorio de trabajo de una tarea programada no es algo en lo que apoyarse, así que da todas las rutas del fichero en absoluto.

FicheroPara una tarea de máquinaPara una cuenta
ConfiguraciónC:/ProgramData/ghchronicle/${LOCALAPPDATA}/ghchronicle/
Estado, registro, cachéC:/ProgramData/ghchronicle/${LOCALAPPDATA}/ghchronicle/
LogC:/ProgramData/ghchronicle/${LOCALAPPDATA}/ghchronicle/

${LOCALAPPDATA} se escribe así porque ${VAR} es la única forma que el colector expande, desde el entorno, al arrancar el proceso, y lo hace en state_file, sinks.dedupe_file y log.file desde la 2.6.1. Una versión anterior no expande ninguna ruta, y lee ${LOCALAPPDATA}/ghchronicle/state.json como un directorio llamado literalmente ${LOCALAPPDATA}; con una de esas, escribe la ruta entera. %VAR% es una notación del intérprete y para el fichero no significa nada: un %LOCALAPPDATA% copiado dentro del YAML te da un directorio llamado literalmente %LOCALAPPDATA%, al lado de donde la tarea estuviera trabajando. Son %LOCALAPPDATA% en cmd y $env:LOCALAPPDATA en PowerShell los que crean el directorio en primer lugar.

La ruta de la propia configuración es la excepción de la tabla: se da en la línea de órdenes, donde el colector no expande nada, así que el -config de una tarea programada la nombra entera, C:\Users\tu\AppData\Local\ghchronicle\config.yaml.

Un directorio bajo C:\ProgramData lo puede escribir quien lo creó y lo puede leer todo el mundo, así que créalo elevado y luego concede escritura a la cuenta con la que corre la tarea. Deja escribibles el fichero de estado y el fichero de caché que hay a su lado. El colector reemplaza los dos escribiendo un fichero nuevo y renombrándolo sobre el viejo, y NTFS se niega a reemplazar un fichero de solo lectura incluso cuando se le pide reemplazarlo, así que un fichero de estado de solo lectura deja de guardarse y cada pasada avisa state not saved. El registro de escrituras y los ficheros rotados del destino de fichero son los dos a los que quita ese atributo él mismo, y una pasada no falla por ellos.