- Timestamp:
- 06/04/04 19:44:17 (21 years ago)
- Location:
- ps/trunk/source/lib/sysdep/win
- Files:
-
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
ps/trunk/source/lib/sysdep/win/wfam.cpp
r262 r399 1 // Windows-specific directory change notification1 // SGI File Alteration Monitor for Win32 2 2 // Copyright (c) 2004 Jan Wassenberg 3 3 // … … 19 19 20 20 #include "wfam.h" 21 21 #include "lib.h" 22 22 #include "win_internal.h" 23 23 24 #if 0 25 26 static const size_t CHANGE_BUF_SIZE = 15000; 27 // better be enough - if too small, we miss changes made to a directory. 28 29 30 // don't worry about size: the user only passes around a pointer 31 // to this struct, due to the pImpl idiom. this is heap-allocated. 32 struct FAMRequest_ 33 { 34 std::string dir_name; 35 HANDLE hDir; 36 37 // history to detect series of notifications, so we can skip 38 // redundant reloads (slow) 39 std::string last_path; 40 DWORD last_action; // FILE_ACTION_* codes or 0 41 DWORD last_ticks; // timestamp via GetTickCount 42 43 OVERLAPPED ovl; 44 // we don't use any of its fields. 45 // overlapped I/O completation notification is via IOCP. 46 // rationale: see below. 47 char changes[CHANGE_BUF_SIZE]; 48 49 50 FAMRequest_(const char* _dir_name) 51 : dir_name(_dir_name), last_path("") 52 { 53 last_action = 0; 54 last_ticks = 0; 55 56 memset(&ovl, 0, sizeof(ovl)); 57 58 // changes[] doesn't need init 59 } 60 }; 61 62 63 // don't worry about size: the user only passes around a pointer 64 // to this struct, due to the pImpl idiom. this is heap-allocated. 65 struct FAMConnection_ 66 { 67 std::string app_name; 68 69 70 HANDLE hIOCP; 71 72 // queue necessary - race condition if pass to app and re-issue 73 // needs to be FIFO, and don't want to constantly shuffle items (can be rather large) 74 // around => list 75 typedef std::list<FAMEvent> Events; 76 Events pending_events; 77 78 // list of all pending requests to detect duplicates and 79 // for easier cleanup. only store pointer in container - 80 // they're not copy-equivalent. 81 typedef std::map<std::string, FAMRequest*> Requests; 82 typedef Requests::iterator RequestIt; 83 Requests requests; 84 85 FAMConnection_(const char* _app_name) 86 : app_name(_app_name) 87 { 88 hIOCP = 0; 89 // not INVALID_HANDLE_VALUE! (CreateIoCompletionPort requirement) 90 } 91 92 ~FAMConnection_() 93 { 94 CloseHandle(hIOCP); 95 hIOCP = INVALID_HANDLE_VALUE; 96 97 // container holds dynamically allocated Watch structs 98 // for(WatchIt it = watches.begin(); it != watches.end(); ++it) 99 // delete it->second; 100 } 101 }; 102 103 104 24 #include <assert.h> 25 26 #include <string> 27 #include <map> 28 #include <list> 29 30 31 // no module init/shutdown necessary: all global data is allocated 32 // as part of a FAMConnection, which must be FAMClose-d by caller. 33 34 35 // rationale for polling: 36 // much simpler than pure asynchronous notification: no need for a 37 // worker thread, mutex, and in/out queues. polling isn't inefficient: 38 // we do not examine each file; we only need to check if Windows 39 // has sent a change notification via ReadDirectoryChangesW. 40 // 41 // the main reason, however, is that user code will want to poll anyway, 42 // instead of select() from a worker thread: handling asynchronous file 43 // changes is much more work, requiring everything to be thread-safe. 44 // we currently poll once a frame, so that file changes will happen 45 // at a defined time. 105 46 106 47 … … 114 55 // - callback notification: notification function is called when the thread 115 56 // that initiated the I/O (ReadDirectoryChangesW) enters an alertable 116 // wait state (e.g. with SleepEx). it would be nice to be able to 117 // check for notifications from the mainline - would obviate 118 // the separate worker thread for RDC and 2 queues to drive it. 119 // unfortunately, cannot come up with a robust yet quick way of 120 // working off all pending APCs - SleepEx(1) is a hack. even worse, 121 // it was noted in a previous project that APCs are sometimes delivered 122 // from within Windows APIs, without having used SleepEx 123 // (it seems threads enter an "AWS" sometimes when calling the kernel). 57 // wait state (e.g. with SleepEx). we need to poll for notifications 58 // from the mainline (see above). unfortunately, cannot come up with 59 // a robust yet quick way of working off all pending APCs - 60 // SleepEx(1) is a hack. even worse, it was noted in a previous project 61 // that APCs are sometimes delivered from within Windows APIs, without 62 // having used SleepEx (it seems threads enter an "AWS" sometimes when 63 // calling the kernel). 124 64 // 125 65 // IOCPs work well and are elegant; have not yet noticed any drawbacks. 126 127 128 129 130 131 132 // ReadDirectoryChangesW must be called again after each time it returns data, 133 // so we need to pass along the associated Request. 134 // since we issue RDC immediately, instead of sending a bogus packet 135 // to the IOCP that triggers the issue, we don't need the key parameter 136 // for anything - use it to pass along the Request. 137 // cleaner than assuming &ovl = &Request, or stuffing it in an unused 138 // member of OVERLAPPED. 139 140 141 142 143 144 int dir_watch_abort(const char* const dir) 145 { 146 // find watch 147 /* const std::string dir_s(dir); 148 WatchIt it = watches.find(dir_s); 149 if(it == watches.end()) 150 return -1; 151 152 delete it->second; 153 watches.erase(it); 154 */ 155 return 0; 156 } 157 158 159 // it'd be nice to have only 1 call site of ReadDirectoryChangesW, namely 160 // in the notification "callback". however, posting a dummy event to the IOCP 161 // and having the callback issue RDC is a bit ugly, and loses changes made 162 // before the poll routine is first called. 163 164 static int dir_watch_issue(FAMRequest_* fr) 165 { 166 // (re-)request change notification from now on 167 const DWORD filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | 168 FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | 169 FILE_NOTIFY_CHANGE_CREATION; 170 BOOL ret = ReadDirectoryChangesW(fr->hDir, fr->changes, CHANGE_BUF_SIZE, FALSE, filter, 0, &fr->ovl, 0); 171 return ret? 0 : -1; 172 } 173 174 175 int dir_add_watch(const char* const dir, const bool watch_subdirs) 176 { 177 return 0; 178 } 179 180 void FAMCancelMonitor(FAMConnection*, FAMRequest* req) 181 { 182 } 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 void wfam_shutdown() 223 { 224 } 225 226 227 228 int FAMOpen2(FAMConnection* const fc, const char* app_name) 66 // the completion key is used to associate Watch with the directory handle. 67 68 69 // don't worry about size: the user only passes around a pointer 70 // to this struct, due to the pImpl idiom. this is heap-allocated. 71 struct Watch 72 { 73 const std::string dir_name; 74 HANDLE hDir; 75 76 // history to detect series of notifications, so we can skip 77 // redundant reloads (slow) 78 std::string last_path; 79 DWORD last_action; // FILE_ACTION_* codes or 0 80 DWORD last_ticks; // timestamp via GetTickCount 81 82 OVERLAPPED ovl; 83 // fields aren't used. 84 // overlapped I/O completation notification is via IOCP. 85 86 char change_buf[15000]; 87 // better be big enough - if too small, 88 // we miss changes made to a directory. 89 // issue code uses sizeof(change_buf) to determine size. 90 91 // these are returned in FAMEvent. could get them via FAMNextEvent's 92 // fc parameter and associating packets with FAMRequest, 93 // but storing them here is more convenient. 94 FAMConnection* fc; 95 FAMRequest* fr; 96 97 98 Watch(const std::string& _dir_name, HANDLE _hDir) 99 : dir_name(_dir_name), last_path("") 100 { 101 hDir = _hDir; 102 103 last_action = 0; 104 last_ticks = 0; 105 106 memset(&ovl, 0, sizeof(ovl)); 107 108 // change_buf[] doesn't need init 109 } 110 111 ~Watch() 112 { 113 CloseHandle(hDir); 114 hDir = INVALID_HANDLE_VALUE; 115 } 116 }; 117 118 119 // list of all active watches to detect duplicates and 120 // for easier cleanup. only store pointer in container - 121 // they're not copy-equivalent. 122 typedef std::map<std::string, Watch*> Watches; 123 typedef Watches::iterator WatchIt; 124 125 typedef std::list<FAMEvent> Events; 126 127 // don't worry about size: the user only passes around a pointer 128 // to this struct, due to the pImpl idiom. this is heap-allocated. 129 struct AppState 130 { 131 std::string app_name; 132 133 HANDLE hIOCP; 134 135 Events pending_events; 136 // rationale: 137 // we need a queue, instead of just taking events from the change_buf, 138 // because we need to re-issue the watch immediately after it returns 139 // data. of course we can't have the app read from the buffer while 140 // waiting for RDC to write to the buffer - race condition. 141 // an alternative to a queue would be to allocate another buffer, 142 // but that's more complicated, and this way is cleaner anyway. 143 // 144 // FAMEvents are somewhat large (~300 bytes), and FIFO, 145 // so make it a list. 146 147 // list of all active watches to detect duplicates and 148 // for easier cleanup. only store pointer in container - 149 // they're not copy-equivalent. 150 Watches watches; 151 152 AppState(const char* _app_name) 153 : app_name(_app_name) 154 { 155 hIOCP = 0; 156 // not INVALID_HANDLE_VALUE! (CreateIoCompletionPort requirement) 157 } 158 159 ~AppState() 160 { 161 CloseHandle(hIOCP); 162 hIOCP = INVALID_HANDLE_VALUE; 163 164 // free all (dynamically allocated) Watch-es 165 for(WatchIt it = watches.begin(); it != watches.end(); ++it) 166 delete it->second; 167 } 168 }; 169 170 171 // macros to return pointers to the above from the FAM* structs (pImpl) 172 // (macro instead of function so we can bail out of the "calling" function) 173 #define GET_APP_STATE(fc, ptr_name)\ 174 AppState* const ptr_name = (AppState*)fc->internal;\ 175 if(!ptr_name)\ 176 {\ 177 debug_warn("no FAM connection");\ 178 return -1;\ 179 } 180 181 #define GET_WATCH(fr, ptr_name)\ 182 Watch* const ptr_name = (Watch*)fr->internal;\ 183 if(!ptr_name)\ 184 {\ 185 debug_warn("FAMRequest.internal invalid!");\ 186 return -1;\ 187 } 188 189 190 int FAMOpen2(FAMConnection* const fc, const char* const app_name) 229 191 { 230 192 try 231 193 { 232 fc->internal = new FAMConnection_(app_name);194 fc->internal = new AppState(app_name); 233 195 } 234 196 catch(std::bad_alloc) … … 247 209 int FAMClose(FAMConnection* const fc) 248 210 { 249 FAMConnection_*& fc_ = (FAMConnection_*)fc->internal; 250 if(!fc_) 251 { 252 debug_warn("FAMClose: already closed"); 253 return -1; 254 } 255 256 delete fc_; 257 fc_ = 0; 211 GET_APP_STATE(fc, state); 212 213 delete state; 214 fc->internal = 0; 258 215 return 0; 259 216 } 260 217 261 218 262 int FAMMonitorDirectory(FAMConnection* fc, char* dir, FAMRequest* fr, void* user) 263 { 264 FAMConnection_* fc_ = (FAMConnection_*)fc->internal; 265 266 /* 219 // HACK - see call site 220 static void get_packet(AppState*); 221 222 223 int FAMMonitorDirectory(FAMConnection* const fc, char* const _dir, FAMRequest* const fr, void* const user) 224 { 225 GET_APP_STATE(fc, state); 226 Watches& watches = state->watches; 227 HANDLE& hIOCP = state->hIOCP; 228 229 const std::string dir(_dir); 230 267 231 // make sure dir is not already being watched 268 const std::string dir_s(dir); 269 WatchIt it = watches.find(dir_s); 232 WatchIt it = watches.find(dir); 270 233 if(it != watches.end()) 271 234 return -1; 272 */273 HANDLE hDir = INVALID_HANDLE_VALUE;274 HANDLE& hIOCP = fc_->hIOCP;275 235 276 236 // open handle to directory 277 237 const DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; 278 238 const DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED; 279 hDir = CreateFile(dir, FILE_LIST_DIRECTORY, share, 0, OPEN_EXISTING, flags, 0);239 HANDLE hDir = CreateFile(_dir, FILE_LIST_DIRECTORY, share, 0, OPEN_EXISTING, flags, 0); 280 240 if(hDir == INVALID_HANDLE_VALUE) 281 241 return -1; 282 242 283 // create IOCP (if not already done) and bind dir to it 284 hIOCP = CreateIoCompletionPort(hDir, hIOCP, KEY_NORMAL, 0); 243 // create Watch and associate with FAM structs 244 Watch* const w = new Watch(dir, hDir); 245 watches[dir] = w; 246 w->fc = fc; 247 w->fr = fr; 248 249 // associate Watch* with the directory handle. when we receive a packet 250 // from the IOCP, we will need to re-issue the watch and find the 251 // corresponding FAMRequest. 252 const ULONG_PTR key = (ULONG_PTR)w; 253 254 // create IOCP (if not already done) and attach hDir to it 255 hIOCP = CreateIoCompletionPort(hDir, hIOCP, key, 0); 285 256 if(hIOCP == 0 || hIOCP == INVALID_HANDLE_VALUE) 286 257 { 287 fail: 258 delete w; 288 259 CloseHandle(hDir); 289 260 return -1; 290 261 } 291 262 292 293 294 // insert 295 Watch* const w = new Watch(hDir, watch_subdirs); 296 watches[dir_s] = w; 297 298 dir_watch_issue(w); 263 // post a dummy kickoff packet; the IOCP polling code will "re"issue 264 // the corresponding watch. this keeps the ReadDirectoryChangesW call 265 // and directory <--> Watch association code in one place. 266 // 267 // we call get_packet so that it's issued immediately, 268 // instead of only at the next call to FAMPending. 269 PostQueuedCompletionStatus(hIOCP, 0, key, 0); 270 get_packet(state); 271 272 fr->internal = w; 299 273 300 274 return 0; 301 302 } 303 304 305 // added bonus: can actually "poll" for changes here - obviates a worker 306 // thread, mutex, and 2 queues. 307 308 309 310 static int extract_events(FAMConnection* conn, FAMRequest* req) 311 { 312 FAMConnection_ conn_ = (FAMConnection_*)conn->internal; 313 const FILE_NOTIFY_INFORMATION* fni = (const FILE_NOTIFY_INFORMATION*)req->changes; 314 Events& events = fc_->pending_events; 315 275 } 276 277 278 int FAMCancelMonitor(FAMConnection* const fc, FAMRequest* const fr) 279 { 280 GET_APP_STATE(fc, state); 281 GET_WATCH(fr, w) 282 283 // TODO 284 285 return -1; 286 } 287 288 289 static int extract_events(Watch* const w) 290 { 291 FAMConnection* const fc = w->fc; 292 FAMRequest* const fr = w->fr; 293 294 GET_APP_STATE(fc, state); 295 Events& events = state->pending_events; 296 297 // will be modified for each event and added to events 316 298 FAMEvent event_template; 317 event_template.conn = conn; 318 event_template.req = req; 299 event_template.fc = fc; 300 event_template.fr = *fr; 301 302 // points to current FILE_NOTIFY_INFORMATION; 303 // char* simplifies advancing to the next (variable length) FNI. 304 char* pos = w->change_buf; 319 305 320 306 // for every packet in buffer: (there's at least one) 321 307 for(;;) 322 308 { 309 const FILE_NOTIFY_INFORMATION* const fni = (const FILE_NOTIFY_INFORMATION*)pos; 310 311 events.push_back(event_template); 312 FAMEvent& event = events.back(); 313 // fields are set below; we need to add the event here 314 // so that we have a place to put the converted filename. 315 316 317 // 318 // interpret action 319 // 320 321 const char* actions[] = { "", "FILE_ACTION_ADDED", "FILE_ACTION_REMOVED", "FILE_ACTION_MODIFIED", "FILE_ACTION_RENAMED_OLD_NAME", "FILE_ACTION_RENAMED_NEW_NAME" }; 322 const char* action = actions[fni->Action]; 323 323 324 // many apps save by creating a temp file, deleting the original, 324 325 // and renaming the temp file. that leads to 2 reloads, which is slow. … … 326 327 // the notification order is always the same. 327 328 328 // TODO: 329 330 const char* actions[] = { "", "FILE_ACTION_ADDED", "FILE_ACTION_REMOVED", "FILE_ACTION_MODIFIED", "FILE_ACTION_RENAMED_OLD_NAME", "FILE_ACTION_RENAMED_NEW_NAME" }; 331 const char* action = actions[fni->Action]; 332 333 // convert Windows BSTR-style path to 334 // portable C string path for the resource manager. 335 // HACK: convert in place, we copy it into 336 char fn[MAX_PATH]; 337 char* p = fn; 329 // TODO 330 331 FAMCodes code; 332 switch(fni->Action) 333 { 334 case FILE_ACTION_ADDED: 335 case FILE_ACTION_RENAMED_NEW_NAME: 336 code = FAMCreated; 337 break; 338 case FILE_ACTION_REMOVED: 339 case FILE_ACTION_RENAMED_OLD_NAME: 340 code = FAMDeleted; 341 break; 342 case FILE_ACTION_MODIFIED: 343 code = FAMChanged; 344 break; 345 }; 346 347 event.code = code; 348 349 350 // 351 // convert filename from Windows BSTR to portable C string 352 // 353 354 char* fn = event.filename; 338 355 const int num_chars = fni->FileNameLength/2; 339 356 for(int i = 0; i < num_chars; i++) … … 342 359 if(c == '\\') 343 360 c = '/'; 344 * p++ = c;361 *fn++ = c; 345 362 } 346 *p = '\0'; 347 348 // don't want to expose details 349 350 events.push_back(event_template); 363 *fn = '\0'; 351 364 352 365 … … 354 367 // advance to next FILE_NOTIFY_INFORMATION (variable length) 355 368 if(ofs) 356 (char*&)fni+= ofs;369 pos += ofs; 357 370 // this was the last entry - no more elements left in buffer. 358 371 else … … 361 374 362 375 return 0; 363 364 365 res_reload(fn); 366 367 return 0; 368 } 369 370 371 int FAMPending(FAMConnection* fc) 372 { 373 FAMConnection_* const fc_ = (FAMConnection_*)fc->internal; 374 Events& pending_events = fc_->pending_events; 375 376 } 377 378 379 // if a packet is pending, extract its events and re-issue its watch. 380 void get_packet(AppState* const state) 381 { 382 // poll for change notifications from all pending FAMRequests 383 DWORD bytes_transferred; 384 // used to determine if packet is valid or a kickoff 385 ULONG_PTR key; 386 OVERLAPPED* povl; 387 BOOL got_packet = GetQueuedCompletionStatus(state->hIOCP, &bytes_transferred, &key, &povl, 0); 388 if(!got_packet) // no new packet - done 389 return; 390 391 Watch* w = (Watch*)key; 392 393 // this is an actual packet, not just a kickoff for issuing the watch. 394 // extract the events and push them onto AppState's queue. 395 if(bytes_transferred != 0) 396 extract_events(w); 397 398 // (re-)issue change notification request. 399 // it's safe to reuse Watch.change_buf, because we copied out all events. 400 const DWORD filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | 401 FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | 402 FILE_NOTIFY_CHANGE_CREATION; 403 const DWORD buf_size = sizeof(w->change_buf); 404 BOOL ret = ReadDirectoryChangesW(w->hDir, w->change_buf, buf_size, FALSE, filter, 0, &w->ovl, 0); 405 if(!ret) 406 debug_warn("ReadDirectoryChangesW failed"); 407 } 408 409 410 int FAMPending(FAMConnection* const fc) 411 { 412 GET_APP_STATE(fc, state); 413 Events& pending_events = state->pending_events; 414 415 // still have events in the queue? 416 // (slight optimization; no need to call get_packet if so) 376 417 if(!pending_events.empty()) 377 418 return 1; 378 419 379 // check if new buffer has been filled 380 DWORD bytes_transferred; // unused 381 ULONG_PTR key; 382 OVERLAPPED* povl; 383 BOOL got_packet = GetQueuedCompletionStatus(fc_->hIOCP, &bytes_transferred, &key, &povl, 0); 384 if(!got_packet) 385 return 0; 386 387 CHECK_ERR(extract_events(conn, req)); 388 389 // dir_watch_issue(buf); 390 391 392 393 394 fc->fni = (FILE_NOTIFY_INFORMATION*)w->buf; 395 return 1; 420 get_packet(state); 421 422 return !pending_events.empty(); 396 423 } 397 424 … … 399 426 int FAMNextEvent(FAMConnection* const fc, FAMEvent* const fe) 400 427 { 401 FAMConnection_* const fc_ = (FAMConnection_*)fc->internal; 402 Events& pending_events = fc_->pending_events; 403 404 if(!fe) 405 { 406 debug_warn("FAMNextEvent: fe = 0"); 407 return ERR_INVALID_PARAM; 408 } 428 GET_APP_STATE(fc, state); 429 Events& pending_events = state->pending_events; 409 430 410 431 if(pending_events.empty()) … … 418 439 return 0; 419 440 } 420 421 422 #endif -
ps/trunk/source/lib/sysdep/win/wfam.h
r262 r399 1 #include "posix.h" 1 // SGI File Alteration Monitor for Win32 2 // Copyright (c) 2004 Jan Wassenberg 3 // 4 // This program is free software; you can redistribute it and/or 5 // modify it under the terms of the GNU General Public License as 6 // published by the Free Software Foundation; either version 2 of the 7 // License, or (at your option) any later version. 8 // 9 // This program is distributed in the hope that it will be useful, but 10 // WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 // General Public License for more details. 13 // 14 // Contact info: 15 // Jan.Wassenberg@stud.uni-karlsruhe.de 16 // http://www.stud.uni-karlsruhe.de/~urkt/ 17 18 #ifndef WFAM_H__ 19 #define WFAM_H__ 20 21 22 #include "posix.h" // PATH_MAX 23 24 25 // 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 26 2 27 3 28 // opaque structs are too hard to keep in sync with the real definition, 4 // and we don't want to expose the internals. therefore, use the pImpl pattern.29 // and we don't want to expose the internals. therefore, use the pImpl idiom. 5 30 6 31 struct FAMRequest 7 32 { 8 33 void* internal; 34 // reqnum not needed, since FAMMonitorDirectory2 isn't supported. 9 35 }; 10 36 … … 14 40 }; 15 41 16 17 18 enum FAMChangeCode { FAMDeleted, FAMCreated, FAMChanged }; 42 enum FAMCodes { FAMDeleted, FAMCreated, FAMChanged }; 19 43 20 44 typedef struct … … 24 48 char filename[PATH_MAX]; 25 49 void* user; 26 FAMC hangeCodecode;50 FAMCodes code; 27 51 } 28 52 FAMEvent; … … 30 54 31 55 extern int FAMOpen2(FAMConnection*, const char* app_name); 32 extern voidFAMClose(FAMConnection*);56 extern int FAMClose(FAMConnection*); 33 57 34 58 extern int FAMMonitorDirectory(FAMConnection*, const char* dir, FAMRequest* req, void* user); 35 extern voidFAMCancelMonitor(FAMConnection*, FAMRequest* req);59 extern int FAMCancelMonitor(FAMConnection*, FAMRequest* req); 36 60 37 61 extern int FAMPending(FAMConnection*); 38 62 extern int FAMNextEvent(FAMConnection*, FAMEvent* event); 39 63 64 65 #endif // #ifndef WFAM_H__
Note:
See TracChangeset
for help on using the changeset viewer.
