guest@6admin.io:~$

Survivre à Windows Update au milieu d'un build

Un peu de contexte d’abord, parce qu’il manque. À l’époque, je faisais tourner des agents de build chez un employeur qui développait des applications Windows, sous Windows et dont la politique de mise à jour était subie bien plus que choisie: les updates tombaient quand elles le décidaient, poussées sur le parc, sans le moindre égard pour ce qui compilait à cet instant. De là le problème qui suit.

Quiconque a exploité des agents de build Windows connaît ce moment particulier où Windows Update décide, au beau milieu d’une étape sensible, qu’il est temps d’installer trois cumulative updates et de programmer un redémarrage, réduisant à néant vingt minutes de compilation. La réaction naïve consiste à couper le service de mise à jour au début du build et à le rétablir à la fin et cette réaction naïve tient exactement jusqu’au jour où deux builds tournent en parallèle sur le même agent ou jusqu’au jour où un build crashe avant d’atteindre son étape de nettoyage, laissant l’agent avec ses mises à jour désactivées pour l’éternité. La bonne nouvelle, c’est que ce problème a une solution propre et qu’elle emprunte son vocabulaire à la gestion de ressources partagées bien plus qu’à l’administration Windows.

Pourquoi le stop/start ne suffit pas

Le naïf échoue sur deux fronts qu’il faut traiter ensemble. D’abord la concurrence, parce que si le build A désactive puis le build B désactive et que A se termine et réactive alors que B tourne encore, vous venez de rendre à B les mises à jour dont il ne voulait pas. Ensuite la robustesse, parce que toute logique de restauration placée en fin de build ne s’exécute pas quand le build meurt avant d’y arriver. Il faut donc un compteur de références qui sache combien de builds réclament simultanément la désactivation, une restauration qui n’ait lieu que quand le dernier sort et une exécution garantie de cette restauration même en cas d’échec.

Le patron

L’idée tient en une shared library qui expose deux primitives, acquérir et relâcher et qui s’appuie sur trois éléments. Un fichier d’état, en JSON, qui persiste sur le disque de l’agent le compteur de références et, surtout, l’état initial exact capturé au tout premier acquire. Un mutex nommé global, du genre Global\..., qui sérialise l’accès à ce fichier, parce que la séquence lire-modifier-écrire du compteur doit être atomique entre processus, faute de quoi deux builds qui incrémentent en même temps s’écraseront mutuellement. Et un bloc de restauration best-effort, placé dans le post ou le finally du pipeline, de sorte qu’un build qui échoue décrémente quand même.

// English comments per house rule.
// Acquire: increment the ref count under the named mutex,
// and on the very first acquire, snapshot the exact prior state.
withGlobalMutex('Global\\6admin_wu_guard') {
    def state = readState()            // {refCount, initial: {...}} or null
    if (state == null) {
        state = [refCount: 0, initial: snapshotWindowsUpdateState()]
    }
    if (state.refCount == 0) {
        disableWindowsUpdate()         // wuauserv + policy keys
    }
    state.refCount++
    writeState(state)
}

// Release (in post/always): decrement, and only the last one out restores.
withGlobalMutex('Global\\6admin_wu_guard') {
    def state = readState()
    state.refCount = Math.max(0, state.refCount - 1)
    if (state.refCount == 0) {
        restoreWindowsUpdateState(state.initial)  // put back EXACTLY what was there
        clearState()
    } else {
        writeState(state)
    }
}

Le point que je tiens à souligner, parce qu’il est celui qu’on oublie, c’est qu’on ne restaure pas “activé”, on restaure ce qui était là. L’agent avait peut-être ses mises à jour pilotées par une politique WSUS ou déjà désactivées pour une raison qui vous dépasse et présumer un état de base revient à corrompre la configuration de la machine en croyant la réparer. On capture le start type du service et les clés de politique concernées, typiquement NoAutoUpdate sous HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU et l’on remet précisément cet instantané, ni plus ni moins.

La limite qu’on assume

Reste un cas que ce patron ne couvre pas et il vaut mieux l’écrire noir sur blanc que de faire semblant. Si l’agent redémarre pendant un build, le compteur de références survit au reboot alors que le build, lui, est mort et vous vous retrouvez avec un refCount fantôme qui empêchera à jamais la restauration. La parade est une scheduled task au démarrage, qui, constatant qu’aucun build n’est actif, remet le compteur à zéro et rétablit l’état capturé. Ce n’est pas élégant, mais c’est honnête et une limite documentée avec sa mitigation vaudra toujours mieux qu’un patron qu’on prétend infaillible jusqu’au premier incident de prod.

[edit: 2026] L’implémentation Ansible de tout ceci: le rôle jenkins-agent .