Surveiller les fichiers sur un Mac cloud sans confondre événements et modifications

Surveiller les fichiers sur un Mac cloud sans confondre événements et modifications

Sur un Mac cloud, un outil de build surveille le répertoire du code source pour lancer une compilation après chaque enregistrement. En pratique, un éditeur peut d’abord écrire dans un fichier temporaire, puis le renommer pour remplacer le fichier d’origine. Plusieurs enregistrements successifs peuvent aussi ne produire qu’un seul rappel. Si l’outil interprète « un événement reçu » comme « exactement un fichier modifié », il risque de manquer une compilation. La petite expérience ci-dessous permet de vérifier séparément les notifications et l’état final des fichiers.

Choisir ce que l’on surveille

Surveiller le descripteur d’un fichier donné permet d’observer cet objet, mais pas de déterminer si le chemin désigne désormais un autre fichier. Beaucoup d’éditeurs effectuent des sauvegardes atomiques : ils écrivent un fichier temporaire, puis le renomment pour remplacer l’original. Le chemin reste alors le même, tandis que le fichier auquel il mène peut avoir changé.

Dans cette expérience, nous surveillons plutôt le répertoire parent. La méthode comporte deux étapes : à la réception d’un événement sur le répertoire, signaler qu’un changement est possible, puis relire son contenu et le comparer à la dernière capture. Elle ne prétend pas retracer chaque écriture intermédiaire, mais répond à la question qui importe davantage à l’outil de build : quels fichiers d’entrée ont changé depuis la dernière vérification ?

Un rappel invite à relire le répertoire ; ce n’est pas un journal exhaustif des modifications. Même si deux écritures sont regroupées dans une seule notification, le fichier final doit être pris en compte lors de la vérification suivante.

Écrire un observateur minimal exécutable

Sur le Mac cloud, créez un répertoire d’essai depuis le terminal et enregistrez le code suivant dans watch.swift. Le script examine uniquement les éléments directement contenus dans le répertoire, sans parcourir les sous-répertoires. Il relève le nom, la taille, la date de modification et le numéro de fichier. Ce dernier aide à repérer le remplacement d’un fichier par un autre portant le même nom.

import Darwin
import Dispatch
import Foundation

struct Entry: Equatable {
    let size: UInt64
    let modified: Date
    let inode: UInt64
}

let path = CommandLine.arguments.dropFirst().first ?? ""
guard !path.isEmpty else {
    fputs("usage: swift watch.swift DIRECTORY\n", stderr)
    exit(2)
}
guard FileManager.default.fileExists(atPath: path) else {
    fputs("directory does not exist\n", stderr)
    exit(2)
}

func snapshot() -> [String: Entry] {
    var result: [String: Entry] = [:]
    let names = (try? FileManager.default.contentsOfDirectory(atPath: path)) ?? []
    for name in names {
        let file = (path as NSString).appendingPathComponent(name)
        guard let attributes = try? FileManager.default.attributesOfItem(atPath: file),
              let size = attributes[.size] as? NSNumber,
              let inode = attributes[.systemFileNumber] as? NSNumber,
              let modified = attributes[.modificationDate] as? Date else {
            continue
        }
        result[name] = Entry(
            size: size.uint64Value,
            modified: modified,
            inode: inode.uint64Value
        )
    }
    return result
}

let fd = open(path, O_EVTONLY)
guard fd >= 0 else {
    perror("open")
    exit(1)
}
let queue = DispatchQueue(label: "watch.lab")
let source = DispatchSource.makeFileSystemObjectSource(
    fileDescriptor: fd,
    eventMask: [.write, .rename, .delete],
    queue: queue
)
var previous = snapshot()
source.setEventHandler {
    let current = snapshot()
    let changed = Set(previous.keys).union(current.keys)
        .filter { previous[$0] != current[$0] }
        .sorted()
    if !changed.isEmpty {
        print("changed: \(changed.joined(separator: ", "))")
        fflush(stdout)
    }
    previous = current
}
source.setCancelHandler { close(fd) }
source.resume()
print("watching: \(path)")
dispatchMain()

Le rappel .write n’est pas directement associé à un nom de fichier : l’événement sur le répertoire indique seulement qu’il faut en examiner le contenu. Les éléments dont la lecture échoue dans snapshot() sont ignorés. Un outil de production devrait donc consigner séparément ces erreurs et réessayer lors du passage suivant, au lieu d’interpréter un bref problème d’autorisation ou une situation de concurrence liée à un renommage comme la preuve d’une suppression.

Tester les limites avec trois opérations

Lancez d’abord l’observateur dans un terminal, puis exécutez les opérations dans un second terminal. Le chemin ci-dessous ne sert qu’à l’expérience : ne le remplacez pas par celui d’un dépôt sur lequel vous travaillez.

mkdir -p "$HOME/watch-lab"
swift watch.swift "$HOME/watch-lab"
printf 'first\n' > "$HOME/watch-lab/input.txt"
printf 'second\n' > "$HOME/watch-lab/input.txt"
printf 'replacement\n' > "$HOME/watch-lab/.input.tmp"
mv -f "$HOME/watch-lab/.input.tmp" "$HOME/watch-lab/input.txt"
rm "$HOME/watch-lab/input.txt"

N’attendez pas que les lignes affichées correspondent une à une aux cinq commandes. Si les écritures et le renommage ont lieu entre deux captures, les états intermédiaires peuvent ne jamais apparaître. En revanche, après la suppression finale, input.txt ne doit plus figurer dans la capture. Pour vérifier séparément les trois résultats, faites une pause après la création, le remplacement et la suppression ; attendez chaque fois l’affichage correspondant avant de continuer.

Pourquoi relever le numéro de fichier lors d’un remplacement sous le même nom

Comparer uniquement les noms ferait passer inaperçu le cas où l’ancien fichier disparaît et un nouveau prend sa place au même chemin. Leurs tailles peuvent aussi être identiques. L’expérience relève donc le numéro de fichier : après un remplacement atomique, elle peut généralement détecter un changement d’objet même si le nom et la taille sont inchangés. La date de modification permet, quant à elle, de repérer une écriture ordinaire dans le fichier existant. Cette capture sert à une comparaison légère, pas à vérifier le contenu. Si votre décision de publier exige une identité octet pour octet, calculez aussi une empreinte du contenu une fois les fichiers concernés stabilisés.

Intégrer la méthode au processus de build

Dans un processus de build réel, mieux vaut ne pas lancer immédiatement une compilation pour chaque événement. Une machine à états plus robuste suit cette séquence : « recevoir une notification → planifier un nouveau balayage → recueillir les chemins modifiés → déclencher le build ». Si une autre notification arrive pendant le balayage, effectuez-en un de plus à la fin du passage en cours. Les événements peuvent ainsi être regroupés sans que les modifications survenues pendant un build soient simplement perdues.

Il faut aussi préciser la portée de la surveillance. Le script ci-dessus ne liste que les éléments directement contenus dans le répertoire ; il ne peut pas détecter de manière exhaustive les changements dans les sous-répertoires du code source. Pour couvrir toute l’arborescence, utilisez un mécanisme adapté aux modifications de répertoires imbriqués, tout en conservant le principe d’un nouveau balayage après notification. Excluez également le répertoire de sortie du build, sans quoi l’écriture des fichiers produits risque de redéclencher le processus en boucle.

Enfin, vérifiez le cycle de vie du répertoire surveillé : s’il est supprimé ou remplacé, l’ancien descripteur ne représente pas le nouveau chemin. L’outil doit vérifier que ce chemin désigne toujours le répertoire attendu et, si nécessaire, le rouvrir et rétablir la surveillance.

Une liste de vérification exécutable

Avant de mettre la logique de surveillance en production, effectuez les contrôles suivants dans un répertoire temporaire indépendant :

Ces contrôles déplacent la question de « pourquoi le système a-t-il envoyé un événement de moins ? » vers « toutes les entrées actuelles ont-elles été vérifiées ? ». Pour un flux de travail sur Mac cloud dont les tâches dépendent des modifications de fichiers, c’est ce second résultat que l’on peut valider.

Questions fréquentes

Pourquoi deux écritures rapides peuvent-elles produire un seul rappel ?

Le système de fichiers peut regrouper les événements. Relisez le dossier après la notification au lieu de compter les rappels.

Pourquoi surveiller le descripteur du fichier ne suffit-il pas ?

Une sauvegarde atomique peut remplacer le fichier par un nouvel objet. Surveillez le dossier parent et vérifiez à nouveau le chemin.

Louez un Mac dédié pour votre prochaine expérience

Choisissez un Mac mini physique, un nœud et une durée de facturation selon vos besoins. Les modèles et disponibilités affichés dans la console sont mis à jour en temps réel.

Voir les offres de location de Mac dans le cloud