GApplication argument handling via OptionArgFunc callback, in main option group?

I asked this over on StackOverflow a couple of days ago, but no responses so I figured I’d try here. Feel free to respond in either place.


Writing a Gtk (3.0, for the moment) application’s main() function and installing the standard GLib/Gio GApplication scaffolding, I’ve hit a snag when it comes to command-line argument processing.

I’m already convinced I don’t want to handle the commandline myself if I can avoid it, because that’s exactly what the toolkits are supposed to be there for. At least, in theory.

So the command line is set up by defining a GOptionEntry array and passing it to g_application_add_main_option_entries() in the usual fashion:

GOptionEntry options[] =
{
    { "verbose", 'v', 0, 
        G_OPTION_ARG_NONE, &opt_verbose,
        "Be verbose", NULL },
    { "otherarg", 'o', 0,
        G_OPTION_ARG_STRING, &opt_other,
        "Secret", NULL },
    { NULL }
};
app = gtk_application_new("org.me.myapp", G_APPLICATION_NON_UNIQUE);
g_application_add_main_option_entries(G_APPLICATION(app), options);

So far, so good. But now I want to include a --debug flag, and modify the G_MESSAGES_DEBUG environment variable to append the G_LOG_DOMAIN when debugging is enabled. (I’d rather not write my own log handling, either.)

That part’s also easy enough, using an argument-processing callback:

gboolean
enable_debug (const gchar* option_name, const gchar* value,
              gpointer data, GError** error)
{
    // ...
}

GOptionEntry options[] =
{
    { "verbose", 'v', 0, 
        G_OPTION_ARG_NONE, &opt_verbose,
        "Be verbose", NULL },
    { "otherarg", 'o', 0,
        G_OPTION_ARG_STRING, &opt_other,
        "Secret", NULL },
    { "debug", 'd', G_OPTION_FLAG_NO_ARG,
        G_OPTION_ARG_CALLBACK, &enable_debug,
        "Enable debug output", NULL },
    { NULL }
};

But, now say I want to actually have that gpointer data set to something other than NULL, and pass some user data to enable_debug(). (Specifically, give it a place to also store the debug flag, when enabled.)

According to the docs, data carries…

User data added to the GOptionGroup containing the option when it was created with g_option_group_new()

But I’m not using g_option_group_new().

I’d be happy to use g_option_group_new(), but AFAICT there’s no way to create the main GApplication option group “by hand” (by calling g_option_group_new()). It’s only created implicitly by calling g_application_add_main_option_entries(), which doesn’t allow user data to be attached to the group.

And if I were to create a new GOptionGroup for the --debug option, using g_option_group_new() followed by g_application_add_option_group(), --debug would be relegated to a different option group than the rest of my arguments, and it wouldn’t be shown in the main --help output — the user would have to run myapp --help-debugging (or whatever I named it) in order to see the --debug flag in the argument synopsis.

Do I have all that right? Are GOptionArgFunc callbacks that take user data really mutually exclusive with arguments that are part of the application’s main option group? Is there a reason why that would be the case, or is it just a weird hole in the API?

Callbacks don’t work with GApplication’s command line parsing, since the command line will be serialised into a GVariant dictionary and may be sent over the D-Bus connection to the primary instance.

Never, ever do that. Setting environment variables post-construction is a recipe for having a terrible time.

If you want to override the logging facilities in your application, use g_log_set_write_func() in your application.

Mmm, well, in this particular case it’s configured G_APPLICATION_NON_UNIQUE so every instance is a primary instance (…right?), but I guess since that’s not the norm and it couldn’t be generalized to the more common case… OK, I can accept that.

Urgh. That’s exactly what I was hoping I could avoid doing. I was hoping I might be able to catch it early enough, with the callback, but I guess any time after gtk_application_new() is already way too late.

*sigh* What I’d really like is a version of g_log_set_debug_enabled() that took a logging domain, and enabled debugging messages only for that domain, instead of being all-or-nothing. That’d make it the equivalent of G_MESSAGES_DEBUG="<domain>" set in the environment before launch. I’d really like to unify the methods so that there’s no difference between setting the envvar and passing --debug on the command line.

Buuuuut, I suppose writing a custom log handler technically achieves that, too, as long as I use g_log_writer_default_would_drop() to check the envvar.

This kind of slightly unusual use case is exactly why g_log_set_writer_func() exists, because otherwise GLib would eventually end up growing its own domain language for specifying log message routing. Writing a log writer function is fairly straightforward, and should be more commonplace.

As Emmanuele said, you need to look at g_log_get_debug_enabled(). See also Gio.DebugController, which will allow you to enable/disable debug output at runtime.

Well… but in a sense, it already has one: G_MESSAGES_DEBUG. Granted, it’s a very_simple_ language, that only accepts a space-separated list of log domains to enable debug output for… but the default logger still supports that configuration. It’s just that it supports it only via the envvar, with no way to influence it programmatically from within the code itself at runtime, which… still strikes me as a bit odd, TBH.

?? @ebassi didn’t mention g_log_get_debug_enabled(), I did (well, I mentioned _set_debug_enabled()), and only in the context of a lament that it didn’t help me at all. I could certainly call g_log_set_debug_enabled(TRUE) from the code, but IIUC that’s basically the equivalent of setting G_MESSAGES_DEBUG="all" in the process environment — it’s an all-or-nothing switch, whereas the envvar allows finer-grained control.

Tangentially, is it just me, or are the docs on writer functions inherently contradictory?

From GLib.log_set_writer_func():

There can only be one writer function. It is an error to set more than one.

From GLib.LogWriterFunc (emphasis and editorial commentary added):

Writer functions [note: plural!] should return G_LOG_WRITER_HANDLED if they handled the log message successfully or if they deliberately ignored it. If there was an error handling the message (for example, if the writer function is meant to send messages to a remote logging server and there is a network error), it should return G_LOG_WRITER_UNHANDLED . This allows writer functions to be chained and fall back to simpler handlers in case of failure.

How do you chain a single, solitary, Highlander-rules writer function?

The way to influence it programmatically from within the code itself at runtime is to write your own log writer function.

To put it another way, you can think of G_MESSAGES_DEBUG as an implementation detail or configuration knob on the default log writer function. It doesn’t have to be the single control channel for all logging.

Oops, indeed he didn’t mention that. Not sure how I got that in my head.

In any case, writing your own log writer function allows finer-grained control.

“Writer functions” means “implementations of GLogWriterFunc”. Different libraries might provide different log writer functions if they want. The docs are not inconsistent, but if you have some improvements in mind then please submit a merge request and it’ll be gratefully received.

There can only be one writer function chosen at the top level in the process though. That writer function chooses how to route log messages.

static GLogWriterOutput
my_log_writer_func (GLogLevelFlags log_level, const GLogField *fields, gsize n_fields, gpointer user_data)
{
  /* Do whatever you want here. This drops the message randomly. */
  if (rand ())
    return G_LOG_WRITER_HANDLED;

  /* This tries to log to a network log sink, and falls back to the default writer if that fails. */
  if (some_network_log_writer (log_level, fields, n_fields, user_data) == G_LOG_WRITER_HANDLED)
    return G_LOG_WRITER_HANDLED;

  return g_log_writer_default (log_level, fields, n_fields, user_data);
}