/*
* This file is part of Cockpit.
*
* Copyright (C) 2014 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see .
*/
#include "config.h"
#include "cockpitfswatch.h"
#include "cockpitfsread.h"
#include "common/cockpitjson.h"
#include
#include
#include
#include
#include
#include
/**
* CockpitFswatch:
*
* A #CockpitChannel that watches a file or directory.
*
* The payload type for this channel is 'fswatch1'.
*/
#define COCKPIT_FSWATCH(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_FSWATCH, CockpitFswatch))
typedef struct {
CockpitChannel parent;
const gchar *path;
GFileMonitor *monitor;
guint sig_changed;
} CockpitFswatch;
typedef struct {
CockpitChannelClass parent_class;
} CockpitFswatchClass;
G_DEFINE_TYPE (CockpitFswatch, cockpit_fswatch, COCKPIT_TYPE_CHANNEL);
static void
cockpit_fswatch_recv (CockpitChannel *channel,
GBytes *message)
{
cockpit_channel_fail (channel, "protocol-error",
"received unexpected message in fswatch channel");
}
static void
cockpit_fswatch_init (CockpitFswatch *self)
{
}
gchar *
cockpit_file_type_to_string (GFileType file_type)
{
switch (file_type) {
case G_FILE_TYPE_REGULAR:
return "file";
case G_FILE_TYPE_DIRECTORY:
return "directory";
case G_FILE_TYPE_SYMBOLIC_LINK:
return "link";
case G_FILE_TYPE_SPECIAL:
return "special";
default:
return "unknown";
}
}
static gchar *
event_type_to_string (GFileMonitorEvent event_type)
{
switch (event_type) {
case G_FILE_MONITOR_EVENT_CHANGED:
return "changed";
case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
return "done-hint";
case G_FILE_MONITOR_EVENT_DELETED:
return "deleted";
case G_FILE_MONITOR_EVENT_CREATED:
return "created";
case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
return "attribute-changed";
case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
return "pre-unmount";
case G_FILE_MONITOR_EVENT_UNMOUNTED:
return "unmounted";
case G_FILE_MONITOR_EVENT_MOVED:
return "moved";
default:
return "unknown";
}
}
void
cockpit_fswatch_emit_event (CockpitChannel *channel,
GFile *file,
GFile *other_file,
GFileMonitorEvent event_type)
{
JsonObject *msg;
GBytes *msg_bytes;
msg = json_object_new ();
json_object_set_string_member (msg, "event", event_type_to_string (event_type));
if (file)
{
char *p = g_file_get_path (file);
char *t = cockpit_get_file_tag (p);
json_object_set_string_member (msg, "path", p);
json_object_set_string_member (msg, "tag", t);
if (event_type == G_FILE_MONITOR_EVENT_CREATED)
{
GError *error = NULL;
GFileInfo *info = g_file_query_info (file,
G_FILE_ATTRIBUTE_STANDARD_TYPE,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
NULL, &error);
if (info)
{
json_object_set_string_member
(msg, "type", cockpit_file_type_to_string (g_file_info_get_file_type (info)));
g_object_unref (info);
}
g_clear_error (&error);
}
g_free (p);
g_free (t);
}
if (other_file)
{
char *p = g_file_get_path (other_file);
json_object_set_string_member (msg, "other", p);
g_free (p);
}
msg_bytes = cockpit_json_write_bytes (msg);
json_object_unref (msg);
cockpit_channel_send (channel, msg_bytes, TRUE);
g_bytes_unref (msg_bytes);
}
static void
on_changed (GFileMonitor *monitor,
GFile *file,
GFile *other_file,
GFileMonitorEvent event_type,
gpointer user_data)
{
CockpitFswatch *self = COCKPIT_FSWATCH (user_data);
cockpit_fswatch_emit_event (COCKPIT_CHANNEL(self), file, other_file, event_type);
}
static void
cockpit_fswatch_prepare (CockpitChannel *channel)
{
CockpitFswatch *self = COCKPIT_FSWATCH (channel);
JsonObject *options;
GError *error = NULL;
const gchar *path;
COCKPIT_CHANNEL_CLASS (cockpit_fswatch_parent_class)->prepare (channel);
options = cockpit_channel_get_options (channel);
if (!cockpit_json_get_string (options, "path", NULL, &path))
{
cockpit_channel_fail (channel, "protocol-error",
"invalid \"path\" option for fswatch channel");
goto out;
}
else if (path == NULL || *path == 0)
{
cockpit_channel_fail (channel, "protocol-error",
"missing \"path\" option for fswatch channel");
goto out;
}
GFile *file = g_file_new_for_path (path);
GFileMonitor *monitor = g_file_monitor (file, 0, NULL, &error);
g_object_unref (file);
if (monitor == NULL)
{
cockpit_channel_fail (channel, "internal-error", "%s: %s", self->path, error->message);
goto out;
}
self->monitor = monitor;
self->sig_changed = g_signal_connect (self->monitor, "changed", G_CALLBACK (on_changed), self);
cockpit_channel_ready (channel, NULL);
out:
g_clear_error (&error);
}
static void
cockpit_fswatch_dispose (GObject *object)
{
CockpitFswatch *self = COCKPIT_FSWATCH (object);
if (self->monitor)
{
if (self->sig_changed)
g_signal_handler_disconnect (self->monitor, self->sig_changed);
self->sig_changed = 0;
// HACK - It is not generally safe to just unref a GFileMonitor.
// Some events might be on their way to the main loop from its
// worker thread and if they arrive after the GFileMonitor has
// been destroyed, bad things will happen.
//
// As a workaround, we cancel the monitor and then spin the main
// loop a bit until nothing is pending anymore.
//
// https://bugzilla.gnome.org/show_bug.cgi?id=740491
g_file_monitor_cancel (self->monitor);
for (int tries = 0; tries < 10; tries ++)
{
if (!g_main_context_iteration (NULL, FALSE))
break;
}
}
G_OBJECT_CLASS (cockpit_fswatch_parent_class)->dispose (object);
}
static void
cockpit_fswatch_finalize (GObject *object)
{
CockpitFswatch *self = COCKPIT_FSWATCH (object);
g_clear_object (&self->monitor);
G_OBJECT_CLASS (cockpit_fswatch_parent_class)->finalize (object);
}
static void
cockpit_fswatch_class_init (CockpitFswatchClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
gobject_class->dispose = cockpit_fswatch_dispose;
gobject_class->finalize = cockpit_fswatch_finalize;
channel_class->prepare = cockpit_fswatch_prepare;
channel_class->recv = cockpit_fswatch_recv;
}
/**
* cockpit_fswatch_open:
* @transport: the transport to send/receive messages on
* @channel_id: the channel id
* @path: the path name of the file to read
*
* This function is mainly used by tests. The usual way
* to get a #CockpitFswatch is via cockpit_channel_open()
*
* Returns: (transfer full): the new channel
*/
CockpitChannel *
cockpit_fswatch_open (CockpitTransport *transport,
const gchar *channel_id,
const gchar *path)
{
CockpitChannel *channel;
JsonObject *options;
g_return_val_if_fail (channel_id != NULL, NULL);
options = json_object_new ();
json_object_set_string_member (options, "path", path);
json_object_set_string_member (options, "payload", "fswatch1");
channel = g_object_new (COCKPIT_TYPE_FSWATCH,
"transport", transport,
"id", channel_id,
"options", options,
NULL);
json_object_unref (options);
return channel;
}