From 0ca334ed39a49777f96bb85da737192905613c22 Mon Sep 17 00:00:00 2001 From: janwas Date: Fri, 4 Jun 2004 17:44:17 +0000 Subject: [PATCH] now emulates FAM API so other code can use one interface. implemented but untested. This was SVN commit r399. --- source/lib/sysdep/win/wfam.cpp | 550 +++++++++++++++++---------------- source/lib/sysdep/win/wfam.h | 42 ++- 2 files changed, 318 insertions(+), 274 deletions(-) diff --git a/source/lib/sysdep/win/wfam.cpp b/source/lib/sysdep/win/wfam.cpp index a1e2ac4d70..3c196d43a2 100755 --- a/source/lib/sysdep/win/wfam.cpp +++ b/source/lib/sysdep/win/wfam.cpp @@ -1,4 +1,4 @@ -// Windows-specific directory change notification +// SGI File Alteration Monitor for Win32 // Copyright (c) 2004 Jan Wassenberg // // This program is free software; you can redistribute it and/or @@ -18,90 +18,31 @@ #include "precompiled.h" #include "wfam.h" - +#include "lib.h" #include "win_internal.h" -#if 0 +#include -static const size_t CHANGE_BUF_SIZE = 15000; - // better be enough - if too small, we miss changes made to a directory. +#include +#include +#include -// don't worry about size: the user only passes around a pointer -// to this struct, due to the pImpl idiom. this is heap-allocated. -struct FAMRequest_ -{ - std::string dir_name; - HANDLE hDir; - - // history to detect series of notifications, so we can skip - // redundant reloads (slow) - std::string last_path; - DWORD last_action; // FILE_ACTION_* codes or 0 - DWORD last_ticks; // timestamp via GetTickCount - - OVERLAPPED ovl; - // we don't use any of its fields. - // overlapped I/O completation notification is via IOCP. - // rationale: see below. - char changes[CHANGE_BUF_SIZE]; - - - FAMRequest_(const char* _dir_name) - : dir_name(_dir_name), last_path("") - { - last_action = 0; - last_ticks = 0; - - memset(&ovl, 0, sizeof(ovl)); - - // changes[] doesn't need init - } -}; - - -// don't worry about size: the user only passes around a pointer -// to this struct, due to the pImpl idiom. this is heap-allocated. -struct FAMConnection_ -{ - std::string app_name; - - - HANDLE hIOCP; - -// queue necessary - race condition if pass to app and re-issue -// needs to be FIFO, and don't want to constantly shuffle items (can be rather large) -// around => list - typedef std::list Events; - Events pending_events; - - // list of all pending requests to detect duplicates and - // for easier cleanup. only store pointer in container - - // they're not copy-equivalent. - typedef std::map Requests; - typedef Requests::iterator RequestIt; - Requests requests; - - FAMConnection_(const char* _app_name) - : app_name(_app_name) - { - hIOCP = 0; - // not INVALID_HANDLE_VALUE! (CreateIoCompletionPort requirement) - } - - ~FAMConnection_() - { - CloseHandle(hIOCP); - hIOCP = INVALID_HANDLE_VALUE; - - // container holds dynamically allocated Watch structs - // for(WatchIt it = watches.begin(); it != watches.end(); ++it) - // delete it->second; - } -}; - +// no module init/shutdown necessary: all global data is allocated +// as part of a FAMConnection, which must be FAMClose-d by caller. +// rationale for polling: +// much simpler than pure asynchronous notification: no need for a +// worker thread, mutex, and in/out queues. polling isn't inefficient: +// we do not examine each file; we only need to check if Windows +// has sent a change notification via ReadDirectoryChangesW. +// +// the main reason, however, is that user code will want to poll anyway, +// instead of select() from a worker thread: handling asynchronous file +// changes is much more work, requiring everything to be thread-safe. +// we currently poll once a frame, so that file changes will happen +// at a defined time. // rationale for using I/O completion ports for notification: @@ -113,123 +54,144 @@ struct FAMConnection_ // having to (re)build the array after every watch add/remove sucks. // - callback notification: notification function is called when the thread // that initiated the I/O (ReadDirectoryChangesW) enters an alertable -// wait state (e.g. with SleepEx). it would be nice to be able to -// check for notifications from the mainline - would obviate -// the separate worker thread for RDC and 2 queues to drive it. -// unfortunately, cannot come up with a robust yet quick way of -// working off all pending APCs - SleepEx(1) is a hack. even worse, -// it was noted in a previous project that APCs are sometimes delivered -// from within Windows APIs, without having used SleepEx -// (it seems threads enter an "AWS" sometimes when calling the kernel). +// wait state (e.g. with SleepEx). we need to poll for notifications +// from the mainline (see above). unfortunately, cannot come up with +// a robust yet quick way of working off all pending APCs - +// SleepEx(1) is a hack. even worse, it was noted in a previous project +// that APCs are sometimes delivered from within Windows APIs, without +// having used SleepEx (it seems threads enter an "AWS" sometimes when +// calling the kernel). // // IOCPs work well and are elegant; have not yet noticed any drawbacks. +// the completion key is used to associate Watch with the directory handle. - - - - -// ReadDirectoryChangesW must be called again after each time it returns data, -// so we need to pass along the associated Request. -// since we issue RDC immediately, instead of sending a bogus packet -// to the IOCP that triggers the issue, we don't need the key parameter -// for anything - use it to pass along the Request. -// cleaner than assuming &ovl = &Request, or stuffing it in an unused -// member of OVERLAPPED. - - - - - -int dir_watch_abort(const char* const dir) +// don't worry about size: the user only passes around a pointer +// to this struct, due to the pImpl idiom. this is heap-allocated. +struct Watch { - // find watch -/* const std::string dir_s(dir); - WatchIt it = watches.find(dir_s); - if(it == watches.end()) - return -1; + const std::string dir_name; + HANDLE hDir; - delete it->second; - watches.erase(it); -*/ - return 0; -} + // history to detect series of notifications, so we can skip + // redundant reloads (slow) + std::string last_path; + DWORD last_action; // FILE_ACTION_* codes or 0 + DWORD last_ticks; // timestamp via GetTickCount + + OVERLAPPED ovl; + // fields aren't used. + // overlapped I/O completation notification is via IOCP. + + char change_buf[15000]; + // better be big enough - if too small, + // we miss changes made to a directory. + // issue code uses sizeof(change_buf) to determine size. + + // these are returned in FAMEvent. could get them via FAMNextEvent's + // fc parameter and associating packets with FAMRequest, + // but storing them here is more convenient. + FAMConnection* fc; + FAMRequest* fr; -// it'd be nice to have only 1 call site of ReadDirectoryChangesW, namely -// in the notification "callback". however, posting a dummy event to the IOCP -// and having the callback issue RDC is a bit ugly, and loses changes made -// before the poll routine is first called. + Watch(const std::string& _dir_name, HANDLE _hDir) + : dir_name(_dir_name), last_path("") + { + hDir = _hDir; -static int dir_watch_issue(FAMRequest_* fr) + last_action = 0; + last_ticks = 0; + + memset(&ovl, 0, sizeof(ovl)); + + // change_buf[] doesn't need init + } + + ~Watch() + { + CloseHandle(hDir); + hDir = INVALID_HANDLE_VALUE; + } +}; + + +// list of all active watches to detect duplicates and +// for easier cleanup. only store pointer in container - +// they're not copy-equivalent. +typedef std::map Watches; +typedef Watches::iterator WatchIt; + +typedef std::list Events; + +// don't worry about size: the user only passes around a pointer +// to this struct, due to the pImpl idiom. this is heap-allocated. +struct AppState { - // (re-)request change notification from now on - const DWORD filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | - FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | - FILE_NOTIFY_CHANGE_CREATION; - BOOL ret = ReadDirectoryChangesW(fr->hDir, fr->changes, CHANGE_BUF_SIZE, FALSE, filter, 0, &fr->ovl, 0); - return ret? 0 : -1; -} + std::string app_name; + + HANDLE hIOCP; + + Events pending_events; + // rationale: + // we need a queue, instead of just taking events from the change_buf, + // because we need to re-issue the watch immediately after it returns + // data. of course we can't have the app read from the buffer while + // waiting for RDC to write to the buffer - race condition. + // an alternative to a queue would be to allocate another buffer, + // but that's more complicated, and this way is cleaner anyway. + // + // FAMEvents are somewhat large (~300 bytes), and FIFO, + // so make it a list. + + // list of all active watches to detect duplicates and + // for easier cleanup. only store pointer in container - + // they're not copy-equivalent. + Watches watches; + + AppState(const char* _app_name) + : app_name(_app_name) + { + hIOCP = 0; + // not INVALID_HANDLE_VALUE! (CreateIoCompletionPort requirement) + } + + ~AppState() + { + CloseHandle(hIOCP); + hIOCP = INVALID_HANDLE_VALUE; + + // free all (dynamically allocated) Watch-es + for(WatchIt it = watches.begin(); it != watches.end(); ++it) + delete it->second; + } +}; -int dir_add_watch(const char* const dir, const bool watch_subdirs) -{ -return 0; -} +// macros to return pointers to the above from the FAM* structs (pImpl) +// (macro instead of function so we can bail out of the "calling" function) +#define GET_APP_STATE(fc, ptr_name)\ + AppState* const ptr_name = (AppState*)fc->internal;\ + if(!ptr_name)\ + {\ + debug_warn("no FAM connection");\ + return -1;\ + } -void FAMCancelMonitor(FAMConnection*, FAMRequest* req) -{ -} +#define GET_WATCH(fr, ptr_name)\ + Watch* const ptr_name = (Watch*)fr->internal;\ + if(!ptr_name)\ + {\ + debug_warn("FAMRequest.internal invalid!");\ + return -1;\ + } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void wfam_shutdown() -{ -} - - - -int FAMOpen2(FAMConnection* const fc, const char* app_name) +int FAMOpen2(FAMConnection* const fc, const char* const app_name) { try { - fc->internal = new FAMConnection_(app_name); + fc->internal = new AppState(app_name); } catch(std::bad_alloc) { @@ -246,166 +208,225 @@ int FAMOpen2(FAMConnection* const fc, const char* app_name) int FAMClose(FAMConnection* const fc) { - FAMConnection_*& fc_ = (FAMConnection_*)fc->internal; - if(!fc_) - { - debug_warn("FAMClose: already closed"); - return -1; - } + GET_APP_STATE(fc, state); - delete fc_; - fc_ = 0; + delete state; + fc->internal = 0; return 0; } -int FAMMonitorDirectory(FAMConnection* fc, char* dir, FAMRequest* fr, void* user) -{ - FAMConnection_* fc_ = (FAMConnection_*)fc->internal; +// HACK - see call site +static void get_packet(AppState*); + + +int FAMMonitorDirectory(FAMConnection* const fc, char* const _dir, FAMRequest* const fr, void* const user) +{ + GET_APP_STATE(fc, state); + Watches& watches = state->watches; + HANDLE& hIOCP = state->hIOCP; + + const std::string dir(_dir); -/* // make sure dir is not already being watched - const std::string dir_s(dir); - WatchIt it = watches.find(dir_s); + WatchIt it = watches.find(dir); if(it != watches.end()) return -1; -*/ - HANDLE hDir = INVALID_HANDLE_VALUE; - HANDLE& hIOCP = fc_->hIOCP; // open handle to directory const DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; const DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED; - hDir = CreateFile(dir, FILE_LIST_DIRECTORY, share, 0, OPEN_EXISTING, flags, 0); + HANDLE hDir = CreateFile(_dir, FILE_LIST_DIRECTORY, share, 0, OPEN_EXISTING, flags, 0); if(hDir == INVALID_HANDLE_VALUE) return -1; - // create IOCP (if not already done) and bind dir to it - hIOCP = CreateIoCompletionPort(hDir, hIOCP, KEY_NORMAL, 0); + // create Watch and associate with FAM structs + Watch* const w = new Watch(dir, hDir); + watches[dir] = w; + w->fc = fc; + w->fr = fr; + + // associate Watch* with the directory handle. when we receive a packet + // from the IOCP, we will need to re-issue the watch and find the + // corresponding FAMRequest. + const ULONG_PTR key = (ULONG_PTR)w; + + // create IOCP (if not already done) and attach hDir to it + hIOCP = CreateIoCompletionPort(hDir, hIOCP, key, 0); if(hIOCP == 0 || hIOCP == INVALID_HANDLE_VALUE) { -fail: + delete w; CloseHandle(hDir); return -1; } + // post a dummy kickoff packet; the IOCP polling code will "re"issue + // the corresponding watch. this keeps the ReadDirectoryChangesW call + // and directory <--> Watch association code in one place. + // + // we call get_packet so that it's issued immediately, + // instead of only at the next call to FAMPending. + PostQueuedCompletionStatus(hIOCP, 0, key, 0); + get_packet(state); - - // insert - Watch* const w = new Watch(hDir, watch_subdirs); - watches[dir_s] = w; - - dir_watch_issue(w); + fr->internal = w; return 0; - } -// added bonus: can actually "poll" for changes here - obviates a worker -// thread, mutex, and 2 queues. - - - -static int extract_events(FAMConnection* conn, FAMRequest* req) +int FAMCancelMonitor(FAMConnection* const fc, FAMRequest* const fr) { - FAMConnection_ conn_ = (FAMConnection_*)conn->internal; - const FILE_NOTIFY_INFORMATION* fni = (const FILE_NOTIFY_INFORMATION*)req->changes; - Events& events = fc_->pending_events; + GET_APP_STATE(fc, state); + GET_WATCH(fr, w) + // TODO + + return -1; +} + + +static int extract_events(Watch* const w) +{ + FAMConnection* const fc = w->fc; + FAMRequest* const fr = w->fr; + + GET_APP_STATE(fc, state); + Events& events = state->pending_events; + + // will be modified for each event and added to events FAMEvent event_template; - event_template.conn = conn; - event_template.req = req; + event_template.fc = fc; + event_template.fr = *fr; + + // points to current FILE_NOTIFY_INFORMATION; + // char* simplifies advancing to the next (variable length) FNI. + char* pos = w->change_buf; // for every packet in buffer: (there's at least one) for(;;) { + const FILE_NOTIFY_INFORMATION* const fni = (const FILE_NOTIFY_INFORMATION*)pos; + + events.push_back(event_template); + FAMEvent& event = events.back(); + // fields are set below; we need to add the event here + // so that we have a place to put the converted filename. + + + // + // interpret action + // + + const char* actions[] = { "", "FILE_ACTION_ADDED", "FILE_ACTION_REMOVED", "FILE_ACTION_MODIFIED", "FILE_ACTION_RENAMED_OLD_NAME", "FILE_ACTION_RENAMED_NEW_NAME" }; + const char* action = actions[fni->Action]; + // many apps save by creating a temp file, deleting the original, // and renaming the temp file. that leads to 2 reloads, which is slow. // try to detect this case with a simple state machine - we assume // the notification order is always the same. - // TODO: + // TODO - const char* actions[] = { "", "FILE_ACTION_ADDED", "FILE_ACTION_REMOVED", "FILE_ACTION_MODIFIED", "FILE_ACTION_RENAMED_OLD_NAME", "FILE_ACTION_RENAMED_NEW_NAME" }; - const char* action = actions[fni->Action]; + FAMCodes code; + switch(fni->Action) + { + case FILE_ACTION_ADDED: + case FILE_ACTION_RENAMED_NEW_NAME: + code = FAMCreated; + break; + case FILE_ACTION_REMOVED: + case FILE_ACTION_RENAMED_OLD_NAME: + code = FAMDeleted; + break; + case FILE_ACTION_MODIFIED: + code = FAMChanged; + break; + }; - // convert Windows BSTR-style path to - // portable C string path for the resource manager. - // HACK: convert in place, we copy it into - char fn[MAX_PATH]; - char* p = fn; + event.code = code; + + + // + // convert filename from Windows BSTR to portable C string + // + + char* fn = event.filename; const int num_chars = fni->FileNameLength/2; for(int i = 0; i < num_chars; i++) { char c = (char)fni->FileName[i]; if(c == '\\') c = '/'; - *p++ = c; + *fn++ = c; } - *p = '\0'; - - // don't want to expose details - - events.push_back(event_template); + *fn = '\0'; const DWORD ofs = fni->NextEntryOffset; // advance to next FILE_NOTIFY_INFORMATION (variable length) if(ofs) - (char*&)fni += ofs; + pos += ofs; // this was the last entry - no more elements left in buffer. else break; } return 0; - - - res_reload(fn); - - return 0; } -int FAMPending(FAMConnection* fc) +// if a packet is pending, extract its events and re-issue its watch. +void get_packet(AppState* const state) { - FAMConnection_* const fc_ = (FAMConnection_*)fc->internal; - Events& pending_events = fc_->pending_events; + // poll for change notifications from all pending FAMRequests + DWORD bytes_transferred; + // used to determine if packet is valid or a kickoff + ULONG_PTR key; + OVERLAPPED* povl; + BOOL got_packet = GetQueuedCompletionStatus(state->hIOCP, &bytes_transferred, &key, &povl, 0); + if(!got_packet) // no new packet - done + return; + Watch* w = (Watch*)key; + + // this is an actual packet, not just a kickoff for issuing the watch. + // extract the events and push them onto AppState's queue. + if(bytes_transferred != 0) + extract_events(w); + + // (re-)issue change notification request. + // it's safe to reuse Watch.change_buf, because we copied out all events. + const DWORD filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | + FILE_NOTIFY_CHANGE_CREATION; + const DWORD buf_size = sizeof(w->change_buf); + BOOL ret = ReadDirectoryChangesW(w->hDir, w->change_buf, buf_size, FALSE, filter, 0, &w->ovl, 0); + if(!ret) + debug_warn("ReadDirectoryChangesW failed"); +} + + +int FAMPending(FAMConnection* const fc) +{ + GET_APP_STATE(fc, state); + Events& pending_events = state->pending_events; + + // still have events in the queue? + // (slight optimization; no need to call get_packet if so) if(!pending_events.empty()) return 1; - // check if new buffer has been filled - DWORD bytes_transferred; // unused - ULONG_PTR key; - OVERLAPPED* povl; - BOOL got_packet = GetQueuedCompletionStatus(fc_->hIOCP, &bytes_transferred, &key, &povl, 0); - if(!got_packet) - return 0; + get_packet(state); - CHECK_ERR(extract_events(conn, req)); - -// dir_watch_issue(buf); - - - - - fc->fni = (FILE_NOTIFY_INFORMATION*)w->buf; - return 1; + return !pending_events.empty(); } int FAMNextEvent(FAMConnection* const fc, FAMEvent* const fe) { - FAMConnection_* const fc_ = (FAMConnection_*)fc->internal; - Events& pending_events = fc_->pending_events; - - if(!fe) - { - debug_warn("FAMNextEvent: fe = 0"); - return ERR_INVALID_PARAM; - } + GET_APP_STATE(fc, state); + Events& pending_events = state->pending_events; if(pending_events.empty()) { @@ -417,6 +438,3 @@ int FAMNextEvent(FAMConnection* const fc, FAMEvent* const fe) pending_events.pop_front(); return 0; } - - -#endif diff --git a/source/lib/sysdep/win/wfam.h b/source/lib/sysdep/win/wfam.h index 9462bfeb6f..201b2c12dd 100755 --- a/source/lib/sysdep/win/wfam.h +++ b/source/lib/sysdep/win/wfam.h @@ -1,11 +1,37 @@ -#include "posix.h" +// SGI File Alteration Monitor for Win32 +// Copyright (c) 2004 Jan Wassenberg +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program 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 +// General Public License for more details. +// +// Contact info: +// Jan.Wassenberg@stud.uni-karlsruhe.de +// http://www.stud.uni-karlsruhe.de/~urkt/ + +#ifndef WFAM_H__ +#define WFAM_H__ + + +#include "posix.h" // PATH_MAX + + +// FAM documentation: http://techpubs.sgi.com/library/tpl/cgi-bin/getdoc.cgi?coll=0650&db=bks&fname=/SGI_Developer/books/IIDsktp_IG/sgi_html/ch08.html + // opaque structs are too hard to keep in sync with the real definition, -// and we don't want to expose the internals. therefore, use the pImpl pattern. +// and we don't want to expose the internals. therefore, use the pImpl idiom. struct FAMRequest { void* internal; + // reqnum not needed, since FAMMonitorDirectory2 isn't supported. }; struct FAMConnection @@ -13,9 +39,7 @@ struct FAMConnection void* internal; }; - - -enum FAMChangeCode { FAMDeleted, FAMCreated, FAMChanged }; +enum FAMCodes { FAMDeleted, FAMCreated, FAMChanged }; typedef struct { @@ -23,17 +47,19 @@ typedef struct FAMRequest fr; char filename[PATH_MAX]; void* user; - FAMChangeCode code; + FAMCodes code; } FAMEvent; extern int FAMOpen2(FAMConnection*, const char* app_name); -extern void FAMClose(FAMConnection*); +extern int FAMClose(FAMConnection*); extern int FAMMonitorDirectory(FAMConnection*, const char* dir, FAMRequest* req, void* user); -extern void FAMCancelMonitor(FAMConnection*, FAMRequest* req); +extern int FAMCancelMonitor(FAMConnection*, FAMRequest* req); extern int FAMPending(FAMConnection*); extern int FAMNextEvent(FAMConnection*, FAMEvent* event); + +#endif // #ifndef WFAM_H__ \ No newline at end of file