监控新访客

Watching New Arrivals

任何枯燥的重复性工作都必须尽可能地简单,否则就会被忽视。我很确定这就是我们发明电脑的原因。备份是挺无聊的。事实上,当涉及到备份时,你要避免任何形式的刺激。所以它们必须尽可能的简单。我有一个脚本,当一个新设备被添加时,由 udev 规则触发。当插入一个磁盘时,这个脚本工作得很好。(很好用。)我有一个 USB 集线器,里面有几个U盘,形成了一个 btrfs raid5,每当我打开U盘集线器的时候,就可以对我的 $home 进行快速备份。在某些情况下,这并不能正常工作。找一个 bash 脚本来检查是否有一个硬盘丢失了,这可不好玩。主要是因为只有合适的语言才会有 Set。我们确实有一种合适的语言。

在 linux 上,找出一个驱动器是否被插入是相当容易的。我们需要做的就是观察 /dev/disk/by-id/ 中是否有新文件出现。我们也可以知道是否发现了新的分区。这个目录看起来是这样的。

$ ls -1 /dev/disk/by-id/
ata-CT120BX500SSD1_1902E16BC135
ata-CT120BX500SSD1_1902E16BC2AA
ata-TOSHIBA_HDWQ140_X83VK0GDFAYG
ata-TOSHIBA_HDWQ140_X83VK0GDFAYG-part1
ata-TOSHIBA_HDWQ140_X83VK0GDFAYG-part2
ata-TOSHIBA_HDWQ140_X83VK0GDFAYG-part3
ata-TOSHIBA_HDWQ140_Y8J9K0TZFAYG
ata-TOSHIBA_HDWQ140_Y8J9K0TZFAYG-part1
ata-TOSHIBA_HDWQ140_Y8J9K0TZFAYG-part2
ata-TOSHIBA_HDWQ140_Y8J9K0TZFAYG-part3
usb-SanDisk_Ultra_USB_3.0_4C530001160708110455-0:0
usb-SanDisk_Ultra_USB_3.0_4C530001190708111070-0:0
usb-SanDisk_Ultra_USB_3.0_4C530001220708110370-0:0
usb-SanDisk_Ultra_USB_3.0_4C530001280708111064-0:0
wwn-0x50000398dc60029a
wwn-0x50000398dc60029a-part1
wwn-0x50000398dc60029a-part2
wwn-0x50000398dc60029a-part3
wwn-0x50000398ebb01681
wwn-0x50000398ebb01681-part1
wwn-0x50000398ebb01681-part2
wwn-0x50000398ebb01681-part3

如果我们寻找任何不以 '-part' \d+ 结尾的东西,我们就得到了一个驱动器。我们也可以通过检查前缀来判断它插在哪里。

sub scan-drive-ids(--> Set) {
    my Set $ret;
    for '/dev/disk/by-id/'.IO.dir.grep(!*.IO.basename.match(/'part' \d+ $/)) {
        $ret ∪= .basename.Str;
        CATCH { default { warn .message } }
    }

    $ret
}

my %last-seen := scan-drive-ids;

集合没有 append 方法,我们可以用 ∪= 代替。现在我们在 %last-seen 中得到了一个可爱的 Set,里面有已经存在的驱动器。现在我们需要等待新的文件出现,并对它们应用集合理论。

react {
    whenever IO::Notification.watch-path('/dev/disk/by-id/') {
        my %just-seen := scan-drive-ids;
        my %new-drives := %just-seen ∖ %last-seen;
        my %old-drives := %last-seen ∩ %just-seen;
        my %removed-drives := %last-seen ∖ %just-seen;
        %last-seen := %just-seen;

        # say ‚old drives: ‘, %old-drives.keys.sort;
        say ‚new drives: ‘, %new-drives.keys.sort || '∅';
        say ‚removed drives: ‘, %removed-drives.keys.sort || '∅';
    }
}

通过将 Set 绑定到 Associative 容器上,我们可以得到for和其他构建的行为。如果我们想在添加某些磁盘时采取行动,我们需要定义包含正确文件名的 Set。

my %usb-backup-set = Set(
    <
    usb-SanDisk_Ultra_USB_3.0_4C530001160708110455-0:0
    usb-SanDisk_Ultra_USB_3.0_4C530001190708111070-0:0
    usb-SanDisk_Ultra_USB_3.0_4C530001220708110370-0:0
    usb-SanDisk_Ultra_USB_3.0_4C530001280708111064-0:0
    >);

my %root-backup-disk = Set(<ata-TOSHIBA_DT01ACA200_8443D04GS>);

my $delayed-check := Channel.new;
my Promise $timeout-promise;

react {
    whenever IO::Notification.watch-path('/dev/disk/by-id/') {
        my %just-seen := scan-drive-ids;
        my %new-drives := %just-seen ∖ %last-seen;
        my %old-drives := %last-seen ∩ %just-seen;
        my %removed-drives := %last-seen ∖ %just-seen;
        %last-seen := %just-seen;

        # say ‚old drives: ‘, %old-drives.keys.sort;
        say ‚new drives: ‘, %new-drives.keys.sort || '∅';
        say ‚removed drives: ‘, %removed-drives.keys.sort || '∅';

        if %usb-backup-set ∩ %new-drives {
            $timeout-promise = Promise.in(5).then: {
                $delayed-check.send: True;
                $timeout-promise = Nil;
            } without $timeout-promise;
        }

        if %root-backup-disk ∩ %new-drives {
            sleep 2;
            backup-root-and-home-to-disk(%root-backup-disk);
        }

        say '';
    }
    whenever $delayed-check {
        my %just-seen := scan-drive-ids;
        if %usb-backup-set ⊆ %just-seen {
            backup-home-to-usb(%usb-backup-set);
        } elsif %usb-backup-set ∩ %just-seen {
            warn 'drive missing in usb set: ' ~ (%usb-backup-set ∖ (%usb-backup-set ∩ %just-seen)).keys;
            reset-usb-hub;
        }
    }
}

我使用 $delayed-check whenever 块来处理其中一个usb棒拒绝上线的情况。usb hub 的 vendorid 和 deviceid 是硬编码的。请注意,状态和启动不能混为一谈。

sub reset-usb-hub(--> True) {
    state $reset-attempt = 0;
    if $reset-attempt++ {
        say ‚already reset, doing nothing‘;
        $reset-attempt = 0;
    } else {
        say ‚Resetting usb hub.‘;
        my $usb_modeswitch = run <usb_modeswitch -v 0x2109 -p 0x0813 --reset-usb>;
        fail ‚resetting usb hub failed‘ unless $usb_modeswitch;
    }
}

整个脚本可以在这里找到。我相信 watch-path 的例子可以使用这个脚本的修改版本。如果你读了它,你可以简单地通过发现集合运算符来判断哪里使用了集合。让 Raku 成为一种面向操作符的语言是个好主意。谢谢你,Larry。

当我把我的备份脚本从 Bash 转到 Raku 的时候,我有了更多关于用适当的语言编写 shell 脚本的发现。我将在接下来的几周内在这里报告这些发现。

by gfldex.

comments powered by Disqus