Как выполнить shell-скрипт при подключении USB-устройства?

Вопрос или проблема

Я хочу выполнить скрипт, когда я подключаю устройство к моей Linux машине. Например, запустить xinput на мыши или backupscript на определенном диске.

Я видел много статей по этой теме, в последний раз здесь и здесь. Но я просто не могу заставить это работать.

Вот несколько простых примеров, чтобы попытаться получить хоть какой-то ответ.

/etc/udev/rules.d/test.rules

#KERNEL=="sd*", ATTRS{vendor}=="*", ATTRS{model}=="*", ATTRS{serial}=="*", RUN+="/usr/local/bin/test.sh"
#KERNEL=="sd*", ACTION=="add", "SUBSYSTEM=="usb", ATTRS{model}=="My Book 1140    ", ATTRS{serial}=="0841752394756103457194857249", RUN+="/usr/local/bin/test.sh"
#ACTION=="add", "SUBSYSTEM=="usb", RUN+="/usr/local/bin/test.sh"
#KERNEL=="sd*", ACTION=={add}, RUN+="/usr/local/bin/test.sh"
KERNEL=="sd*", RUN+="/usr/local/bin/test.sh"
KERNEL=="*", RUN+="/usr/local/bin/test.sh"

/usr/local/bin/test.sh

#!/usr/bin/env bash
echo touched >> /var/log/test.log

if [ "${ACTION}" = "add" ] && [ -f "${DEVICE}" ]
then
    echo ${DEVICE} >> /var/log/test.log
fi

Папка rules отслеживается inotify и должна быть активной сразу. Я продолжаю переподключать клавиатуру, мышь, планшет, флешку и usb-диск, но ничего. Файл журнала не записывается.

Итак, какой был бы самый простой способ хотя бы узнать, что что-то работает? Легче работать с тем, что работает, чем с тем, что нет.

Если вы хотите запустить скрипт на определенном устройстве, вы можете использовать идентификаторы поставщика и продукта.

  • В /etc/udev/rules.d/test.rules:

    ATTRS{idVendor}=="152d", ATTRS{idProduct}=="2329", RUN+="/tmp/test.sh"
    
  • в test.sh:

    #! /bin/sh
    
    env >>/tmp/test.log
    file "/sys${DEVPATH}" >>/tmp/test.log
    
    if [ "${ACTION}" = add -a -d "/sys${DEVPATH}" ]; then
    echo "add ${DEVPATH}" >>/tmp/test.log
    fi
    

С помощью env вы можете увидеть, какая среда установлена из udev, а с помощью file вы узнаете тип файла.

Конкретные атрибуты вашего устройства можно обнаружить с помощью lsusb

lsusb

выдает


Bus 001 Device 016: ID 152d:2329 JMicron Technology Corp. / JMicron USA Technology Corp. JM20329 SATA Bridge

Это не напрямую относится к вашему вопросу, а к тому, что вы делаете. Если вы запускаете скрипт резервного копирования из udev, вы столкнетесь с двумя основными проблемами:

  1. Ваш скрипт может быть запущен до того, как устройство будет готово и может быть смонтировано, вам нужно сохранить условие KERNEL==”sd*”, если вы хотите использовать /dev узел для монтирования
  2. Более важно, если ваш скрипт занимает некоторое время для выполнения (что может легко произойти с скриптом резервного копирования), он будет убит вскоре после его запуска (примерно через 5 секунд)
  3. Вы столкнетесь со многими сложными проблемами с разрешениями пользователя

Мой совет – создать скрипт в вашем домашнем каталоге, который слушает именованный канал и будет запущен асинхронно, например:

#!/bin/bash

PIPE="/tmp/IomegaUsbPipe"
REMOTE_PATH="/path/to/mount/point"
LOCAL_PATH="/local/path/"

doSynchronization()
{
  #ваше резервное копирование здесь
}

trap "rm -f $PIPE" EXIT

#Если канал не существует, создайте его
if [[ ! -p $PIPE ]]; then
    mkfifo $PIPE
fi

#Если диск уже подключен при запуске, выполните синхронизацию
if [[ -e "$REMOTE_PATH" ]]
then
    doSynchronization
fi

#Создайте постоянный цикл для отслеживания подключения usb
while true
do
    if read line <$PIPE; then
        #Проверьте сообщение, прочитанное из fifo
        if [[ "$line" == "connected" ]]
        then
            #USB был подключен, подождите, пока диск будет смонтирован KDE
            while [[ ! -e "$REMOTE_PATH" ]]
            do
                sleep 1
            done
            doSynchronization
        else
            echo "Необработанное сообщение из fifo : [$line]"
        fi
    fi
done
echo "Чтение завершено"

Примечание: я использую автоматическое монтирование с kde, поэтому проверяю, появилась ли папка. Вы можете передать параметр /dev/sd* в fifo из правила udev и смонтировать его самостоятельно в скрипте. Чтобы записать в fifo, не забывайте, что udev не является оболочкой и что перенаправление не работает. Ваш RUN должен быть таким:

RUN+=”/bin/sh -c ‘/bin/echo connected >> /tmp/IomegaUsbPipe'”

Я опубликовал решение на https://askubuntu.com/a/516336 и также копирую решение здесь.

Я написал скрипт на Python, используя pyudev, который я оставляю работать в фоновом режиме. Этот скрипт слушает udev события (поэтому он очень эффективен) и выполняет все, что я хочу. В моем случае, он выполняет команды xinput для настройки моих устройств (ссылка на самую новую версию).

Вот краткая версия того же скрипта:

#!/usr/bin/env python3

import pyudev
import subprocess

def main():
    context = pyudev.Context()
    monitor = pyudev.Monitor.from_netlink(context)
    monitor.filter_by(subsystem='usb')
    monitor.start()

    for device in iter(monitor.poll, None):
        # Я могу добавить больше логики здесь, чтобы запускать различные скрипты для разных устройств.
        subprocess.call(['/home/foo/foobar.sh', '--foo', '--bar'])

if __name__ == '__main__':
    main()

В качестве расширения к отличному ответу Дэнилсона Сэ Майя: обычно правила udev срабатывают несколько раз. В зависимости от того, какой именно скрипт вы запускаете, может быть более желательно запускать его только один раз за подключение/отключение. Добавив следующую логику, после того как произошло событие, мы будем выполнять наш скрипт только после одной секунды без дальнейших событий. В противном случае мы блокируемся бесконечно. Прекрасно.

#!/usr/bin/env python3

import pyudev
import subprocess
import sys
from functools import partial

def main():
    context = pyudev.Context()
    monitor = pyudev.Monitor.from_netlink(context)
    monitor.filter_by(subsystem='usb')
    monitor.start()

    while True:
        # избегаем слишком частых запусков внешней команды, ожидая, пока
        # другие события не будут происходить и мы не истечем по времени
        device = monitor.poll(timeout=None)
        for device in iter(partial(monitor.poll, 1), None):
            pass
        subprocess.call(['usb-keyboard.sh'])

if __name__ == '__main__':
    try:
        main()
    except (BrokenPipeError, KeyboardInterrupt) as e:
        # избегаем дополнительных ошибок разрыва канала. см. https://stackoverflow.com/a/26738736
        sys.stderr.close()
        exit(e.errno)

Современный способ запуска долгосрочного скрипта с использованием ваших обычных разрешений пользователя – это использование пользовательских сервисов systemd, и их запуск через udev.

Смотрите также Как автоматически запускать GUI приложение при подключении USB устройства?

Создайте сервис systemd в ~/.local/share/systemd/user/some-descriptive-name.service

[Unit]
Description=Текстовое описание того, что это делает

[Service]
Type=simple
ExecStart=/usr/local/bin/your_script.sh

Затем создайте правило udev в /etc/udev/rules.d/10-some-name.rules для запуска сервиса. Убедитесь, что имя сервиса совпадает:

SUBSYSTEM=="usb", ATTRS{idVendor}=="vendor_id", ATTRS={idProduct}=="product_id", ACTION=="add", ENV{SYSTEMD_USER_WANTS}+="some-descriptive-name.service", TAG+="systemd"

Перезагрузите udev и systemd (или перезагрузите компьютер):

sudo systemctl daemon-reload
systemctl --user daemon-reload
sudo systemctl restart udev.service

Чтобы запустить скрипт при загрузке, когда USB устройство подключено, я использую решение ниже:

Отформатируйте флешку или любое другое USB-устройство хранения данных и дайте ему имя при этом. Затем
в /etc/rc.local
добавьте строку ls -q /dev/disk/by-label > /home/pi/label.txt

это создаст текстовый файл с именем label.txt (может быть любым другим именем)

затем снова в /etc/rc.local добавьте еще 2 строки:

if  grep -q USB_drive_name /home/pi/label.txt; then
sudo /home/pi/script.sh

Теперь каждый раз, когда вставляется флешка с именем USB_drive_name, будет запущен скрипт.

С небольшими модификациями вышеуказанное решение может использоваться, когда система уже запущена и работает.

Ответ или решение

Для автоматического выполнения сценария при подключении USB-устройства на Linux-системе, вы можете использовать udev, систему динамичного управления устройствами. Udev позволяет вам задавать правила, которые будут выполнять определенные действия при появлении или удалении устройств.

Теория

Основным принципом работы udev является возможность задавать правила для наблюдения за событиями, связанными с устройствами. Когда устройство подключается к системе или отключается от нее, создается событие в udev, которое может запускать выполнение команд или сценариев. Чтобы задействовать эту функциональность, необходимо создать правило в каталоге /etc/udev/rules.d/, которое будет идентифицировать устройство и выполнять указанный сценарий.

Пример

  1. Определите параметры устройства:

    Для начала, вам нужно узнать ID производителя и продукта вашего устройства. Это можно сделать с помощью команды lsusb, которая выводит список подключенных USB-устройств. Пример вывода:

    Bus 001 Device 004: ID 1234:5678 Производитель Продукт

    Здесь 1234 — это idVendor, а 5678 — idProduct.

  2. Создайте правило udev:

    На основе полученной информации создайте правило в файле, например, /etc/udev/rules.d/90-custom.rules:

    SUBSYSTEM=="usb", ATTR{idVendor}=="1234", ATTR{idProduct}=="5678", ACTION=="add", RUN+="/usr/local/bin/test.sh"

    Это правило запускает test.sh при подключении устройства с заданными idVendor и idProduct.

  3. Напишите сценарий выполнения:

    Сценарий /usr/local/bin/test.sh может быть таким:

    #!/usr/bin/env bash
    echo "Устройство подключено" >> /var/log/usb_device.log

    Этот простой скрипт записывает в журнал информацию о подключении устройства.

  4. Проверьте выполнение:

    Перезагрузите udev службу, чтобы применить изменения:

    sudo udevadm control --reload-rules
    sudo udevadm trigger

    Подключите устройство и проверьте содержимое /var/log/usb_device.log, чтобы убедиться, что скрипт был выполнен.

Применение

Такая конфигурация может быть полезна в различных сценариях. Например, вы можете запускать резервное копирование данных, автоматическое монтирование дисков, выполнение определенных команд для ввода/вывода при подключении периферийных устройств, таких как мыши или клавиатуры, и многое другое. Это может быть особенно полезно в корпоративных системах, где требуется соблюдать определенные политики управления устройствами.

Помимо использования udev, возможно использование pyudev — обертки для Python, которая позволяет управлять событиями устройств также эффективно и гибко. С его помощью вы можете создавать сложные логики обработки событий прямо в скриптах на Python.

При использовании udev важно учесть некоторые ограничения: сценарии, вызванные udev, должны быть короткими и не требующими длительного выполнения, так как они являются частью процесса управления устройствами и могут быть убиты, если работают слишком долго. Если требуется выполнить длительное действие, рекомендуется инициировать его как фоновый процесс или использовать более сложные решения с использованием systemd-триггеров или фоновых демон-сервисов.

Таким образом, настройка системы таким образом, чтобы она запускала определенные сценарии при подключении USB-устройств, позволяет автоматизировать и упростить многие рутинные операции, повышая тем самым общую эффективность работы системы.

Оцените материал
Добавить комментарий

Капча загружается...