Files
glib/gio/win32/gwin32filemonitorutils.c
Chun-wei Fan 55847d9258 Continue to Improve File Monitoring on Windows
This updates the file monitoring on Windows by:
-Clean up the code, and ensure things are indeed being freed when we exit.
-Have attributes changes in files (when monitoring directories) properly
 done.
-Split up the code, to ease readability

To Possibly Do:
-Check on whether we want to monitor the case when we try to pull out a USB
 stick without ejecting in in Windows (the monitoring mechanism
 understandbly denies such ejection requests, since the monitored
 file/directory is obviously in use in this case)
-Investigate on the interesting/boredom algorithm, whether we can do it
 here.

Possible Limitations:
-If moving a file out of the directory (or vice versa), the system reports
 that as a delete event (or create event in the vice versa case), so in the
 current form without using NTFS hournals (which is not optimal as that
 would trigger UAC), so we can't reliably emit MOVE OUT or MOVE IN events
 on Windows.
-Hard links are supported transparently, but notifications are only sent
 by the system on changes when the monitored file/directory (or monitored
 hard link) is being changed, when the file is modified via the hard link
 or original file respectively in this case.

https://bugzilla.gnome.org/show_bug.cgi?id=730116
2015-03-30 14:24:38 +08:00

455 lines
17 KiB
C

/* GIO - GLib Input, Output and Streaming Library
*
* Copyright (C) 2006-2015 Red Hat, Inc.
* Copyright (C) 2015 Chun-wei Fan
*
* This library 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 of the License, or (at your option) any later version.
*
* This library 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 this library; if not, see <http://www.gnu.org/licenses/>.
*
* Author: Vlad Grecescu <b100dian@gmail.com>
* Author: Chun-wei Fan <fanc999@yahoo.com.tw>
*
*/
#include "config.h"
#include "gwin32filemonitorutils.h"
#include "gwin32filemonitorevents.h"
#include "gio/gfile.h"
/*
* We are using the Wide/Unicode (W) variants of the Windows APIs as:
* -There isn't a ReadDirectoryChangesA()
* -We want to support long paths, over 256 characters, and using the W variants is required in this case
*/
/*
* Note: We don't support monitoring for "normal" unmounts in Windows as the Windows file/directory monitoring
* mechanism will disallow the unmounting of the monitored volume (unless of course we pull out the USB stick,
* for instance, without ejecting it
*/
/*
* On Windows, there is the notion of hard links, but they are treated more or less like standard files, but any
* changes to them will also cause a notification to be sent for the directory where the original file resides,
* so ReadDirectoryChangesW() also works with hard links, though transparently, so there isn't a special code path
* for hard links here. The notification, however, may be sent only when the file (or hard link) in question
* is being accessed.
*/
gboolean
g_win32_file_monitor_long_pfx_needed (const gchar* path)
{
if (g_utf8_strlen (path, -1) > MAX_PATH)
return TRUE;
else
return FALSE;
}
GHashTable*
g_win32_file_monitor_dir_refresh_attribs (GWin32FileMonitorPrivate *monitor,
GWin32FileMonitorInfo *info)
{
GHashTable *ht_attribs;
WIN32_FILE_ATTRIBUTE_DATA *attrib_data;
gchar *fullpath, *dirname, *dirname_casefold;
wchar_t *wfullpath;
HANDLE hFind = INVALID_HANDLE_VALUE;
WIN32_FIND_DATA file_data = {0,};
if (info->isfile)
return NULL;
fullpath = g_strconcat (info->dirname_with_long_prefix,
G_DIR_SEPARATOR_S,
info->name,
G_DIR_SEPARATOR_S,
"*",
NULL);
wfullpath = g_utf8_to_utf16 (fullpath, -1, NULL, NULL, NULL);
dirname = g_strconcat (info->dirname_with_long_prefix + STRIP_PFX_LENGTH,
G_DIR_SEPARATOR_S,
info->name,
NULL);
dirname_casefold = g_utf8_casefold (dirname, -1);
hFind = FindFirstFileExW (wfullpath,
FindExInfoBasic,
&file_data,
0,
NULL,
0);
ht_attribs = g_hash_table_lookup (monitor->ht_files_attribs, dirname_casefold);
if (ht_attribs == NULL)
{
ht_attribs = g_hash_table_new_full (g_str_hash,
g_str_equal,
g_free,
g_free);
}
else
g_hash_table_remove_all (ht_attribs);
g_free (dirname_casefold);
g_free (dirname);
if (hFind == INVALID_HANDLE_VALUE)
/*
* Initially, we won't get here, if we started monitoring on a non-existing directory.
* If we do later on, the directory was moved or deleted, forget about updating the info
*/
return NULL;
else
{
do
{
attrib_data = g_new0 (WIN32_FILE_ATTRIBUTE_DATA, 1);
attrib_data->dwFileAttributes = file_data.dwFileAttributes;
attrib_data->ftCreationTime = file_data.ftCreationTime;
attrib_data->ftLastAccessTime = file_data.ftLastAccessTime;
attrib_data->ftLastWriteTime = file_data.ftLastWriteTime;
attrib_data->nFileSizeHigh = file_data.nFileSizeHigh;
attrib_data->nFileSizeLow = file_data.nFileSizeLow;
g_hash_table_insert (ht_attribs,
g_utf8_casefold (file_data.cFileName, -1),
attrib_data);
}
while (FindNextFile (hFind, &file_data));
FindClose (hFind);
}
return ht_attribs;
}
/*
* ReadDirectoryChangesW() can return the normal filename or the
* "8.3" format filename, so we need to keep track of both these names
* so that we can check against them later when it returns
*/
void
g_win32_file_monitor_set_names (GWin32FileMonitorInfo *info)
{
wchar_t *wfullpath, *wbasename_long, *wbasename_short;
wchar_t wlongname[MAX_PATH_LONG];
wchar_t wshortname[MAX_PATH_LONG];
gboolean success_attribs;
gchar *fullpath_with_long_pfx = g_strconcat (info->dirname_with_long_prefix,
G_DIR_SEPARATOR_S,
info->name,
NULL);
wfullpath = g_utf8_to_utf16 (fullpath_with_long_pfx, -1, NULL, NULL, NULL);
if (info->longname != NULL)
g_free (info->longname);
if (info->shortname != NULL)
g_free (info->shortname);
/*
* Get long and short paths of the specified path, if it exists, otherwise
* just assume the filename part in the passed in filename and directory as
* the long and short filenames
*/
if (GetLongPathNameW (wfullpath, wlongname, MAX_PATH_LONG) != 0)
{
wbasename_long = wcsrchr (wlongname, G_DIR_SEPARATOR_W) != NULL ?
wcsrchr (wlongname, G_DIR_SEPARATOR_W) + 1 :
wlongname + STRIP_PFX_LENGTH;
}
else
{
wbasename_long = wcsrchr (wfullpath, G_DIR_SEPARATOR_W) != NULL ?
wcsrchr (wfullpath, G_DIR_SEPARATOR_W) + 1 :
wfullpath + STRIP_PFX_LENGTH;
}
if (GetShortPathNameW (wfullpath, wshortname, MAX_PATH_LONG) != 0)
{
wbasename_short = wcsrchr (wshortname, G_DIR_SEPARATOR_W) != NULL ?
wcsrchr (wshortname, G_DIR_SEPARATOR_W) + 1 :
wshortname + STRIP_PFX_LENGTH;
}
else
{
wbasename_short = wcsrchr (wfullpath, G_DIR_SEPARATOR_W) != NULL ?
wcsrchr (wfullpath, G_DIR_SEPARATOR_W) + 1 :
wfullpath + STRIP_PFX_LENGTH;
}
info->longname = g_utf16_to_utf8 (wbasename_long, -1, NULL, NULL, NULL);
info->shortname = g_utf16_to_utf8 (wbasename_short, -1, NULL, NULL, NULL);
/* Store up current file attributes */
success_attribs = GetFileAttributesExW (wfullpath,
GetFileExInfoStandard,
&info->attribs);
g_free (wfullpath);
g_free (fullpath_with_long_pfx);
if (!success_attribs)
info->attribs.dwFileAttributes = INVALID_FILE_ATTRIBUTES;
}
gboolean
g_win32_file_monitor_check_attrib_changed (GWin32FileMonitorInfo *info,
gchar *filename,
GHashTable *ht_attribs)
{
gchar *fullpath_with_long_prefix = g_strconcat (info->dirname_with_long_prefix,
G_DIR_SEPARATOR_S,
filename,
NULL);
wchar_t *wfullpath_with_long_prefix = g_utf8_to_utf16 (fullpath_with_long_prefix, -1, NULL, NULL, NULL);
gboolean is_attribs_changed = FALSE;
WIN32_FILE_ATTRIBUTE_DATA attrib_data = {0, };
gboolean success_attribs = GetFileAttributesExW (wfullpath_with_long_prefix,
GetFileExInfoStandard,
&attrib_data);
if (!success_attribs)
attrib_data.dwFileAttributes = INVALID_FILE_ATTRIBUTES;
if (info->isfile)
{
/*
* Monitoring a file: Simply check if attributes changed, or file changed, and
* store up the new attributes for the file if needed.
*/
if (info->attribs.dwFileAttributes != INVALID_FILE_ATTRIBUTES &&
success_attribs &&
attrib_data.dwFileAttributes != info->attribs.dwFileAttributes)
{
is_attribs_changed = TRUE;
info->attribs = attrib_data;
}
return is_attribs_changed;
}
else
{
/*
* Monitoring directories: The situation is more complex, we need to first see whether
* a file changed, or a directory changed. If it is just the directory that changed,
* than it's like the file case, that is to check only the attribute of the directory.
* If it is on a file in a directory, we need to check against the attributes of the file,
* against the attribues of the files in the directory, which we stored up when we were
* creating the monitor object, and update our stored records on that file if the attributes
* of that file changed.
*/
WIN32_FILE_ATTRIBUTE_DATA *attrib_data_orig;
gchar *filename_casefold;
if (g_utf8_strrchr (filename, -1, G_DIR_SEPARATOR) != NULL)
{
/* A file in the monitored directory changed */
gboolean attribs_success;
gchar *longname;
wchar_t wfullpath_long[MAX_PATH_LONG];
WIN32_FILE_ATTRIBUTE_DATA *curr_attrib_data = g_new0 (WIN32_FILE_ATTRIBUTE_DATA, 1);
gchar *fullpath = g_strconcat (info->dirname_with_long_prefix,
G_DIR_SEPARATOR_S,
info->name,
G_DIR_SEPARATOR_S,
g_utf8_strrchr (filename, -1, G_DIR_SEPARATOR) + 1,
NULL);
wchar_t *wfullpath = g_utf8_to_utf16 (fullpath, -1, NULL, NULL, NULL);
attribs_success = GetFileAttributesExW (wfullpath,
GetFileExInfoStandard,
curr_attrib_data);
if (!attribs_success)
{
curr_attrib_data->dwFileAttributes = INVALID_FILE_ATTRIBUTES;
longname = g_strdup (g_utf8_strrchr (longname, -1, G_DIR_SEPARATOR) + 1);
}
else
{
if (GetLongPathNameW (wfullpath, wfullpath_long, MAX_PATH_LONG) != 0)
longname = g_utf16_to_utf8 (wcsrchr (wfullpath_long, G_DIR_SEPARATOR_W) + 1,
-1,
NULL,
NULL,
NULL);
else
longname = g_strdup (g_utf8_strrchr (longname, -1, G_DIR_SEPARATOR) + 1);
}
filename_casefold = g_utf8_casefold (longname, -1);
g_free (longname);
attrib_data_orig = g_hash_table_lookup (ht_attribs, filename_casefold);
if (attrib_data_orig->dwFileAttributes != INVALID_FILE_ATTRIBUTES &&
success_attribs &&
curr_attrib_data->dwFileAttributes != attrib_data_orig->dwFileAttributes)
{
is_attribs_changed = TRUE;
}
if (is_attribs_changed)
g_hash_table_replace (ht_attribs, g_strdup (filename_casefold), curr_attrib_data);
else
g_free (curr_attrib_data);
g_free (filename_casefold);
return is_attribs_changed;
}
else
{
/* The monitored directory itself changed */
if (info->attribs.dwFileAttributes != INVALID_FILE_ATTRIBUTES &&
success_attribs &&
attrib_data.dwFileAttributes != info->attribs.dwFileAttributes)
{
is_attribs_changed = TRUE;
info->attribs = attrib_data;
}
return is_attribs_changed;
}
}
}
static GWin32FileMonitorInfo *
g_win32_file_monitor_set_paths (GWin32FileMonitorPrivate *monitor,
const gchar *dirname,
const gchar *filename)
{
GWin32FileMonitorInfo *info = g_new0 (GWin32FileMonitorInfo, 1);
gboolean updated;
if (filename != NULL)
{
/* monitor a file */
gchar *fullpath;
info->isfile = TRUE;
info->dirname_with_long_prefix = g_strconcat (LONGPFX, dirname, NULL);
info->name = g_strdup (filename);
fullpath = g_strconcat (dirname,
G_DIR_SEPARATOR_S,
info->name,
NULL);
updated = g_hash_table_replace (monitor->ht_watched_names,
g_utf8_casefold (fullpath, -1),
info);
g_free (fullpath);
}
else
{
/* monitor a directory */
gchar *parentdir = g_malloc0 (g_utf8_strlen (dirname, -1) + 1);
GHashTable *ht_attribs;
g_utf8_strncpy (parentdir,
dirname,
g_utf8_strlen (dirname, -1) -
g_utf8_strlen (g_utf8_strrchr (dirname, -1, G_DIR_SEPARATOR), -1));
info->isfile = FALSE;
info->dirname_with_long_prefix = g_strconcat (LONGPFX, parentdir, NULL);
info->name = g_strdup (g_utf8_strrchr (dirname, -1, G_DIR_SEPARATOR) + 1);
g_free (parentdir);
ht_attribs = g_win32_file_monitor_dir_refresh_attribs (monitor, info);
if (ht_attribs != NULL)
g_hash_table_replace (monitor->ht_files_attribs,
g_utf8_casefold (dirname, -1),
ht_attribs);
g_hash_table_replace (monitor->ht_watched_names,
g_utf8_casefold (dirname, -1),
info);
}
g_win32_file_monitor_set_names (info);
return info;
}
/* Set up call to ReadDirectoryChangesW() */
static void
g_win32_file_monitor_begin_monitor (GWin32FileMonitorPrivate *monitor,
GWin32FileMonitorInfo *info)
{
gchar *fullpath;
wchar_t *wdirname;
DWORD notify_filter = info->isfile ?
(FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_ATTRIBUTES |
FILE_NOTIFY_CHANGE_SIZE) :
(FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_ATTRIBUTES |
FILE_NOTIFY_CHANGE_SIZE);
fullpath = g_strconcat (info->dirname_with_long_prefix + STRIP_PFX_LENGTH,
G_DIR_SEPARATOR_S,
info->name,
NULL);
wdirname = g_utf8_to_utf16 (info->dirname_with_long_prefix, -1, NULL, NULL, NULL);
monitor->hDirectory = CreateFileW (wdirname,
FILE_GENERIC_READ | FILE_GENERIC_WRITE,
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
NULL);
g_hash_table_insert (monitor->ht_watched_dirs,
monitor->hDirectory,
g_utf8_casefold (fullpath, -1));
if (monitor->hDirectory != INVALID_HANDLE_VALUE)
{
ReadDirectoryChangesW (monitor->hDirectory,
monitor->file_notify_buffer,
monitor->buffer_allocated_bytes,
!info->isfile,
notify_filter,
&monitor->buffer_filled_bytes,
&monitor->overlapped,
g_win32_file_monitor_callback);
}
}
void
g_win32_file_monitor_prepare (GWin32FileMonitorPrivate *monitor,
gchar *dirname,
gchar *filename,
gboolean isfile)
{
GWin32FileMonitorInfo *info;
gchar *fullpath_with_long_prefix, *dirname_with_long_prefix;
DWORD notify_filter = isfile ?
(FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_ATTRIBUTES |
FILE_NOTIFY_CHANGE_SIZE) :
(FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_ATTRIBUTES |
FILE_NOTIFY_CHANGE_SIZE);
monitor->pfni_prev = NULL;
if (isfile)
info = g_win32_file_monitor_set_paths (monitor, dirname, filename);
else
info = g_win32_file_monitor_set_paths (monitor, dirname, NULL);
g_win32_file_monitor_begin_monitor (monitor, info);
}