简体   繁体   中英

GFileMonitor - g_signal_handler_block “changed” signal doesn't block handler?

All,

This may take a moment to set up. I have a small editor project I've worked on the past several months[1]. I originally wanted to implement an inotify watch on the current editor file to protect against modification by a foreing process. I created a custom signal, and routines to add , remove , and monitor (with pselect ) the watch and routines to block and unblock emission of the custom signal to allow for normal save/save as without triggering the callback. The problem I had was how to add the monitor to the gtk_event_loop so that a check was performed on each iteration of the loop. I settled on using g_idle_add and went to the gtk-app-devel list to determine if the approach was reasonable or if there was a better way. The concensus was to use GIO/ GFileMonitor instead of working with inotify directly.

Fast forward to the current problem. I rewrote the implementation to use GFileMonitor/g_file_monitor_file and rewrote the block and unblock routines to block handling of the "changed" signal to allow a normal save/save as without firing the callback. The problem is when I block the instance and handler_id for the callback before saving the file, the callback still fires. When using the inotify implementation with a custom signal, blocking the emission of the signal worked great. I have posted this back to the gtk-app-devel list, but have received nothing in return -- that's why I'm asking here. Why does g_signal_handler_block with the GIO/ GFileMonitor not block handling to the "changed" signal callback? (more importantly, how do I fix it)

note: (MCVE - complete test code is at https://github.com/drankinatty/gtktest ). To build with GtkSourceView2, simply type make with=-DWGTKSOURCEVIEW2 , it will build as bin/gtkwrite , otherwise to build without, simply type make and it will build as bin/gtkedit .

The logic of the relevant code is as follows ( app is an instance of the struct holding relevant editor variables/info and settings) The GIO/GFileMonitor implementation is in gtk_filemon.[ch] and the wrapper around the save function is in gtk_filebuf.c :

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

kwinst *app = g_slice_new (kwinst);

I set the watch with:

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);

Both the instance ( app->filemon ) and handler_id ( app->mfp_handler ) are saved. ( mfp is just short for modified by foreign process ) In order to prevent handling of changes during normal save/save as operations, I created block and unblock functions to prevent the changes to the file from firing the callback, eg show below with debug g_print calls:

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);
}

To implement the block/unblock , I wrap the file 'save/save as' function with the block , then save[2], then unblock , but the callback is still firing on normal saves. eg the relevant part of the save function I have is:

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 */

With the debug g_print statements above, issuing save results in the following output:

$ ./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

It makes no difference whether I include the block/unblock , the firing of the callback is unchanged. I suspect that the problem lies in the fact that the "changed" signal and it handling are implemented through GIO rather than GTK (as the custom signal for the inotify implementation was) and there being some difference in the abiliby to block handling of the "changed" signal using g_signal_handler_block -- though I cannot find a distinction in the documentation. The documentation states:

 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

The application itself makes no use of any explicit threading and does not fork any part of the save. So I'm at a loss as to why the block/unblock does not prevent handling of the "changed" signal. The save is completely wrapped in the block/unblock , unless the g_file_set_contents call is asyncronous, there shouldn't be any timing issues I can see.

Why would calls to g_signal_handler_block/g_signal_handler_unblock fail to prevent handling of the "changed" signal emitted on changes to the current file? I can g_signal_handler_disconnect and nothing fires, but I shouldn't have to disconnect to temporarily block handling. What am I missing?

For completeness the file_monitor_on_changed function is included below along with the footnotes:

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) {}
}

Using g_signal_handler_block works fine in other cases

As a point brought up in the comments, I can confirm I can easily block , unblock other signal handlers without issues. Specifically, when working with the inotify implementation, I created the following custom signal:

/* 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);

Then connecting the signal to a handler and saving the handler_id as follows:

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

The on_modified_foreign callback is a simple test callback for purposes of testing the 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) {}
}

Above dlg_info is just a wrapper to gtk_message_dialog_new to pop up a dialog when the "modified-foreign" signal is emitted.

Then implementing a simple test where one menuitem causes the signal to be emitted, eg:

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

And, finally blocking/unblocking works just fine:

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);
}

It all works fine. Select the bigredbtn menu item, the signal is emitted and up pops the dialog. Then selecting the block menu item and then again trying the bigredbtn -- nothing happens, no dialog, no nothing. Then selecting the unblock menu item and again selecting the bigredbtn , the dialog again pops up on each selection. (and the signals emitted while the handler was blocked were not queued and do not invoke the handler once it is unblocked)

So that is where I'm stuck. A large part of the problem is without picking though the GIO source line-by-line it, to a large extent, is a big black box. All works fine on the GTK side, but when doing the same thing involving a GIO function, the results seem not to work as expected.

Thanks for any other insight you may have on this issue.

footnote 1: https://github.com/drankinatty/gtkwrite

footnote 2: buffer_write_file calls g_file_set_contents to write to disk.

Ok, I've played with it long enough, and I've found a solution. The key seems to be, for whatever reason in the grand scheme of GIO, to call the block and unblock from within the same source that the signals were originally connected in. For example, adding block/unblock functions within the gtk_filemon.c source and then calling from anywhere (as done from a test menu item above) works fine, eg:

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);
}

This allows the calls from the above two functions in menu_status_block_activate and menu_status_unblock_activate to work as intended and successfully block handling of the "changed" signal while blocked, and resume when unblocked. Why it cannot be done by direct call to g_signal_handler_block and g_signal_handler_unblock using the instance and handler_id will just have to remain a documentation mystery for the time being.

note: I'll leave the github.com gtktest code up until mid-April if anyone wants to play with it, thereafter the working code will exist at https://github.com/drankinatty/gtkwrite .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM