Ключевые функции
seed_index(root)
При старте заполняетlive_pathsабсолютными путями всех файлов и каталогов под корнем. Это база, от которой считаем удаленные объекты.norm(path)
Приводит путь к абсолютному через.resolve()(убирает./.., разворачивает симлинки). Нужен для консистентности ключей.is_under(child, parent)
Проверяет, лежит лиchildвнутриparent. Используется для фильтрации потомков.on_deleted(event)
Главная функция.
• Если удалили папку: отмечаем «могилку» (tombstone), вычищаем её потомков изlive_paths, считаем их количество, сдвигаем коалесинговое окно и отпечатываем итоговую сводку по таймеру.
• Если удалили файл: добавляем запись в буфер «ранних» удалений и, если файл лежит в папке с «могилкой», увеличиваем её счётчик._arm_summary_timer(dir_key)/_emit_summary(dir_key)
Перезапускают/исполняют таймер сводки. По истечении окна коалесинга печатают суммарный итог и очищают состояние.
Теория: macOS FSEvents, очереди и коалесинг
Как FSEvents доставляет события
На macOS служба FSEvents ведёт пер-директорный журнал изменений. Приложение (watchdog) подписывается на директории и получает пачки событий, где пути уже нормализованы ядром. Важные особенности:
- Пакетность и коалесинг на уровне ОС: при быстром каскадном удалении (много файлов → папка) система может не прислать каждое детское событие по отдельности. Иногда вы увидите только «папка удалена».
- Порядок доставки не гарантирован строго: отдельные события по детям могут прилетать до или после события об удалении папки — разница в десятки/сотни миллисекунд.
- Потоки: watchdog вызывает обработчики из рабочего потока наблюдателя; наши таймеры живут в своих потоках. Значит, нужна синхронизация.
Почему без коалесинга возможны потери/ошибки подсчёта
Если сразу печатать логи без «окна ожидания», то:
- можно не учесть часть детских удалений (они прилетят позже события «папка удалена»);
- или двойной учёт: сначала насчитали потомков по индексу, затем пришли «запаздывающие» удаления файлов и увеличили число второй раз.
Что делает наш коалесинг
Мы добавили временное окно COALESCE_MS. Если пришёл DELETE по папке, мы:
- Считаем её потомков по нашему индексу
live_paths(тот, что был созданseed_indexи пополняется вon_created/on_modified). - Подчищаем их из
live_pathsи помечаем «могилкой»pending_dir_deletes[p]. - Запускаем таймер сводки на
COALESCE_MS. - Все «запаздывающие» удаления файлов внутри этой папки, пришедшие в пределах окна, поглощаются в счётчик
pending_dir_counts[p]. - Когда окно закрывается — печатаем одну сводку и очищаем состояние.
Это защищает от «дыр» в статистике и от двойного учёта.
Как исполняется код при удалении папки
Ниже шаги, которые происходят в коде при событии on_deleted(is_directory=True):
- Нормализация пути
p = norm(event.src_path)→ путь абсолютный и консистентный. - Подготовка окна коалесинга
now = time.time()иcutoff = now - COALESCE_MS/1000. Заводим могилку для папки (под локом)
pending_dir_deletes[p] = now pending_dir_counts[p] = 0Это корень сводки.
- Поглощение ранних удалений (под локом)
Идём поrecent_file_deletesи вынимаем те, что произошли не раньшеcutoffи лежат подp. Они увеличиваютabsorbed_from_buffer. - Подсчёт по индексу (под локом)
Находим всех потомковpвlive_pathsи считаем их. Это покрывает случай, когда ОС прислала только «папку удалили», без детей. Потом удаляем их из индекса (и самуpтоже). - Итоговый счётчик (под локом)
pending_dir_counts[p] = count_index + absorbed_from_buffer. - Лог мгновенного факта
Печатаем, что удалена папкаp. (Сводка по количеству — позже.) - Старт/продление таймера
_arm_summary_timer(p)— заводит/перезапускает таймер наCOALESCE_MS. - Что происходит «во время окна»
Если в окно прилетят отдельныеDELETEфайлов внутриp, они найдут родителя вpending_dir_deletesи увеличатpending_dir_counts[p]. Таймер продлится, если мы так хотим (в твоём коде — перезапуск при повторном вызове_arm_summary_timerдля родителя). - Когда таймер срабатывает
_emit_summary(p)печатает сводку: «Удалена папка X; удалено N объектов», и очищаетpending_dir_*.
Диаграммы для наглядного понимания работы кода
1) Архитектура (высокий уровень)
┌─────────────┐ ┌─────────────────┐ ┌───────────────────┐ ┌───────────────────────┐
│ ОС (macOS) │──►│ FSEvents (OS) │──►│ watchdog.Observer │──►│ MyHandler (callbacks) │
└─────────────┘ └─────────────────┘ └───────────────────┘ └───────────┬───────────┘
│
▼
┌──────────────┐
│ on_deleted() │
└──────┬───────┘
│
▼
┌────────────────────────────┐
│ коалесинг/сводка (300 мс) │
└─────────────┬──────────────┘
▼
┌─────────────┐
│ вывод │
└─────────────┘2) Последовательность исполнения кода при удалении папки
Actor: Filesystem(FSEvents) Observer(watchdog) MyHandler.on_deleted State/Stores
───────────────────── ────────────────── ───────────────────── ─────────────
1) DELETE dir=/A ───────────► event: is_directory ─► p=norm(/A) live_paths (до)
│
2) lock ────┼──► pending_dir_deletes[p]=now
│ pending_dir_counts[p]=0
│ absorb recent_file_deletes within cutoff
│ descendants = {x ∈ live_paths | x under p}
│ remove descendants & p from live_paths
│ pending_dir_counts[p]+=len(descendants)+absorbed
▼
3) print "[Удалено] Папка: /A"
4) _arm_summary_timer(p) ─────► start/restart Timer(COALESCE_MS)
5) (в течение окна) ◄───── могут прийти DELETE file=/A/…
on_deleted(file) → +1 к pending_dir_counts[p], продлить таймер
6) timer fires ──────────► _emit_summary(p) → print "[Сводка] … удалено N объектов"
clear pending_dir_* for p
3) Конечный автомат состояний для on_deleted
┌───────────────────────────────────────────────────────────┐
│ [IDLE] │
└───────────┬───────────────────────────────────────────────┘
│ DELETE(dir)
▼
┌───────────────────────────────────────────────────────────┐
│ [DIR_TOMBSTONE_SET] (могилка заведена, индекс посчитан) │
└───────────┬───────────────────────────────────────────────┘
│ DELETE(file under dir) в течение окна
▼
┌───────────────────────────────────────────────────────────┐
│ [COALESCE_WINDOW] (накапливаем счётчик, таймер тикает) │
└───────────┬───────────────────────────────────────────────┘
│ таймер истёк
▼
┌───────────────────────────────────────────────────────────┐
│ [SUMMARY_EMIT] → печать сводки, очистка pending_* │
└───────────┬───────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────┐
│ [IDLE] │
└───────────────────────────────────────────────────────────┘
4) Таймлайн коалесинга
время → t0 t0+60ms t0+140ms t0+280ms t0+300ms
│ │ │ │ │
FSEvents │ DELETE /A ───►│ │ │ │
files │ DELETE /A/f1 ──►│ │ │
│ │ DELETE /A/f2 ──►│ │
│ │ │ │ (таймер)
summary │ emit summary: count = index(A)+absorbed
5) Связи структур данных
┌───────────────────┐ write/remove ┌─────────────────────┐
│ live_paths │◄──────────────────────│ seed_index, events │
│ set[str] │ │ on_created/modified│
└─────────┬─────────┘ └─────────┬───────────┘
│ read (for dir delete) │
▼ │
┌───────────────────┐ append ┌─────────────────────────┐ refer/absorb ┌──────────────────────┐
│ recent_file_del. │◄────────────│ on_deleted(file) │────────────────────►│ pending_dir_counts │
│ deque[(str,float)]│ └─────────────────────────┘ │ dict[str,int] │
└───────────────────┘ └───────────┬──────────┘
│
▼
┌──────────────────────┐ ┌─────────────────────┐
│ │ set/start timer ┌─────────────────────────┐ │ pending_dir_deletes │
│ watchdog timer thread│─────────────────────►│ _arm_summary_timer(dir) │─────────►│ dict[str,float] │
│ │ └───────────┬─────────────┘ └──────────────────────┘ │ │
│ │
▼ │
┌─────────────────────────┐ │
│ _emit_summary(dir) │◄────────────────────┘
└─────────────────────────┘ (clear pending_*)
6) Потоки и блокировки
┌───────────────────────────────────────────────┐
│ Observer Thread (watchdog) │
│ calls MyHandler.on_* │
│ - modifies live_paths │
│ - updates pending_dir_* / deque │
└───────────────▲───────────────────────────────┘
│
acquire lock() ┌─────┴─────┐ acquire lock()
│ lock │
release lock() └─────┬─────┘ release lock()
│
┌───────────────▼───────────────────────────────┐
│ Timer Thread(s) │
│ - _arm_summary_timer/_emit_summary │
│ - reads & clears pending_dir_* │
└───────────────────────────────────────────────┘
7) Очередь «ранних» удалений и поглощение
recent_file_deletes (deque):
head tail
┌───────────────┬───────────────┬───────────────┬───────────────┐
│ (fp1, ts=100) │ (fp2, ts=180) │ (fp3, ts=240) │ (fp4, ts=410) │
└───────────────┴───────────────┴───────────────┴───────────────┘
DELETE dir=/A at now=300, cutoff=300-COALESCE=0.3s ⇒ 300-0.3=0.0s (пример)
Поглощаем элементы, удовлетворяющие:
ts ≥ cutoff И is_under(fp, /A)
Допустим, fp1, fp2, fp3 — под /A и удовлетворяют ts ≥ cutoff ⇒ absorbed=3
Неподходящие элементы перекладываем в новый буфер и возвращаем в deque.
Таблицы
1) Основные структуры
| Структура | Тип | Кто пишет | Кто читает | Назначение |
|---|---|---|---|---|
live_paths | set[str] | seed_index, on_created/modified | on_deleted | Индекс «живых» путей, чтобы знать, кто исчез при удалении папки |
recent_file_deletes | deque[(str, float)] | on_deleted(file) | on_deleted(dir) | Буфер ранних DELETE файлов, «поглощаемых» родительской папкой |
pending_dir_deletes | dict[str, float] | on_deleted(dir) | _emit_summary, on_deleted(file) | Могилка: факт удаления папки и старт окна |
pending_dir_counts | dict[str, int] | on_deleted(dir/file) | _emit_summary | Счётчик «сколько исчезло» |
pending_dir_timers | dict[str, threading.Timer] | _arm_summary_timer | _emit_summary | Отложенный вывод сводки |
2) Выбор окна коалесинга COALESCE_MS
| Критерий / Окно | 100 мс | 300 мс (рекоменд.) | 700 мс |
|---|---|---|---|
| Риск «пропущенных» файлов (поздние DELETE) | средний | низкий | ещё ниже |
| Риск «двойного» учёта | низкий | низкий | низкий |
| Задержка сводки для пользователя | низкая | умеренная | заметная |
| Нагрузка таймеров/локов | низкая | умеренная | чуть выше |
| Баланс UX/корректности | недоучёт возможен | лучший баланс | задержка велика |
| Почему так | быстро, но хрупко при батч-операциях | успевают доехать поздние события FSEvents | слишком «тормозит» отклик |
Почему 300 мс: на macOS FSEvents пакетирует каскадные операции, и отложенные удаления дочерних объектов часто приходят в пределах ~200–250 мс; 300 мс надёжно их «доглатывает», не внося заметной задержки в опыт пользователя.
Ограничения и заметки по потокам
- Все структуры изменяются под
lock, потому что callbacks watchdog и таймеры — в разных потоках. - При очень больших деревьях
seed_indexможет занять время. Это плата за корректную «сводку при удалении папки». - Если пользователь удалил дерево через инструмент, который не генерирует детские события, мы всё равно корректны: счёт берём из
live_paths.
Полный код с комментариями
В статье выше мы объясняли только ключевые функции. Ниже — полный листинг, в котором комментарии уже содержат подробные пояснения всех участков.
import time
import datetime
from datetime import datetime as dt
import threading
from pathlib import Path
from collections import deque
import os
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
# ============================================================================
# ПЕРЕМЕННЫЕ
# ============================================================================
COALESCE_MS = 300
'''
Окно коалесинга (мс): в течение этого времени мы "склеиваем" лавину удалений.
'''
pending_dir_deletes: dict[str, float] = {} # папка -> время удаления
pending_dir_counts: dict[str, int] = {} # папка -> число удалённых объектов
pending_dir_timers: dict[str, threading.Timer] = {} # папка -> таймер сводки
'''
"Могилки" (tombstones) по удалённым папкам и связанные артефакты.
pending_dir_deletes - Имя переменной словаря
: dict[] - Подсказка для человека/IDE/линтера, просто хороший тон для отладки
str - Строка пути к папке (String)
float/int - Числовые значения в зависимости от контекта функции
threading.Timer - Класс таймера
= {} - Словарь создается пустым
'''
recent_file_deletes: deque[tuple[str, float]] = deque()
'''
Буфер "ранних" удалений файлов (на случай, если сначала пришли DELETE по файлам, а потом DELETE по папке).
recent_file_deletes - Имя переменной двухсторонней очереди
: deque[] - Подсказка для человека/IDE/линтера, просто хороший тон для отладки
tuple[] - Неизменяемый, упорядоченный и с гибкими объектами кортеж, почти как список, но со своими особенностями
str - Строка пути к папке (String)
float - Дробные числа, используем для времени
= deque() - Очередь создается пустой
'''
live_paths: set[str] = set()
'''
Легкий индекс живых путей, содержит абсолютные нормализованные пути всех файлов и папок, которые мы считаем "существующими".
Нужно, чтобы на macOS уметь посчитать, сколько именно внутри папки исчезло, даже если система не прислала отдельные события по детям.
live_paths - Имя переменной множества (набор уникальных и неупорядоченных элементов, можно изменить)
: set[] - Подсказка для человека/IDE/линтера, просто хороший тон для отладки
str - Строка пути к папке (String)
= set() - Множество создается пустым
'''
lock = threading.Lock()
'''
Защита критических секций кода, где осуществляется доступ и модификация общих ресурсов. Захватывая блокировку перед входом
в критическую секцию, можно гарантировать, что только один поток может выполнять этот код в определённый момент времени,
предотвращая гонки данных.
Lock() - Создать экземпляр класса. По умолчанию блокировка не захвачена, пока её не получит поток
acquire() - Захватить блокировку методом
release() - Освободить блокировку методом
with lock: - Это acquire() в начале блока и release() при выходе из блока
'''
# ============================================================================
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
# ============================================================================
def stamp():
return dt.now().strftime("%Y-%m-%d %H:%M:%S")
'''
Удобное отображение даты
'''
def norm(path: str) -> str:
return str(Path(path).resolve())
'''
Создание абсолютно нормального пути для единообразия ключей.
norm - Имя функции
(path: str) - Создаем параметр path, в котором фукнция ожидает принять значение str
-> str - Функция выдаст значение str
str(Path(path).resolve()) - Обращаемся к пути с параметром path, преобразовываем в полный путь, конвертируем в строку
'''
def is_under(child: str, parent: str) -> bool:
try:
Path(child).resolve().relative_to(Path(parent).resolve())
return True
except Exception:
return False
'''
Проверка: child лежит внутри parent?
is_under - Имя функции
(child: str, parent: str) - Создаем 2 параметра, оба - строки
-> bool - Функция выдаст значение bool
Path(child).resolve().relative_to(Path(parent).resolve()) - Преобразем 2 параметра строк в пути и сравниваем, является
ли один путь родителем другого или нет. Если выполняется без
ошибок, код исполняется с return True. Ошибка - return False
'''
def seed_index(root: str):
root_abs = norm(root)
with lock:
live_paths.add(root_abs)
for dirpath, dirnames, filenames in os.walk(root_abs):
dp = norm(dirpath)
live_paths.add(dp)
for name in dirnames:
live_paths.add(norm(os.path.join(dp, name)))
for name in filenames:
live_paths.add(norm(os.path.join(dp, name)))
'''
Инициализируем индекс live_paths текущим состоянием диска. Важно для корректных подсчётов, если папка уже содержит что-то на старте.
seed_index - Имя функции
(root: str) - Создаем параметр root, в котором фукнция ожидает принять значение str
root_abs = norm(root) - Вызываем функцию norm, нормализуя строку пути параметра root в нормальный вид и присываеваем переменной root_abs
with lock: — Блокируем ресурс, защищая общий live_paths и оставляем один поток для работы с ним
live_paths.add(root_abs) - Добавляем строку параметра root в множество live_paths
for dirpath, dirnames, filenames in os.walk(root_abs) - Бежим по всем путям папки root_abs и ищем, существуют ли
dirpath - текущая папка,
dirnames - список подкаталогов,
filenames - список файлов
dp = norm(dirpath) - Вызываем функцию norm, нормализуя строку пути dirpath в нормальный вид и присываеваем переменной dp
live_paths.add(dp) - Добавляем путь в live_paths
for name in dirnames - Проходим весь список подкаталогов и присваеваем значения к name один за другим
live_paths.add(norm(os.path.join(dp, name))) - Объединяем путь текущей папки с путем подкаталога, нормализуем путь и вставляем в множество live_paths
for name in filenames - Проходим весь список файлов и присваеваем значения к name один за другим
live_paths.add(norm(os.path.join(dp, name))) - Объединяем путь текущей папки с путем файла, нормализуем путь и вставляем в множество live_paths
'''
def _emit_summary(dir_key: str):
with lock:
count = pending_dir_counts.pop(dir_key, 0)
print(f"{stamp()} [Сводка] Удалена папка {dir_key}; вместе с ней удалено {count} объектов")
pending_dir_deletes.pop(dir_key, None)
t = pending_dir_timers.pop(dir_key, None)
if t:
t.cancel()
'''
Срабатывает по таймеру: печатает сводку и очищает состояние по папке.
_emit_summary - Имя функции
(dir_key: str) - Создаем параметр dir_key, в котором фукнция ожидает принять значение str
with lock: — Блокируем ресурс, защищая общий live_paths и оставляем один поток для работы с ним
count = pending_dir_counts.pop(dir_key, 0) - Забираем и удаляем из словоря pending_dir_counts значение по ключу параметра dir_key.
Если ключа dir_key нет, то возвращаем в переменную count значение 0.
Если ключ есть, то count присваевается значение уже посчитанных файлов.
pending_dir_deletes.pop(dir_key, None) - Чистим словарь pending_dir_deletes
t = pending_dir_timers.pop(dir_key, None) - Чистим словарь pending_dir_timers
if t: t.cancel() - Если нашли таймер, то останавливаем его
'''
def _arm_summary_timer(dir_key: str):
ttl = COALESCE_MS / 1000.0
with lock:
old = pending_dir_timers.get(dir_key)
if old:
old.cancel()
t = threading.Timer(ttl, _emit_summary, args=(dir_key,))
pending_dir_timers[dir_key] = t
t.start()
'''
(Пере)запуск таймера сводки для папки.
_arm_summary_timer - Имя функции
(dir_key: str) - Создаем параметр dir_key, в котором фукнция ожидает принять значение str
ttl = COALESCE_MS / 1000.0 - Преобразовываем миллисекунды коалесинга в секунды
with lock: — Блокируем ресурс, защищая общий live_paths и оставляем один поток для работы с ним
old = pending_dir_timers.get(dir_key) - Получаем значение из pending_dir_timers по ключу параметра dir_key и присваеваем его переменной old
if old: old.cancel() - Если таймер существует, то останавливаем задачу таймера
t = threading.Timer(ttl, _emit_summary, args=(dir_key,)) - Создаем новый класс потока таймера с параметрами:
1. Инервал ttl
2. Функция _emit_summary, которую нужно выполнить по таймеру
3. Список аргументов args=(dir_key,) для функции
pending_dir_timers[dir_key] = t - Передаем таймер в словарь pending_dir_timers по ключу параметра dir_key
t.start() - Запуск таймера
'''
# ============================================================================
# ОБРАБОТЧИК СОБЫТИЙ
# ============================================================================
class MyHandler(FileSystemEventHandler):
def on_modified(self, event):
p = norm(event.src_path)
kind = 'Папка' if event.is_directory else "Файл"
with lock:
live_paths.add(p)
print(f"{stamp()} [Изменено] {kind}: {p}")
def on_moved(self, event):
p = norm(event.src_path)
p_dest = norm(event.dest_path)
kind = 'Папка' if event.is_directory else "Файл"
print(f"{stamp()} [Перемещено] {kind}: {p} -> {p_dest}")
def on_created(self, event):
p = norm(event.src_path)
kind = 'Папка' if event.is_directory else "Файл"
with lock:
live_paths.add(p)
print(f"{stamp()} [Создано] {kind}: {p}")
def on_deleted(self, event): # Функция, вызывающаяся при удалении
p = norm(event.src_path) # Вызываем функцию norm для нормалзации пути event
if event.is_directory: # Проверка, является ли путь путем к папке
kind = 'Папка' # Переменная kind будет использоваться в логах
now = time.time() # Берем текущее время в виде значения float
cutoff = now - COALESCE_MS / 1000.0 # Все, что будет больше значения COALESCE_MS, будет удаляться
absorbed_from_buffer = 0 # Счетчик удалений, которые были до удаления папки
with lock: # Оставляем только 1 поток
pending_dir_deletes[p] = now # Добавлем могилку папки p в словарь pending_dir_deletes
pending_dir_counts[p] = 0 # Обнуляем накопительный счётчик удалённых объектов в папке
global recent_file_deletes # Объявляем, что будем переназначать глобальную очередь recent_file_deletes
newbuf = deque() # Очередь, куда переложим только те записи, которые не относятся к этой папке или слишком старые
for fp, ts in recent_file_deletes: # Идём по буферу удаленных объектов до нынешнего удаления папки
if ts >= cutoff and is_under(fp, p): # Если файл fp удалился не ранее, чем cutoff и он лежит внутри удаляемой папки p
absorbed_from_buffer += 1 # Увеличиваем счетчик на 1
else: # Иначе
newbuf.append((fp, ts)) # Добавляем в конец списка нового буффера пути этих объектов
recent_file_deletes = newbuf # Обновляем список объектов, которые были удалены до удаления папки
descendants = [lp for lp in live_paths if lp != p and is_under(lp, p)] # Проеряем, есть ли в множестве существующих объектов live_paths потомок пути нашей папки и
# не является ли он нашей папкой. Если все хорошо, то присваеваем значение к
# списку descendants
count_index = len(descendants) # Считаем количество таких объектов
for lp in descendants: # Если объект существует в списке, то
live_paths.discard(lp) # Удаляем объект из множества существующих объектов live_paths
live_paths.discard(p) # Убираем и сам каталог
pending_dir_counts[p] = count_index + absorbed_from_buffer # Считаем итоговое число удаленных объектов
print(f"{stamp()} [Удалено] {kind}: {p}") # Выводим, что папка удалена
_arm_summary_timer(p) # Выводим сводку удаленных объектов в папке
else: # Если удалили файл, а не папку
kind = 'Файл' # В логах будет указано, что удалили файл
abs_path = p # Перезаписываем путь файла
now = time.time() # Берем текущее время в виде значения float
with lock: # Оставляем только 1 поток
recent_file_deletes.append((abs_path, now)) # Кладём запись (путь, время) в буфер “ранних удалений”
was_present = abs_path in live_paths # Если файл существовал, то помечаем это
live_paths.discard(abs_path) # Удаляем файл из существующих
parents = [d for d in pending_dir_deletes if is_under(abs_path, d)] # Проверяем, не относится ли удаление к уже помеченной, как удаленная, папке
parent = max(parents, key=len) if parents else None # Берем самый длинный путь
if parent and was_present: # Только если файл реально был "живым"
pending_dir_counts[parent] = pending_dir_counts.get(parent, 0) + 1 # Увеличиваем количество мертвых
if parent: # Если все еще существует запись об удаленном файле, то
_arm_summary_timer(parent) # Подавляем отдельный лог файла; продлеваем задачу сводки родителя
return
print(f"{stamp()} [Удалено] {kind}: {abs_path}") # Выводим сводку об удалении файла
# ============================================================================
# ТОЧКА ВХОДА
# ============================================================================
if __name__ == "__main__":
path = "." # Путь слежки
seed_index(path)
event_handler = MyHandler()
observer = Observer()
observer.schedule(event_handler, path, recursive=True)
observer.start()
print(f"Слежение за {Path(path).resolve()} запущено. Нажмите Ctrl+C для выхода.")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()