Инструмент сборки следит за каталогом исходного кода на облачном Mac: сохранение файла запускает компиляцию. На практике редактор может сначала записать временный файл, а затем переименовать его с заменой исходного. Несколько быстрых сохранений тоже могут привести лишь к одному вызову обработчика. Если считать, что одно событие означает изменение ровно одного файла, легко пропустить нужную пересборку. Небольшой эксперимент ниже поможет отдельно проверить уведомления и конечное состояние файлов.
Определите, за чем следить
Слежение за дескриптором отдельного файла позволяет наблюдать за самим файловым объектом, но не подходит для проверки того, не занял ли его путь новый файл. Многие редакторы сохраняют данные атомарно: записывают временный файл и переименовывают его с заменой исходного. Путь остаётся прежним, а файловый объект за ним может измениться.
В этом эксперименте мы следим за родительским каталогом. Алгоритм состоит из двух шагов: получив событие каталога, отмечаем, что изменения возможны, затем перечитываем каталог и сравниваем его с предыдущим снимком. Такой подход не обещает зафиксировать каждую промежуточную запись, зато отвечает на более важный для сборки вопрос: какие входные файлы изменились с момента последней проверки?
Вызов обработчика — повод повторно просканировать каталог, а не запись в журнале каждого изменения. Если две записи объединились в одно уведомление, итоговый файл всё равно должен попасть в следующую проверку.
Напишите минимальный рабочий наблюдатель
Создайте каталог для эксперимента через командную строку облачного Mac и сохраните следующий код в watch.swift. Скрипт проверяет только непосредственные элементы каталога, без рекурсивного обхода. Он записывает имя файла, размер, время изменения и номер файлового объекта. Номер помогает распознать замену файла другим с тем же именем.
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()
Здесь вызов обработчика .write не сопоставляется напрямую с именем файла: событие каталога лишь сообщает, что каталог нужно проверить. snapshot() пропускает элементы, которые не удалось прочитать. Поэтому в рабочем инструменте ошибки чтения следует регистрировать отдельно и повторять попытку на следующем проходе, а не считать временную проблему с правами доступа или гонку при переименовании достоверным признаком удаления.
Проверьте границы на трёх операциях
Сначала запустите наблюдатель в одном терминале, затем выполните операции во втором. Эти пути предназначены только для эксперимента: не указывайте работающий репозиторий.
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"
Не ожидайте, что каждой команде будет соответствовать отдельная строка вывода. Если запись и переименование произойдут между двумя снимками, промежуточное состояние может вообще не появиться в выводе. Но после заключительного удаления в снимке уже не должно быть input.txt. Чтобы проверить результаты создания, замены и удаления по отдельности, делайте паузу после каждой операции и дожидайтесь вывода, прежде чем продолжить.
Зачем при замене с тем же именем нужен номер файлового объекта
Если сравнивать только имена, ситуация «старый файл исчез, новый занял тот же путь» будет выглядеть как отсутствие изменений. Размеры файлов тоже могут случайно совпасть. Поэтому эксперимент дополнительно записывает номер файлового объекта: обычно это позволяет заметить атомарную замену, даже если имя и размер остались прежними. Время изменения помогает обнаружить обычную запись в существующий файл. Такой снимок даёт лёгкую проверку изменений, а не подтверждает идентичность содержимого. Если для выпуска необходима побайтовая уверенность, отдельно вычислите хеш содержимого после того, как файл-кандидат перестанет меняться.
Встройте подход в процесс сборки
В рабочем процессе не стоит запускать компиляцию сразу при каждом событии. Надёжнее использовать последовательность состояний «получить уведомление → запланировать повторное сканирование → собрать изменившиеся пути → запустить сборку». Если во время сканирования придёт ещё одно уведомление, после завершения текущего прохода выполните следующий. Это допускает объединение событий, но не позволяет потерять новые изменения только потому, что сборка уже идёт.
Также важно определить глубину обхода каталогов. Скрипт выше перечисляет только непосредственные элементы каталога и не может полностью обнаруживать изменения содержимого вложенных каталогов с исходным кодом. Чтобы охватить всё дерево, используйте механизм, подходящий для отслеживания изменений во вложенных каталогах, но сохраните принцип «получили уведомление — просканировали заново». Исключите каталоги с результатами сборки, иначе запись артефактов может снова и снова запускать сборку.
Наконец, проверьте жизненный цикл самого отслеживаемого каталога. Если каталог удалят или заменят, старый дескриптор не сможет представлять новый объект по тому же пути. Инструмент должен проверять, что путь по-прежнему указывает на ожидаемый каталог, и при необходимости открывать его заново и восстанавливать наблюдение.
Проверочный список перед запуском
Прежде чем запускать логику слежения в рабочем окружении, выполните следующие проверки в отдельном временном каталоге:
- После нескольких быстрых записей файл с итоговым содержимым попадает в набор входных файлов для сборки; число вызовов обработчика не служит критерием успеха.
- После переименования временного файла с заменой исходного входной файл с тем же именем проверяется повторно.
- После удаления входного файла объект из предыдущего снимка больше не считается существующим.
- Если во время сборки произойдёт второе изменение, после её завершения выполнится как минимум ещё одно сканирование.
- Каталог с результатами сборки исключён; после замены отслеживаемого каталога наблюдение создаётся заново.
- Ошибки чтения регистрируются, предусмотрена повторная попытка; такие ошибки не трактуются молча как «изменений нет».
Эти проверки смещают акцент с вопроса «почему система прислала на одно событие меньше?» на вопрос «полностью ли проверены текущие входные файлы?». Для процессов на облачном Mac, где изменения файлов запускают задачи, именно второй результат можно проверить при приёмке.
Часто задаваемые вопросы
Почему после двух быстрых изменений приходит только одно событие?
Файловая система может объединять уведомления. После события повторно сканируйте каталог и сравнивайте его состояние.
Почему слежение за открытым файлом не замечает атомарное сохранение?
Новый файл заменяет старый, а дескриптор может остаться связанным со старым объектом. Следите за родительским каталогом.
Выделенный Mac для следующего эксперимента
Выберите физический Mac mini, узел и период оплаты. Модель и доступность уточняйте в консоли — там отображаются актуальные данные.