GFileMonitor - g_signal_handler_block "changed" 信号不阻止处理程序?

GFileMonitor - g_signal_handler_block "changed" signal doesn't block handler?

全部,

这可能需要一些时间来设置。在过去的几个月里,我有一个小型编辑器项目[1]。我最初想在当前编辑器文件上实现一个 inotify 监视,以防止被外部进程修改。我创建了一个自定义信号和 addremovemonitor(使用 pselect)的例程和 block 和 [=30= 的监视和例程] 发射自定义信号以允许正常 save/save as 而不会触发回调。我遇到的问题是如何将 monitor 添加到 gtk_event_loop 以便对循环的每次迭代执行检查。我决定使用 g_idle_add 并转到 gtk-app-devel 列表以确定该方法是否合理或者是否有更好的方法。共识是使用 GIO/GFileMonitor 而不是直接使用 inotify

快进到当前问题。我重写了使用 GFileMonitor/g_file_monitor_file 的实现并重写了 blockunblock 例程以阻止处理 "changed" 信号以允许正常的 save/save as 而不触发回调.问题是当我在保存文件之前 block 实例和 handler_id 回调时,回调仍然会触发。当使用带有自定义信号的 inotify 实现时,阻止信号的发射效果很好。我已将其发回 gtk-app-devel 列表,但在 return 中没有收到任何信息——这就是我在这里提问的原因。为什么 g_signal_handler_block 和 GIO/GFileMonitor 不阻止对 "changed" 信号回调的处理? (更重要的是,我该如何修复它)

注意:(MCVE - 完整的测试代码在 https://github.com/drankinatty/gtktest)。要使用 GtkSourceView2 构建,只需键入 make with=-DWGTKSOURCEVIEW2,它将构建为 bin/gtkwrite,否则不构建,只需键入 make,它将构建为 bin/gtkedit.

相关代码逻辑如下(appstruct持有相关编辑器variables/info和设置的实例)GIO/GFileMonitor实现在gtk_filemon.[ch] 保存函数的包装在 gtk_filebuf.c:

typedef struct {
    ...
    gchar           *filename;
    GFileMonitor    *filemon;
    gulong          mfp_handler;
    ...
} kwinst;

kwinst *app = g_slice_new (kwinst);

我将手表设置为:

GFile *gfile = g_file_new_for_path (app->filename);
...

/* create monitor for app->filename */
app->filemon = g_file_monitor_file (gfile,
                        G_FILE_MONITOR_NONE,
                        cancellable, &err);
...

/* connect changed signal to monitored file saving ID */
app->mfp_handler = g_signal_connect (G_OBJECT(app->filemon), "changed",
                        G_CALLBACK (file_monitor_on_changed), data);

实例 (app->filemon) 和 handler_id (app->mfp_handler) 都已保存。 (mfp 只是 modified by foreign process 的缩写)为了防止在正常 save/save as 操作期间处理更改,我创建了 block 和 unblock 函数以防止触发回调对文件的更改,例如下面显示调试 g_print 调用:

void file_monitor_block_changed (gpointer data)
{
    kwinst *app = (kwinst *)data;

    if (!app->filemon || !app->mfp_handler) return;

    g_print ("blocking changed (%lu)\n", app->mfp_handler);

    g_signal_handler_block (app->filemon, app->mfp_handler);
}

void file_monitor_unblock_changed (gpointer data)
{
    kwinst *app = (kwinst *)data;

    if (!app->filemon || !app->mfp_handler) return;

    g_print ("unblocking changed (%lu)\n", app->mfp_handler);

    g_signal_handler_unblock (app->filemon, app->mfp_handler);
}

为了实现block/unblock,我用block包装文件'save/save as'函数,然后保存[2],然后unblock,但回调仍然是在正常保存时开火。例如我拥有的保存功能的相关部分是:

if (app->mfp_handler)                   /* current file monitor on file */
    file_monitor_block_changed (app);   /* block "changed" signal */

g_print ("  buffer_write_file (app, filename)\n");
buffer_write_file (app, filename);      /* write to file app->filename */

if (filename)
    file_monitor_add (app);             /* setup monitoring on new name */
else if (app->mfp_handler)
    file_monitor_unblock_changed (app); /* unblock "changed" signal */

使用上面的调试 g_print 语句,发出 save 结果如下:

$ ./bin/gtkwrite
blocking changed (669)
  buffer_write_file (app, filename)
unblocking changed (669)
Monitor Event: File = /home/david/tmp/foo.txt.UY9IXY
G_FILE_MONITOR_EVENT_DELETED
Monitor Event: File = /home/david/tmp/foo.txt
G_FILE_MONITOR_EVENT_CREATED
Monitor Event: File = /home/david/tmp/foo.txt
G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT
Monitor Event: File = /home/david/tmp/foo.txt
G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED

我是否包含 block/unblock 没有区别,回调的触发没有改变。我怀疑问题在于 "changed" 信号及其处理是通过 GIO 而不是 GTK 实现的(因为 inotify 实现的自定义信号是)使用 g_signal_handler_block 阻止处理 "changed" 信号的能力存在一些差异——尽管我在文档中找不到区别。文档指出:

To get informed about changes to the file or directory you are monitoring, 
connect to the “changed” signal. The signal will be emitted in the thread-default 
main context of the thread that the monitor was created in (though if the global 
default main context is blocked, this may cause notifications to be blocked 
even if the thread-default context is still running).

https://developer.gnome.org/gio/stable/GFile.html#g-file-monitor-file

应用程序本身不使用任何显式线程,也不 fork 保存的任何部分。所以我不知道为什么 block/unblock 不阻止处理 "changed" 信号。保存完全包含在 block/unblock 中,除非 g_file_set_contents 调用是异步的,否则我看不到任何时间问题。

为什么调用 g_signal_handler_block/g_signal_handler_unblock 无法阻止对当前文件更改时发出的 "changed" 信号的处理?我可以 g_signal_handler_disconnect 并且什么都不触发,但我不应该 disconnect 来暂时阻止处理。我错过了什么?

为了完整起见,file_monitor_on_changed 函数连同脚注包含在下面:

void file_monitor_on_changed (GFileMonitor *mon, 
                                GFile *file, GFile *other,
                                GFileMonitorEvent evtype,
                                gpointer data)
{
    kwinst *app = (kwinst *)data;

    g_print ("Monitor Event: File = %s\n", g_file_get_parse_name (file));

    switch (evtype)
    {
        case G_FILE_MONITOR_EVENT_CHANGED:
            /* prompt or emit custom signal modified by foreign process */
            g_print ("G_FILE_MONITOR_EVENT_CHANGED\n");
            break;
        case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
            g_print ("G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT\n");
            break;
        case G_FILE_MONITOR_EVENT_DELETED:
            /* avoid firing on normal '.tmp' file delete */
            if (g_strcmp0 (g_file_get_parse_name (file), app->filename)) {
                g_print ("  ignoring 'tmp' file delete.\n");
                break;
            }
            /* prompt or emit custom signal modified by foreign process */
            g_print ("G_FILE_MONITOR_EVENT_DELETED\n");
            /* prompt save file */
            break;
        case G_FILE_MONITOR_EVENT_CREATED:
            g_print ("G_FILE_MONITOR_EVENT_CREATED\n");
            break;
        case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
            g_print ("G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED\n");
            break;
        case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
            g_print ("G_FILE_MONITOR_EVENT_PRE_UNMOUNT\n");
            break;
        case G_FILE_MONITOR_EVENT_UNMOUNTED:
            g_print ("G_FILE_MONITOR_EVENT_UNMOUNTED\n");
            break;
        case G_FILE_MONITOR_EVENT_MOVED:
            g_print ("G_FILE_MONITOR_EVENT_MOVED\n");
            /* prompt save file */
            break;
        case G_FILE_MONITOR_EVENT_RENAMED:
            /* prompt save file */
            g_print ("G_FILE_MONITOR_EVENT_RENAMED\n");
            break;
        case G_FILE_MONITOR_EVENT_MOVED_IN:
            g_print ("G_FILE_MONITOR_EVENT_MOVED_IN\n");
            break;
        case G_FILE_MONITOR_EVENT_MOVED_OUT:
            g_print ("G_FILE_MONITOR_EVENT_MOVED_OUT\n");
            break;
        default:
            g_print ("unknown EVENT on changed signal.\n");
    }

    if (mon || other) {}
}

使用g_signal_handler_block在其他情况下工作正常

作为评论中提到的一点,我可以确认我可以轻松 blockunblock 其他信号处理程序而不会出现问题。具体来说,在使用 inotify 实现时,我创建了以下自定义信号:

/* create signal to monitor file modified by foreign process */
app->SIG_MODFP = g_signal_new ("modified-foreign",
                                GTK_TYPE_TEXT_BUFFER,
                                GTK_RUN_ACTION,
                                0,
                                NULL,
                                NULL,
                                NULL,
                                G_TYPE_NONE,
                                1,
                                G_TYPE_POINTER);

然后将信号连接到处理程序并保存 handler_id 如下:

/* custom signals */
app->mfp_handler2 = g_signal_connect (GTK_TEXT_BUFFER(app->buffer), 
                  "modified-foreign",
                  G_CALLBACK (on_modified_foreign), app);

on_modified_foreign 回调是一个简单的测试回调,用于测试 block/unblock:

void on_modified_foreign (GtkTextBuffer *buffer,
                        kwinst *app)
{
    dlg_info ("File has changed on disk, reload?", "Modified by Foreign Process");

    if (buffer || app) {}
}

以上 dlg_info 只是 gtk_message_dialog_new 的包装器,用于在发出 "modified-foreign" 信号时弹出对话框。

然后执行一个简单的测试,其中一个菜单项会导致发出信号,例如:

void menu_status_bigredbtn_activate (GtkMenuItem *menuitem, kwinst *app)
{
    g_signal_emit_by_name (G_OBJECT(app->buffer), "modified-foreign::", app);
}

最后 blocking/unblocking 工作正常:

void menu_status_block_activate (GtkMenuItem *menuitem, kwinst *app)
{
    if (!app->mfp_handler2) return;
    GtkTextBuffer *buffer = GTK_TEXT_BUFFER(app->buffer);
    g_signal_handler_block (buffer, app->mfp_handler2);
}

void menu_status_unblock_activate (GtkMenuItem *menuitem, kwinst *app)
{
    if (!app->mfp_handler2) return;
    GtkTextBuffer *buffer = GTK_TEXT_BUFFER(app->buffer);
    g_signal_handler_unblock (buffer, app->mfp_handler2);
}

一切正常。 Select bigredbtn 菜单项,发出信号向上弹出对话框。然后选择 block 菜单项,然后再次尝试 bigredbtn -- 没有任何反应,没有对话框,什么也没有。然后选择 unblock 菜单项并再次选择 bigredbtn,每次选择都会再次弹出对话框。 (并且在处理程序被阻塞时发出的信号没有排队,并且一旦解除阻塞就不会调用处理程序)

这就是我卡住的地方。很大一部分问题是没有逐行挑选GIO源代码,在很大程度上,它是一个大黑盒子。在 GTK 方面一切正常,但是当使用 GIO 功能做同样的事情时,结果似乎没有按预期工作。

感谢您对此问题的任何其他见解。

脚注 1: https://github.com/drankinatty/gtkwrite

脚注 2: buffer_write_file 调用 g_file_set_contents 写入磁盘。

好的,我已经玩够了,我找到了解决办法。无论出于何种原因,在 GIO 的宏伟计划中,关键似乎是从信号最初连接的同一源中调用 blockunblock。例如,添加 block/unblock gtk_filemon.c 源中的函数,然后从任何地方调用(如上面的测试菜单项所做的那样)工作正常,例如:

void file_monitor_block_changed (gpointer data)
{
    kwinst *app = (kwinst *)data;

    if (!app->filemon || !app->mfp_handler) return;

    g_signal_handler_block (app->filemon, app->mfp_handler);
}

void file_monitor_unblock_changed (gpointer data)
{
    kwinst *app = (kwinst *)data;

    if (!app->filemon || !app->mfp_handler) return;

    g_signal_handler_unblock (app->filemon, app->mfp_handler);
}

这允许来自 menu_status_block_activatemenu_status_unblock_activate 中的上述两个函数的调用按预期工作,并在阻塞时成功阻止处理 "changed" 信号,并在解除阻塞时恢复。为什么不能通过使用 实例 handler_id 直接调用 g_signal_handler_blockg_signal_handler_unblock 来完成只是暂时必须保持文档之谜。

注意: 如果有人想玩的话,我会将 github.com gtktest 代码保留到 4 月中旬,此后工作代码将存在于 https://github.com/drankinatty/gtkwrite.