diff options
Diffstat (limited to 'compat')
-rw-r--r-- | compat/fsmonitor/fsm-darwin-gcc.h | 92 | ||||
-rw-r--r-- | compat/fsmonitor/fsm-health-darwin.c | 24 | ||||
-rw-r--r-- | compat/fsmonitor/fsm-health-win32.c | 278 | ||||
-rw-r--r-- | compat/fsmonitor/fsm-health.h | 47 | ||||
-rw-r--r-- | compat/fsmonitor/fsm-listen-darwin.c | 513 | ||||
-rw-r--r-- | compat/fsmonitor/fsm-listen-win32.c | 874 | ||||
-rw-r--r-- | compat/fsmonitor/fsm-listen.h | 49 | ||||
-rw-r--r-- | compat/fsmonitor/fsm-settings-darwin.c | 89 | ||||
-rw-r--r-- | compat/fsmonitor/fsm-settings-win32.c | 137 | ||||
-rw-r--r-- | compat/mingw.c | 98 | ||||
-rw-r--r-- | compat/mingw.h | 10 | ||||
-rw-r--r-- | compat/mkdir.c | 2 | ||||
-rw-r--r-- | compat/mmap.c | 2 | ||||
-rw-r--r-- | compat/nedmalloc/nedmalloc.c | 1 | ||||
-rw-r--r-- | compat/terminal.c | 238 | ||||
-rw-r--r-- | compat/terminal.h | 9 | ||||
-rw-r--r-- | compat/win32/path-utils.h | 6 | ||||
-rw-r--r-- | compat/win32/syslog.c | 7 |
18 files changed, 2423 insertions, 53 deletions
diff --git a/compat/fsmonitor/fsm-darwin-gcc.h b/compat/fsmonitor/fsm-darwin-gcc.h new file mode 100644 index 0000000..1c75c3d --- /dev/null +++ b/compat/fsmonitor/fsm-darwin-gcc.h @@ -0,0 +1,92 @@ +#ifndef FSM_DARWIN_GCC_H +#define FSM_DARWIN_GCC_H + +#ifndef __clang__ +/* + * It is possible to #include CoreFoundation/CoreFoundation.h when compiling + * with clang, but not with GCC as of time of writing. + * + * See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=93082 for details. + */ +typedef unsigned int FSEventStreamCreateFlags; +#define kFSEventStreamEventFlagNone 0x00000000 +#define kFSEventStreamEventFlagMustScanSubDirs 0x00000001 +#define kFSEventStreamEventFlagUserDropped 0x00000002 +#define kFSEventStreamEventFlagKernelDropped 0x00000004 +#define kFSEventStreamEventFlagEventIdsWrapped 0x00000008 +#define kFSEventStreamEventFlagHistoryDone 0x00000010 +#define kFSEventStreamEventFlagRootChanged 0x00000020 +#define kFSEventStreamEventFlagMount 0x00000040 +#define kFSEventStreamEventFlagUnmount 0x00000080 +#define kFSEventStreamEventFlagItemCreated 0x00000100 +#define kFSEventStreamEventFlagItemRemoved 0x00000200 +#define kFSEventStreamEventFlagItemInodeMetaMod 0x00000400 +#define kFSEventStreamEventFlagItemRenamed 0x00000800 +#define kFSEventStreamEventFlagItemModified 0x00001000 +#define kFSEventStreamEventFlagItemFinderInfoMod 0x00002000 +#define kFSEventStreamEventFlagItemChangeOwner 0x00004000 +#define kFSEventStreamEventFlagItemXattrMod 0x00008000 +#define kFSEventStreamEventFlagItemIsFile 0x00010000 +#define kFSEventStreamEventFlagItemIsDir 0x00020000 +#define kFSEventStreamEventFlagItemIsSymlink 0x00040000 +#define kFSEventStreamEventFlagOwnEvent 0x00080000 +#define kFSEventStreamEventFlagItemIsHardlink 0x00100000 +#define kFSEventStreamEventFlagItemIsLastHardlink 0x00200000 +#define kFSEventStreamEventFlagItemCloned 0x00400000 + +typedef struct __FSEventStream *FSEventStreamRef; +typedef const FSEventStreamRef ConstFSEventStreamRef; + +typedef unsigned int CFStringEncoding; +#define kCFStringEncodingUTF8 0x08000100 + +typedef const struct __CFString *CFStringRef; +typedef const struct __CFArray *CFArrayRef; +typedef const struct __CFRunLoop *CFRunLoopRef; + +struct FSEventStreamContext { + long long version; + void *cb_data, *retain, *release, *copy_description; +}; + +typedef struct FSEventStreamContext FSEventStreamContext; +typedef unsigned int FSEventStreamEventFlags; +#define kFSEventStreamCreateFlagNoDefer 0x02 +#define kFSEventStreamCreateFlagWatchRoot 0x04 +#define kFSEventStreamCreateFlagFileEvents 0x10 + +typedef unsigned long long FSEventStreamEventId; +#define kFSEventStreamEventIdSinceNow 0xFFFFFFFFFFFFFFFFULL + +typedef void (*FSEventStreamCallback)(ConstFSEventStreamRef streamRef, + void *context, + __SIZE_TYPE__ num_of_events, + void *event_paths, + const FSEventStreamEventFlags event_flags[], + const FSEventStreamEventId event_ids[]); +typedef double CFTimeInterval; +FSEventStreamRef FSEventStreamCreate(void *allocator, + FSEventStreamCallback callback, + FSEventStreamContext *context, + CFArrayRef paths_to_watch, + FSEventStreamEventId since_when, + CFTimeInterval latency, + FSEventStreamCreateFlags flags); +CFStringRef CFStringCreateWithCString(void *allocator, const char *string, + CFStringEncoding encoding); +CFArrayRef CFArrayCreate(void *allocator, const void **items, long long count, + void *callbacks); +void CFRunLoopRun(void); +void CFRunLoopStop(CFRunLoopRef run_loop); +CFRunLoopRef CFRunLoopGetCurrent(void); +extern CFStringRef kCFRunLoopDefaultMode; +void FSEventStreamScheduleWithRunLoop(FSEventStreamRef stream, + CFRunLoopRef run_loop, + CFStringRef run_loop_mode); +unsigned char FSEventStreamStart(FSEventStreamRef stream); +void FSEventStreamStop(FSEventStreamRef stream); +void FSEventStreamInvalidate(FSEventStreamRef stream); +void FSEventStreamRelease(FSEventStreamRef stream); + +#endif /* !clang */ +#endif /* FSM_DARWIN_GCC_H */ diff --git a/compat/fsmonitor/fsm-health-darwin.c b/compat/fsmonitor/fsm-health-darwin.c new file mode 100644 index 0000000..b9f709e --- /dev/null +++ b/compat/fsmonitor/fsm-health-darwin.c @@ -0,0 +1,24 @@ +#include "cache.h" +#include "config.h" +#include "fsmonitor.h" +#include "fsm-health.h" +#include "fsmonitor--daemon.h" + +int fsm_health__ctor(struct fsmonitor_daemon_state *state) +{ + return 0; +} + +void fsm_health__dtor(struct fsmonitor_daemon_state *state) +{ + return; +} + +void fsm_health__loop(struct fsmonitor_daemon_state *state) +{ + return; +} + +void fsm_health__stop_async(struct fsmonitor_daemon_state *state) +{ +} diff --git a/compat/fsmonitor/fsm-health-win32.c b/compat/fsmonitor/fsm-health-win32.c new file mode 100644 index 0000000..2ea08c1 --- /dev/null +++ b/compat/fsmonitor/fsm-health-win32.c @@ -0,0 +1,278 @@ +#include "cache.h" +#include "config.h" +#include "fsmonitor.h" +#include "fsm-health.h" +#include "fsmonitor--daemon.h" + +/* + * Every minute wake up and test our health. + */ +#define WAIT_FREQ_MS (60 * 1000) + +/* + * State machine states for each of the interval functions + * used for polling our health. + */ +enum interval_fn_ctx { + CTX_INIT = 0, + CTX_TERM, + CTX_TIMER +}; + +typedef int (interval_fn)(struct fsmonitor_daemon_state *state, + enum interval_fn_ctx ctx); + +struct fsm_health_data +{ + HANDLE hEventShutdown; + + HANDLE hHandles[1]; /* the array does not own these handles */ +#define HEALTH_SHUTDOWN 0 + int nr_handles; /* number of active event handles */ + + struct wt_moved + { + wchar_t wpath[MAX_PATH + 1]; + BY_HANDLE_FILE_INFORMATION bhfi; + } wt_moved; +}; + +/* + * Lookup the system unique ID for the path. This is as close as + * we get to an inode number, but this also contains volume info, + * so it is a little stronger. + */ +static int lookup_bhfi(wchar_t *wpath, + BY_HANDLE_FILE_INFORMATION *bhfi) +{ + DWORD desired_access = FILE_LIST_DIRECTORY; + DWORD share_mode = + FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE; + HANDLE hDir; + + hDir = CreateFileW(wpath, desired_access, share_mode, NULL, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (hDir == INVALID_HANDLE_VALUE) { + error(_("[GLE %ld] health thread could not open '%ls'"), + GetLastError(), wpath); + return -1; + } + + if (!GetFileInformationByHandle(hDir, bhfi)) { + error(_("[GLE %ld] health thread getting BHFI for '%ls'"), + GetLastError(), wpath); + CloseHandle(hDir); + return -1; + } + + CloseHandle(hDir); + return 0; +} + +/* + * Compare the relevant fields from two system unique IDs. + * We use this to see if two different handles to the same + * path actually refer to the same *instance* of the file + * or directory. + */ +static int bhfi_eq(const BY_HANDLE_FILE_INFORMATION *bhfi_1, + const BY_HANDLE_FILE_INFORMATION *bhfi_2) +{ + return (bhfi_1->dwVolumeSerialNumber == bhfi_2->dwVolumeSerialNumber && + bhfi_1->nFileIndexHigh == bhfi_2->nFileIndexHigh && + bhfi_1->nFileIndexLow == bhfi_2->nFileIndexLow); +} + +/* + * Shutdown if the original worktree root directory been deleted, + * moved, or renamed? + * + * Since the main thread did a "chdir(getenv($HOME))" and our CWD + * is not in the worktree root directory and because the listener + * thread added FILE_SHARE_DELETE to the watch handle, it is possible + * for the root directory to be moved or deleted while we are still + * watching it. We want to detect that here and force a shutdown. + * + * Granted, a delete MAY cause some operations to fail, such as + * GetOverlappedResult(), but it is not guaranteed. And because + * ReadDirectoryChangesW() only reports on changes *WITHIN* the + * directory, not changes *ON* the directory, our watch will not + * receive a delete event for it. + * + * A move/rename of the worktree root will also not generate an event. + * And since the listener thread already has an open handle, it may + * continue to receive events for events within the directory. + * However, the pathname of the named-pipe was constructed using the + * original location of the worktree root. (Remember named-pipes are + * stored in the NPFS and not in the actual file system.) Clients + * trying to talk to the worktree after the move/rename will not + * reach our daemon process, since we're still listening on the + * pipe with original path. + * + * Furthermore, if the user does something like: + * + * $ mv repo repo.old + * $ git init repo + * + * A new daemon cannot be started in the new instance of "repo" + * because the named-pipe is still being used by the daemon on + * the original instance. + * + * So, detect move/rename/delete and shutdown. This should also + * handle unsafe drive removal. + * + * We use the file system unique ID to distinguish the original + * directory instance from a new instance and force a shutdown + * if the unique ID changes. + * + * Since a worktree move/rename/delete/unmount doesn't happen + * that often (and we can't get an immediate event anyway), we + * use a timeout and periodically poll it. + */ +static int has_worktree_moved(struct fsmonitor_daemon_state *state, + enum interval_fn_ctx ctx) +{ + struct fsm_health_data *data = state->health_data; + BY_HANDLE_FILE_INFORMATION bhfi; + int r; + + switch (ctx) { + case CTX_TERM: + return 0; + + case CTX_INIT: + if (xutftowcs_path(data->wt_moved.wpath, + state->path_worktree_watch.buf) < 0) { + error(_("could not convert to wide characters: '%s'"), + state->path_worktree_watch.buf); + return -1; + } + + /* + * On the first call we lookup the unique sequence ID for + * the worktree root directory. + */ + return lookup_bhfi(data->wt_moved.wpath, &data->wt_moved.bhfi); + + case CTX_TIMER: + r = lookup_bhfi(data->wt_moved.wpath, &bhfi); + if (r) + return r; + if (!bhfi_eq(&data->wt_moved.bhfi, &bhfi)) { + error(_("BHFI changed '%ls'"), data->wt_moved.wpath); + return -1; + } + return 0; + + default: + die(_("unhandled case in 'has_worktree_moved': %d"), + (int)ctx); + } + + return 0; +} + + +int fsm_health__ctor(struct fsmonitor_daemon_state *state) +{ + struct fsm_health_data *data; + + CALLOC_ARRAY(data, 1); + + data->hEventShutdown = CreateEvent(NULL, TRUE, FALSE, NULL); + + data->hHandles[HEALTH_SHUTDOWN] = data->hEventShutdown; + data->nr_handles++; + + state->health_data = data; + return 0; +} + +void fsm_health__dtor(struct fsmonitor_daemon_state *state) +{ + struct fsm_health_data *data; + + if (!state || !state->health_data) + return; + + data = state->health_data; + + CloseHandle(data->hEventShutdown); + + FREE_AND_NULL(state->health_data); +} + +/* + * A table of the polling functions. + */ +static interval_fn *table[] = { + has_worktree_moved, + NULL, /* must be last */ +}; + +/* + * Call all of the polling functions in the table. + * Shortcut and return first error. + * + * Return 0 if all succeeded. + */ +static int call_all(struct fsmonitor_daemon_state *state, + enum interval_fn_ctx ctx) +{ + int k; + + for (k = 0; table[k]; k++) { + int r = table[k](state, ctx); + if (r) + return r; + } + + return 0; +} + +void fsm_health__loop(struct fsmonitor_daemon_state *state) +{ + struct fsm_health_data *data = state->health_data; + int r; + + r = call_all(state, CTX_INIT); + if (r < 0) + goto force_error_stop; + if (r > 0) + goto force_shutdown; + + for (;;) { + DWORD dwWait = WaitForMultipleObjects(data->nr_handles, + data->hHandles, + FALSE, WAIT_FREQ_MS); + + if (dwWait == WAIT_OBJECT_0 + HEALTH_SHUTDOWN) + goto clean_shutdown; + + if (dwWait == WAIT_TIMEOUT) { + r = call_all(state, CTX_TIMER); + if (r < 0) + goto force_error_stop; + if (r > 0) + goto force_shutdown; + continue; + } + + error(_("health thread wait failed [GLE %ld]"), + GetLastError()); + goto force_error_stop; + } + +force_error_stop: + state->health_error_code = -1; +force_shutdown: + ipc_server_stop_async(state->ipc_server_data); +clean_shutdown: + call_all(state, CTX_TERM); + return; +} + +void fsm_health__stop_async(struct fsmonitor_daemon_state *state) +{ + SetEvent(state->health_data->hHandles[HEALTH_SHUTDOWN]); +} diff --git a/compat/fsmonitor/fsm-health.h b/compat/fsmonitor/fsm-health.h new file mode 100644 index 0000000..45547ba --- /dev/null +++ b/compat/fsmonitor/fsm-health.h @@ -0,0 +1,47 @@ +#ifndef FSM_HEALTH_H +#define FSM_HEALTH_H + +/* This needs to be implemented by each backend */ + +#ifdef HAVE_FSMONITOR_DAEMON_BACKEND + +struct fsmonitor_daemon_state; + +/* + * Initialize platform-specific data for the fsmonitor health thread. + * This will be called from the main thread PRIOR to staring the + * thread. + * + * Returns 0 if successful. + * Returns -1 otherwise. + */ +int fsm_health__ctor(struct fsmonitor_daemon_state *state); + +/* + * Cleanup platform-specific data for the health thread. + * This will be called from the main thread AFTER joining the thread. + */ +void fsm_health__dtor(struct fsmonitor_daemon_state *state); + +/* + * The main body of the platform-specific event loop to monitor the + * health of the daemon process. This will run in the health thread. + * + * The health thread should call `ipc_server_stop_async()` if it needs + * to cause a shutdown. (It should NOT do so if it receives a shutdown + * shutdown signal.) + * + * It should set `state->health_error_code` to -1 if the daemon should exit + * with an error. + */ +void fsm_health__loop(struct fsmonitor_daemon_state *state); + +/* + * Gently request that the health thread shutdown. + * It does not wait for it to stop. The caller should do a JOIN + * to wait for it. + */ +void fsm_health__stop_async(struct fsmonitor_daemon_state *state); + +#endif /* HAVE_FSMONITOR_DAEMON_BACKEND */ +#endif /* FSM_HEALTH_H */ diff --git a/compat/fsmonitor/fsm-listen-darwin.c b/compat/fsmonitor/fsm-listen-darwin.c new file mode 100644 index 0000000..8e208e8 --- /dev/null +++ b/compat/fsmonitor/fsm-listen-darwin.c @@ -0,0 +1,513 @@ +#ifndef __clang__ +#include "fsm-darwin-gcc.h" +#else +#include <CoreFoundation/CoreFoundation.h> +#include <CoreServices/CoreServices.h> + +#ifndef AVAILABLE_MAC_OS_X_VERSION_10_13_AND_LATER +/* + * This enum value was added in 10.13 to: + * + * /Applications/Xcode.app/Contents/Developer/Platforms/ \ + * MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/ \ + * Library/Frameworks/CoreServices.framework/Frameworks/ \ + * FSEvents.framework/Versions/Current/Headers/FSEvents.h + * + * If we're compiling against an older SDK, this symbol won't be + * present. Silently define it here so that we don't have to ifdef + * the logging or masking below. This should be harmless since older + * versions of macOS won't ever emit this FS event anyway. + */ +#define kFSEventStreamEventFlagItemCloned 0x00400000 +#endif +#endif + +#include "cache.h" +#include "fsmonitor.h" +#include "fsm-listen.h" +#include "fsmonitor--daemon.h" + +struct fsm_listen_data +{ + CFStringRef cfsr_worktree_path; + CFStringRef cfsr_gitdir_path; + + CFArrayRef cfar_paths_to_watch; + int nr_paths_watching; + + FSEventStreamRef stream; + + CFRunLoopRef rl; + + enum shutdown_style { + SHUTDOWN_EVENT = 0, + FORCE_SHUTDOWN, + FORCE_ERROR_STOP, + } shutdown_style; + + unsigned int stream_scheduled:1; + unsigned int stream_started:1; +}; + +static void log_flags_set(const char *path, const FSEventStreamEventFlags flag) +{ + struct strbuf msg = STRBUF_INIT; + + if (flag & kFSEventStreamEventFlagMustScanSubDirs) + strbuf_addstr(&msg, "MustScanSubDirs|"); + if (flag & kFSEventStreamEventFlagUserDropped) + strbuf_addstr(&msg, "UserDropped|"); + if (flag & kFSEventStreamEventFlagKernelDropped) + strbuf_addstr(&msg, "KernelDropped|"); + if (flag & kFSEventStreamEventFlagEventIdsWrapped) + strbuf_addstr(&msg, "EventIdsWrapped|"); + if (flag & kFSEventStreamEventFlagHistoryDone) + strbuf_addstr(&msg, "HistoryDone|"); + if (flag & kFSEventStreamEventFlagRootChanged) + strbuf_addstr(&msg, "RootChanged|"); + if (flag & kFSEventStreamEventFlagMount) + strbuf_addstr(&msg, "Mount|"); + if (flag & kFSEventStreamEventFlagUnmount) + strbuf_addstr(&msg, "Unmount|"); + if (flag & kFSEventStreamEventFlagItemChangeOwner) + strbuf_addstr(&msg, "ItemChangeOwner|"); + if (flag & kFSEventStreamEventFlagItemCreated) + strbuf_addstr(&msg, "ItemCreated|"); + if (flag & kFSEventStreamEventFlagItemFinderInfoMod) + strbuf_addstr(&msg, "ItemFinderInfoMod|"); + if (flag & kFSEventStreamEventFlagItemInodeMetaMod) + strbuf_addstr(&msg, "ItemInodeMetaMod|"); + if (flag & kFSEventStreamEventFlagItemIsDir) + strbuf_addstr(&msg, "ItemIsDir|"); + if (flag & kFSEventStreamEventFlagItemIsFile) + strbuf_addstr(&msg, "ItemIsFile|"); + if (flag & kFSEventStreamEventFlagItemIsHardlink) + strbuf_addstr(&msg, "ItemIsHardlink|"); + if (flag & kFSEventStreamEventFlagItemIsLastHardlink) + strbuf_addstr(&msg, "ItemIsLastHardlink|"); + if (flag & kFSEventStreamEventFlagItemIsSymlink) + strbuf_addstr(&msg, "ItemIsSymlink|"); + if (flag & kFSEventStreamEventFlagItemModified) + strbuf_addstr(&msg, "ItemModified|"); + if (flag & kFSEventStreamEventFlagItemRemoved) + strbuf_addstr(&msg, "ItemRemoved|"); + if (flag & kFSEventStreamEventFlagItemRenamed) + strbuf_addstr(&msg, "ItemRenamed|"); + if (flag & kFSEventStreamEventFlagItemXattrMod) + strbuf_addstr(&msg, "ItemXattrMod|"); + if (flag & kFSEventStreamEventFlagOwnEvent) + strbuf_addstr(&msg, "OwnEvent|"); + if (flag & kFSEventStreamEventFlagItemCloned) + strbuf_addstr(&msg, "ItemCloned|"); + + trace_printf_key(&trace_fsmonitor, "fsevent: '%s', flags=0x%x %s", + path, flag, msg.buf); + + strbuf_release(&msg); +} + +static int ef_is_root_changed(const FSEventStreamEventFlags ef) +{ + return (ef & kFSEventStreamEventFlagRootChanged); +} + +static int ef_is_root_delete(const FSEventStreamEventFlags ef) +{ + return (ef & kFSEventStreamEventFlagItemIsDir && + ef & kFSEventStreamEventFlagItemRemoved); +} + +static int ef_is_root_renamed(const FSEventStreamEventFlags ef) +{ + return (ef & kFSEventStreamEventFlagItemIsDir && + ef & kFSEventStreamEventFlagItemRenamed); +} + +static int ef_is_dropped(const FSEventStreamEventFlags ef) +{ + return (ef & kFSEventStreamEventFlagMustScanSubDirs || + ef & kFSEventStreamEventFlagKernelDropped || + ef & kFSEventStreamEventFlagUserDropped); +} + +/* + * If an `xattr` change is the only reason we received this event, + * then silently ignore it. Git doesn't care about xattr's. We + * have to be careful here because the kernel can combine multiple + * events for a single path. And because events always have certain + * bits set, such as `ItemIsFile` or `ItemIsDir`. + * + * Return 1 if we should ignore it. + */ +static int ef_ignore_xattr(const FSEventStreamEventFlags ef) +{ + static const FSEventStreamEventFlags mask = + kFSEventStreamEventFlagItemChangeOwner | + kFSEventStreamEventFlagItemCreated | + kFSEventStreamEventFlagItemFinderInfoMod | + kFSEventStreamEventFlagItemInodeMetaMod | + kFSEventStreamEventFlagItemModified | + kFSEventStreamEventFlagItemRemoved | + kFSEventStreamEventFlagItemRenamed | + kFSEventStreamEventFlagItemXattrMod | + kFSEventStreamEventFlagItemCloned; + + return ((ef & mask) == kFSEventStreamEventFlagItemXattrMod); +} + +/* + * On MacOS we have to adjust for Unicode composition insensitivity + * (where NFC and NFD spellings are not respected). The different + * spellings are essentially aliases regardless of how the path is + * actually stored on the disk. + * + * This is related to "core.precomposeUnicode" (which wants to try + * to hide NFD completely and treat everything as NFC). Here, we + * don't know what the value the client has (or will have) for this + * config setting when they make a query, so assume the worst and + * emit both when the OS gives us an NFD path. + */ +static void my_add_path(struct fsmonitor_batch *batch, const char *path) +{ + char *composed; + + /* add the NFC or NFD path as received from the OS */ + fsmonitor_batch__add_path(batch, path); + + /* if NFD, also add the corresponding NFC spelling */ + composed = (char *)precompose_string_if_needed(path); + if (!composed || composed == path) + return; + + fsmonitor_batch__add_path(batch, composed); + free(composed); +} + + +static void fsevent_callback(ConstFSEventStreamRef streamRef, + void *ctx, + size_t num_of_events, + void *event_paths, + const FSEventStreamEventFlags event_flags[], + const FSEventStreamEventId event_ids[]) +{ + struct fsmonitor_daemon_state *state = ctx; + struct fsm_listen_data *data = state->listen_data; + char **paths = (char **)event_paths; + struct fsmonitor_batch *batch = NULL; + struct string_list cookie_list = STRING_LIST_INIT_DUP; + const char *path_k; + const char *slash; + int k; + struct strbuf tmp = STRBUF_INIT; + + /* + * Build a list of all filesystem changes into a private/local + * list and without holding any locks. + */ + for (k = 0; k < num_of_events; k++) { + /* + * On Mac, we receive an array of absolute paths. + */ + path_k = paths[k]; + + /* + * If you want to debug FSEvents, log them to GIT_TRACE_FSMONITOR. + * Please don't log them to Trace2. + * + * trace_printf_key(&trace_fsmonitor, "Path: '%s'", path_k); + */ + + /* + * If event[k] is marked as dropped, we assume that we have + * lost sync with the filesystem and should flush our cached + * data. We need to: + * + * [1] Abort/wake any client threads waiting for a cookie and + * flush the cached state data (the current token), and + * create a new token. + * + * [2] Discard the batch that we were locally building (since + * they are conceptually relative to the just flushed + * token). + */ + if (ef_is_dropped(event_flags[k])) { + if (trace_pass_fl(&trace_fsmonitor)) + log_flags_set(path_k, event_flags[k]); + + fsmonitor_force_resync(state); + fsmonitor_batch__free_list(batch); + string_list_clear(&cookie_list, 0); + + /* + * We assume that any events that we received + * in this callback after this dropped event + * may still be valid, so we continue rather + * than break. (And just in case there is a + * delete of ".git" hiding in there.) + */ + continue; + } + + if (ef_is_root_changed(event_flags[k])) { + /* + * The spelling of the pathname of the root directory + * has changed. This includes the name of the root + * directory itself or of any parent directory in the + * path. + * + * (There may be other conditions that throw this, + * but I couldn't find any information on it.) + * + * Force a shutdown now and avoid things getting + * out of sync. The Unix domain socket is inside + * the .git directory and a spelling change will make + * it hard for clients to rendezvous with us. + */ + trace_printf_key(&trace_fsmonitor, + "event: root changed"); + goto force_shutdown; + } + + if (ef_ignore_xattr(event_flags[k])) { + trace_printf_key(&trace_fsmonitor, + "ignore-xattr: '%s', flags=0x%x", + path_k, event_flags[k]); + continue; + } + + switch (fsmonitor_classify_path_absolute(state, path_k)) { + + case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX: + case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX: + /* special case cookie files within .git or gitdir */ + + /* Use just the filename of the cookie file. */ + slash = find_last_dir_sep(path_k); + string_list_append(&cookie_list, + slash ? slash + 1 : path_k); + break; + + case IS_INSIDE_DOT_GIT: + case IS_INSIDE_GITDIR: + /* ignore all other paths inside of .git or gitdir */ + break; + + case IS_DOT_GIT: + case IS_GITDIR: + /* + * If .git directory is deleted or renamed away, + * we have to quit. + */ + if (ef_is_root_delete(event_flags[k])) { + trace_printf_key(&trace_fsmonitor, + "event: gitdir removed"); + goto force_shutdown; + } + if (ef_is_root_renamed(event_flags[k])) { + trace_printf_key(&trace_fsmonitor, + "event: gitdir renamed"); + goto force_shutdown; + } + break; + + case IS_WORKDIR_PATH: + /* try to queue normal pathnames */ + + if (trace_pass_fl(&trace_fsmonitor)) + log_flags_set(path_k, event_flags[k]); + + /* + * Because of the implicit "binning" (the + * kernel calls us at a given frequency) and + * de-duping (the kernel is free to combine + * multiple events for a given pathname), an + * individual fsevent could be marked as both + * a file and directory. Add it to the queue + * with both spellings so that the client will + * know how much to invalidate/refresh. + */ + + if (event_flags[k] & kFSEventStreamEventFlagItemIsFile) { + const char *rel = path_k + + state->path_worktree_watch.len + 1; + + if (!batch) + batch = fsmonitor_batch__new(); + my_add_path(batch, rel); + } + + if (event_flags[k] & kFSEventStreamEventFlagItemIsDir) { + const char *rel = path_k + + state->path_worktree_watch.len + 1; + + strbuf_reset(&tmp); + strbuf_addstr(&tmp, rel); + strbuf_addch(&tmp, '/'); + + if (!batch) + batch = fsmonitor_batch__new(); + my_add_path(batch, tmp.buf); + } + + break; + + case IS_OUTSIDE_CONE: + default: + trace_printf_key(&trace_fsmonitor, + "ignoring '%s'", path_k); + break; + } + } + + fsmonitor_publish(state, batch, &cookie_list); + string_list_clear(&cookie_list, 0); + strbuf_release(&tmp); + return; + +force_shutdown: + fsmonitor_batch__free_list(batch); + string_list_clear(&cookie_list, 0); + + data->shutdown_style = FORCE_SHUTDOWN; + CFRunLoopStop(data->rl); + strbuf_release(&tmp); + return; +} + +/* + * In the call to `FSEventStreamCreate()` to setup our watch, the + * `latency` argument determines the frequency of calls to our callback + * with new FS events. Too slow and events get dropped; too fast and + * we burn CPU unnecessarily. Since it is rather obscure, I don't + * think this needs to be a config setting. I've done extensive + * testing on my systems and chosen the value below. It gives good + * results and I've not seen any dropped events. + * + * With a latency of 0.1, I was seeing lots of dropped events during + * the "touch 100000" files test within t/perf/p7519, but with a + * latency of 0.001 I did not see any dropped events. So I'm going + * to assume that this is the "correct" value. + * + * https://developer.apple.com/documentation/coreservices/1443980-fseventstreamcreate + */ + +int fsm_listen__ctor(struct fsmonitor_daemon_state *state) +{ + FSEventStreamCreateFlags flags = kFSEventStreamCreateFlagNoDefer | + kFSEventStreamCreateFlagWatchRoot | + kFSEventStreamCreateFlagFileEvents; + FSEventStreamContext ctx = { + 0, + state, + NULL, + NULL, + NULL + }; + struct fsm_listen_data *data; + const void *dir_array[2]; + + CALLOC_ARRAY(data, 1); + state->listen_data = data; + + data->cfsr_worktree_path = CFStringCreateWithCString( + NULL, state->path_worktree_watch.buf, kCFStringEncodingUTF8); + dir_array[data->nr_paths_watching++] = data->cfsr_worktree_path; + + if (state->nr_paths_watching > 1) { + data->cfsr_gitdir_path = CFStringCreateWithCString( + NULL, state->path_gitdir_watch.buf, + kCFStringEncodingUTF8); + dir_array[data->nr_paths_watching++] = data->cfsr_gitdir_path; + } + + data->cfar_paths_to_watch = CFArrayCreate(NULL, dir_array, + data->nr_paths_watching, + NULL); + data->stream = FSEventStreamCreate(NULL, fsevent_callback, &ctx, + data->cfar_paths_to_watch, + kFSEventStreamEventIdSinceNow, + 0.001, flags); + if (!data->stream) + goto failed; + + /* + * `data->rl` needs to be set inside the listener thread. + */ + + return 0; + +failed: + error(_("Unable to create FSEventStream.")); + + FREE_AND_NULL(state->listen_data); + return -1; +} + +void fsm_listen__dtor(struct fsmonitor_daemon_state *state) +{ + struct fsm_listen_data *data; + + if (!state || !state->listen_data) + return; + + data = state->listen_data; + + if (data->stream) { + if (data->stream_started) + FSEventStreamStop(data->stream); + if (data->stream_scheduled) + FSEventStreamInvalidate(data->stream); + FSEventStreamRelease(data->stream); + } + + FREE_AND_NULL(state->listen_data); +} + +void fsm_listen__stop_async(struct fsmonitor_daemon_state *state) +{ + struct fsm_listen_data *data; + + data = state->listen_data; + data->shutdown_style = SHUTDOWN_EVENT; + + CFRunLoopStop(data->rl); +} + +void fsm_listen__loop(struct fsmonitor_daemon_state *state) +{ + struct fsm_listen_data *data; + + data = state->listen_data; + + data->rl = CFRunLoopGetCurrent(); + + FSEventStreamScheduleWithRunLoop(data->stream, data->rl, kCFRunLoopDefaultMode); + data->stream_scheduled = 1; + + if (!FSEventStreamStart(data->stream)) { + error(_("Failed to start the FSEventStream")); + goto force_error_stop_without_loop; + } + data->stream_started = 1; + + CFRunLoopRun(); + + switch (data->shutdown_style) { + case FORCE_ERROR_STOP: + state->listen_error_code = -1; + /* fall thru */ + case FORCE_SHUTDOWN: + ipc_server_stop_async(state->ipc_server_data); + /* fall thru */ + case SHUTDOWN_EVENT: + default: + break; + } + return; + +force_error_stop_without_loop: + state->listen_error_code = -1; + ipc_server_stop_async(state->ipc_server_data); + return; +} diff --git a/compat/fsmonitor/fsm-listen-win32.c b/compat/fsmonitor/fsm-listen-win32.c new file mode 100644 index 0000000..03df8d9 --- /dev/null +++ b/compat/fsmonitor/fsm-listen-win32.c @@ -0,0 +1,874 @@ +#include "cache.h" +#include "config.h" +#include "fsmonitor.h" +#include "fsm-listen.h" +#include "fsmonitor--daemon.h" + +/* + * The documentation of ReadDirectoryChangesW() states that the maximum + * buffer size is 64K when the monitored directory is remote. + * + * Larger buffers may be used when the monitored directory is local and + * will help us receive events faster from the kernel and avoid dropped + * events. + * + * So we try to use a very large buffer and silently fallback to 64K if + * we get an error. + */ +#define MAX_RDCW_BUF_FALLBACK (65536) +#define MAX_RDCW_BUF (65536 * 8) + +struct one_watch +{ + char buffer[MAX_RDCW_BUF]; + DWORD buf_len; + DWORD count; + + struct strbuf path; + wchar_t wpath_longname[MAX_PATH + 1]; + DWORD wpath_longname_len; + + HANDLE hDir; + HANDLE hEvent; + OVERLAPPED overlapped; + + /* + * Is there an active ReadDirectoryChangesW() call pending. If so, we + * need to later call GetOverlappedResult() and possibly CancelIoEx(). + */ + BOOL is_active; + + /* + * Are shortnames enabled on the containing drive? This is + * always true for "C:/" drives and usually never true for + * other drives. + * + * We only set this for the worktree because we only need to + * convert shortname paths to longname paths for items we send + * to clients. (We don't care about shortname expansion for + * paths inside a GITDIR because we never send them to + * clients.) + */ + BOOL has_shortnames; + BOOL has_tilde; + wchar_t dotgit_shortname[16]; /* for 8.3 name */ +}; + +struct fsm_listen_data +{ + struct one_watch *watch_worktree; + struct one_watch *watch_gitdir; + + HANDLE hEventShutdown; + + HANDLE hListener[3]; /* we don't own these handles */ +#define LISTENER_SHUTDOWN 0 +#define LISTENER_HAVE_DATA_WORKTREE 1 +#define LISTENER_HAVE_DATA_GITDIR 2 + int nr_listener_handles; +}; + +/* + * Convert the WCHAR path from the event into UTF8 and normalize it. + * + * `wpath_len` is in WCHARS not bytes. + */ +static int normalize_path_in_utf8(wchar_t *wpath, DWORD wpath_len, + struct strbuf *normalized_path) +{ + int reserve; + int len = 0; + + strbuf_reset(normalized_path); + if (!wpath_len) + goto normalize; + + /* + * Pre-reserve enough space in the UTF8 buffer for + * each Unicode WCHAR character to be mapped into a + * sequence of 2 UTF8 characters. That should let us + * avoid ERROR_INSUFFICIENT_BUFFER 99.9+% of the time. + */ + reserve = 2 * wpath_len + 1; + strbuf_grow(normalized_path, reserve); + + for (;;) { + len = WideCharToMultiByte(CP_UTF8, 0, + wpath, wpath_len, + normalized_path->buf, + strbuf_avail(normalized_path) - 1, + NULL, NULL); + if (len > 0) + goto normalize; + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + error(_("[GLE %ld] could not convert path to UTF-8: '%.*ls'"), + GetLastError(), (int)wpath_len, wpath); + return -1; + } + + strbuf_grow(normalized_path, + strbuf_avail(normalized_path) + reserve); + } + +normalize: + strbuf_setlen(normalized_path, len); + return strbuf_normalize_path(normalized_path); +} + +/* + * See if the worktree root directory has shortnames enabled. + * This will help us decide if we need to do an expensive shortname + * to longname conversion on every notification event. + * + * We do not want to create a file to test this, so we assume that the + * root directory contains a ".git" file or directory. (Our caller + * only calls us for the worktree root, so this should be fine.) + * + * Remember the spelling of the shortname for ".git" if it exists. + */ +static void check_for_shortnames(struct one_watch *watch) +{ + wchar_t buf_in[MAX_PATH + 1]; + wchar_t buf_out[MAX_PATH + 1]; + wchar_t *last; + wchar_t *p; + + /* build L"<wt-root-path>/.git" */ + swprintf(buf_in, ARRAY_SIZE(buf_in) - 1, L"%ls.git", + watch->wpath_longname); + + if (!GetShortPathNameW(buf_in, buf_out, ARRAY_SIZE(buf_out))) + return; + + /* + * Get the final filename component of the shortpath. + * We know that the path does not have a final slash. + */ + for (last = p = buf_out; *p; p++) + if (*p == L'/' || *p == '\\') + last = p + 1; + + if (!wcscmp(last, L".git")) + return; + + watch->has_shortnames = 1; + wcsncpy(watch->dotgit_shortname, last, + ARRAY_SIZE(watch->dotgit_shortname)); + + /* + * The shortname for ".git" is usually of the form "GIT~1", so + * we should be able to avoid shortname to longname mapping on + * every notification event if the source string does not + * contain a "~". + * + * However, the documentation for GetLongPathNameW() says + * that there are filesystems that don't follow that pattern + * and warns against this optimization. + * + * Lets test this. + */ + if (wcschr(watch->dotgit_shortname, L'~')) + watch->has_tilde = 1; +} + +enum get_relative_result { + GRR_NO_CONVERSION_NEEDED, + GRR_HAVE_CONVERSION, + GRR_SHUTDOWN, +}; + +/* + * Info notification paths are relative to the root of the watch. + * If our CWD is still at the root, then we can use relative paths + * to convert from shortnames to longnames. If our process has a + * different CWD, then we need to construct an absolute path, do + * the conversion, and then return the root-relative portion. + * + * We use the longname form of the root as our basis and assume that + * it already has a trailing slash. + * + * `wpath_len` is in WCHARS not bytes. + */ +static enum get_relative_result get_relative_longname( + struct one_watch *watch, + const wchar_t *wpath, DWORD wpath_len, + wchar_t *wpath_longname, size_t bufsize_wpath_longname) +{ + wchar_t buf_in[2 * MAX_PATH + 1]; + wchar_t buf_out[MAX_PATH + 1]; + DWORD root_len; + DWORD out_len; + + /* + * Build L"<wt-root-path>/<event-rel-path>" + * Note that the <event-rel-path> might not be null terminated + * so we avoid swprintf() constructions. + */ + root_len = watch->wpath_longname_len; + if (root_len + wpath_len >= ARRAY_SIZE(buf_in)) { + /* + * This should not happen. We cannot append the observed + * relative path onto the end of the worktree root path + * without overflowing the buffer. Just give up. + */ + return GRR_SHUTDOWN; + } + wcsncpy(buf_in, watch->wpath_longname, root_len); + wcsncpy(buf_in + root_len, wpath, wpath_len); + buf_in[root_len + wpath_len] = 0; + + /* + * We don't actually know if the source pathname is a + * shortname or a longname. This Windows routine allows + * either to be given as input. + */ + out_len = GetLongPathNameW(buf_in, buf_out, ARRAY_SIZE(buf_out)); + if (!out_len) { + /* + * The shortname to longname conversion can fail for + * various reasons, for example if the file has been + * deleted. (That is, if we just received a + * delete-file notification event and the file is + * already gone, we can't ask the file system to + * lookup the longname for it. Likewise, for moves + * and renames where we are given the old name.) + * + * Since deleting or moving a file or directory by its + * shortname is rather obscure, I'm going ignore the + * failure and ask the caller to report the original + * relative path. This seems kinder than failing here + * and forcing a resync. Besides, forcing a resync on + * every file/directory delete would effectively + * cripple monitoring. + * + * We might revisit this in the future. + */ + return GRR_NO_CONVERSION_NEEDED; + } + + if (!wcscmp(buf_in, buf_out)) { + /* + * The path does not have a shortname alias. + */ + return GRR_NO_CONVERSION_NEEDED; + } + + if (wcsncmp(buf_in, buf_out, root_len)) { + /* + * The spelling of the root directory portion of the computed + * longname has changed. This should not happen. Basically, + * it means that we don't know where (without recomputing the + * longname of just the root directory) to split out the + * relative path. Since this should not happen, I'm just + * going to let this fail and force a shutdown (because all + * subsequent events are probably going to see the same + * mismatch). + */ + return GRR_SHUTDOWN; + } + + if (out_len - root_len >= bufsize_wpath_longname) { + /* + * This should not happen. We cannot copy the root-relative + * portion of the path into the provided buffer without an + * overrun. Just give up. + */ + return GRR_SHUTDOWN; + } + + /* Return the worktree root-relative portion of the longname. */ + + wcscpy(wpath_longname, buf_out + root_len); + return GRR_HAVE_CONVERSION; +} + +void fsm_listen__stop_async(struct fsmonitor_daemon_state *state) +{ + SetEvent(state->listen_data->hListener[LISTENER_SHUTDOWN]); +} + +static struct one_watch *create_watch(struct fsmonitor_daemon_state *state, + const char *path) +{ + struct one_watch *watch = NULL; + DWORD desired_access = FILE_LIST_DIRECTORY; + DWORD share_mode = + FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE; + HANDLE hDir; + DWORD len_longname; + wchar_t wpath[MAX_PATH + 1]; + wchar_t wpath_longname[MAX_PATH + 1]; + + if (xutftowcs_path(wpath, path) < 0) { + error(_("could not convert to wide characters: '%s'"), path); + return NULL; + } + + hDir = CreateFileW(wpath, + desired_access, share_mode, NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, + NULL); + if (hDir == INVALID_HANDLE_VALUE) { + error(_("[GLE %ld] could not watch '%s'"), + GetLastError(), path); + return NULL; + } + + len_longname = GetLongPathNameW(wpath, wpath_longname, + ARRAY_SIZE(wpath_longname)); + if (!len_longname) { + error(_("[GLE %ld] could not get longname of '%s'"), + GetLastError(), path); + CloseHandle(hDir); + return NULL; + } + + if (wpath_longname[len_longname - 1] != L'/' && + wpath_longname[len_longname - 1] != L'\\') { + wpath_longname[len_longname++] = L'/'; + wpath_longname[len_longname] = 0; + } + + CALLOC_ARRAY(watch, 1); + + watch->buf_len = sizeof(watch->buffer); /* assume full MAX_RDCW_BUF */ + + strbuf_init(&watch->path, 0); + strbuf_addstr(&watch->path, path); + + wcscpy(watch->wpath_longname, wpath_longname); + watch->wpath_longname_len = len_longname; + + watch->hDir = hDir; + watch->hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + + return watch; +} + +static void destroy_watch(struct one_watch *watch) +{ + if (!watch) + return; + + strbuf_release(&watch->path); + if (watch->hDir != INVALID_HANDLE_VALUE) + CloseHandle(watch->hDir); + if (watch->hEvent != INVALID_HANDLE_VALUE) + CloseHandle(watch->hEvent); + + free(watch); +} + +static int start_rdcw_watch(struct fsm_listen_data *data, + struct one_watch *watch) +{ + DWORD dwNotifyFilter = + FILE_NOTIFY_CHANGE_FILE_NAME | + FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_ATTRIBUTES | + FILE_NOTIFY_CHANGE_SIZE | + FILE_NOTIFY_CHANGE_LAST_WRITE | + FILE_NOTIFY_CHANGE_CREATION; + + ResetEvent(watch->hEvent); + + memset(&watch->overlapped, 0, sizeof(watch->overlapped)); + watch->overlapped.hEvent = watch->hEvent; + + /* + * Queue an async call using Overlapped IO. This returns immediately. + * Our event handle will be signalled when the real result is available. + * + * The return value here just means that we successfully queued it. + * We won't know if the Read...() actually produces data until later. + */ + watch->is_active = ReadDirectoryChangesW( + watch->hDir, watch->buffer, watch->buf_len, TRUE, + dwNotifyFilter, &watch->count, &watch->overlapped, NULL); + + if (watch->is_active) + return 0; + + error(_("ReadDirectoryChangedW failed on '%s' [GLE %ld]"), + watch->path.buf, GetLastError()); + return -1; +} + +static int recv_rdcw_watch(struct one_watch *watch) +{ + DWORD gle; + + watch->is_active = FALSE; + + /* + * The overlapped result is ready. If the Read...() was successful + * we finally receive the actual result into our buffer. + */ + if (GetOverlappedResult(watch->hDir, &watch->overlapped, &watch->count, + TRUE)) + return 0; + + gle = GetLastError(); + if (gle == ERROR_INVALID_PARAMETER && + /* + * The kernel throws an invalid parameter error when our + * buffer is too big and we are pointed at a remote + * directory (and possibly for other reasons). Quietly + * set it down and try again. + * + * See note about MAX_RDCW_BUF at the top. + */ + watch->buf_len > MAX_RDCW_BUF_FALLBACK) { + watch->buf_len = MAX_RDCW_BUF_FALLBACK; + return -2; + } + + /* + * GetOverlappedResult() fails if the watched directory is + * deleted while we were waiting for an overlapped IO to + * complete. The documentation did not list specific errors, + * but I observed ERROR_ACCESS_DENIED (0x05) errors during + * testing. + * + * Note that we only get notificaiton events for events + * *within* the directory, not *on* the directory itself. + * (These might be properies of the parent directory, for + * example). + * + * NEEDSWORK: We might try to check for the deleted directory + * case and return a better error message, but I'm not sure it + * is worth it. + * + * Shutdown if we get any error. + */ + + error(_("GetOverlappedResult failed on '%s' [GLE %ld]"), + watch->path.buf, gle); + return -1; +} + +static void cancel_rdcw_watch(struct one_watch *watch) +{ + DWORD count; + + if (!watch || !watch->is_active) + return; + + /* + * The calls to ReadDirectoryChangesW() and GetOverlappedResult() + * form a "pair" (my term) where we queue an IO and promise to + * hang around and wait for the kernel to give us the result. + * + * If for some reason after we queue the IO, we have to quit + * or otherwise not stick around for the second half, we must + * tell the kernel to abort the IO. This prevents the kernel + * from writing to our buffer and/or signalling our event + * after we free them. + * + * (Ask me how much fun it was to track that one down). + */ + CancelIoEx(watch->hDir, &watch->overlapped); + GetOverlappedResult(watch->hDir, &watch->overlapped, &count, TRUE); + watch->is_active = FALSE; +} + +/* + * Process a single relative pathname event. + * Return 1 if we should shutdown. + */ +static int process_1_worktree_event( + struct string_list *cookie_list, + struct fsmonitor_batch **batch, + const struct strbuf *path, + enum fsmonitor_path_type t, + DWORD info_action) +{ + const char *slash; + + switch (t) { + case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX: + /* special case cookie files within .git */ + + /* Use just the filename of the cookie file. */ + slash = find_last_dir_sep(path->buf); + string_list_append(cookie_list, + slash ? slash + 1 : path->buf); + break; + + case IS_INSIDE_DOT_GIT: + /* ignore everything inside of "<worktree>/.git/" */ + break; + + case IS_DOT_GIT: + /* "<worktree>/.git" was deleted (or renamed away) */ + if ((info_action == FILE_ACTION_REMOVED) || + (info_action == FILE_ACTION_RENAMED_OLD_NAME)) { + trace2_data_string("fsmonitor", NULL, + "fsm-listen/dotgit", + "removed"); + return 1; + } + break; + + case IS_WORKDIR_PATH: + /* queue normal pathname */ + if (!*batch) + *batch = fsmonitor_batch__new(); + fsmonitor_batch__add_path(*batch, path->buf); + break; + + case IS_GITDIR: + case IS_INSIDE_GITDIR: + case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX: + default: + BUG("unexpected path classification '%d' for '%s'", + t, path->buf); + } + + return 0; +} + +/* + * Process filesystem events that happen anywhere (recursively) under the + * <worktree> root directory. For a normal working directory, this includes + * both version controlled files and the contents of the .git/ directory. + * + * If <worktree>/.git is a file, then we only see events for the file + * itself. + */ +static int process_worktree_events(struct fsmonitor_daemon_state *state) +{ + struct fsm_listen_data *data = state->listen_data; + struct one_watch *watch = data->watch_worktree; + struct strbuf path = STRBUF_INIT; + struct string_list cookie_list = STRING_LIST_INIT_DUP; + struct fsmonitor_batch *batch = NULL; + const char *p = watch->buffer; + wchar_t wpath_longname[MAX_PATH + 1]; + + /* + * If the kernel gets more events than will fit in the kernel + * buffer associated with our RDCW handle, it drops them and + * returns a count of zero. + * + * Yes, the call returns WITHOUT error and with length zero. + * This is the documented behavior. (My testing has confirmed + * that it also sets the last error to ERROR_NOTIFY_ENUM_DIR, + * but we do not rely on that since the function did not + * return an error and it is not documented.) + * + * (The "overflow" case is not ambiguous with the "no data" case + * because we did an INFINITE wait.) + * + * This means we have a gap in coverage. Tell the daemon layer + * to resync. + */ + if (!watch->count) { + trace2_data_string("fsmonitor", NULL, "fsm-listen/kernel", + "overflow"); + fsmonitor_force_resync(state); + return LISTENER_HAVE_DATA_WORKTREE; + } + + /* + * On Windows, `info` contains an "array" of paths that are + * relative to the root of whichever directory handle received + * the event. + */ + for (;;) { + FILE_NOTIFY_INFORMATION *info = (void *)p; + wchar_t *wpath = info->FileName; + DWORD wpath_len = info->FileNameLength / sizeof(WCHAR); + enum fsmonitor_path_type t; + enum get_relative_result grr; + + if (watch->has_shortnames) { + if (!wcscmp(wpath, watch->dotgit_shortname)) { + /* + * This event exactly matches the + * spelling of the shortname of + * ".git", so we can skip some steps. + * + * (This case is odd because the user + * can "rm -rf GIT~1" and we cannot + * use the filesystem to map it back + * to ".git".) + */ + strbuf_reset(&path); + strbuf_addstr(&path, ".git"); + t = IS_DOT_GIT; + goto process_it; + } + + if (watch->has_tilde && !wcschr(wpath, L'~')) { + /* + * Shortnames on this filesystem have tildes + * and the notification path does not have + * one, so we assume that it is a longname. + */ + goto normalize_it; + } + + grr = get_relative_longname(watch, wpath, wpath_len, + wpath_longname, + ARRAY_SIZE(wpath_longname)); + switch (grr) { + case GRR_NO_CONVERSION_NEEDED: /* use info buffer as is */ + break; + case GRR_HAVE_CONVERSION: + wpath = wpath_longname; + wpath_len = wcslen(wpath); + break; + default: + case GRR_SHUTDOWN: + goto force_shutdown; + } + } + +normalize_it: + if (normalize_path_in_utf8(wpath, wpath_len, &path) == -1) + goto skip_this_path; + + t = fsmonitor_classify_path_workdir_relative(path.buf); + +process_it: + if (process_1_worktree_event(&cookie_list, &batch, &path, t, + info->Action)) + goto force_shutdown; + +skip_this_path: + if (!info->NextEntryOffset) + break; + p += info->NextEntryOffset; + } + + fsmonitor_publish(state, batch, &cookie_list); + batch = NULL; + string_list_clear(&cookie_list, 0); + strbuf_release(&path); + return LISTENER_HAVE_DATA_WORKTREE; + +force_shutdown: + fsmonitor_batch__free_list(batch); + string_list_clear(&cookie_list, 0); + strbuf_release(&path); + return LISTENER_SHUTDOWN; +} + +/* + * Process filesystem events that happened anywhere (recursively) under the + * external <gitdir> (such as non-primary worktrees or submodules). + * We only care about cookie files that our client threads created here. + * + * Note that we DO NOT get filesystem events on the external <gitdir> + * itself (it is not inside something that we are watching). In particular, + * we do not get an event if the external <gitdir> is deleted. + * + * Also, we do not care about shortnames within the external <gitdir>, since + * we never send these paths to clients. + */ +static int process_gitdir_events(struct fsmonitor_daemon_state *state) +{ + struct fsm_listen_data *data = state->listen_data; + struct one_watch *watch = data->watch_gitdir; + struct strbuf path = STRBUF_INIT; + struct string_list cookie_list = STRING_LIST_INIT_DUP; + const char *p = watch->buffer; + + if (!watch->count) { + trace2_data_string("fsmonitor", NULL, "fsm-listen/kernel", + "overflow"); + fsmonitor_force_resync(state); + return LISTENER_HAVE_DATA_GITDIR; + } + + for (;;) { + FILE_NOTIFY_INFORMATION *info = (void *)p; + const char *slash; + enum fsmonitor_path_type t; + + if (normalize_path_in_utf8( + info->FileName, + info->FileNameLength / sizeof(WCHAR), + &path) == -1) + goto skip_this_path; + + t = fsmonitor_classify_path_gitdir_relative(path.buf); + + switch (t) { + case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX: + /* special case cookie files within gitdir */ + + /* Use just the filename of the cookie file. */ + slash = find_last_dir_sep(path.buf); + string_list_append(&cookie_list, + slash ? slash + 1 : path.buf); + break; + + case IS_INSIDE_GITDIR: + goto skip_this_path; + + default: + BUG("unexpected path classification '%d' for '%s'", + t, path.buf); + } + +skip_this_path: + if (!info->NextEntryOffset) + break; + p += info->NextEntryOffset; + } + + fsmonitor_publish(state, NULL, &cookie_list); + string_list_clear(&cookie_list, 0); + strbuf_release(&path); + return LISTENER_HAVE_DATA_GITDIR; +} + +void fsm_listen__loop(struct fsmonitor_daemon_state *state) +{ + struct fsm_listen_data *data = state->listen_data; + DWORD dwWait; + int result; + + state->listen_error_code = 0; + + if (start_rdcw_watch(data, data->watch_worktree) == -1) + goto force_error_stop; + + if (data->watch_gitdir && + start_rdcw_watch(data, data->watch_gitdir) == -1) + goto force_error_stop; + + for (;;) { + dwWait = WaitForMultipleObjects(data->nr_listener_handles, + data->hListener, + FALSE, INFINITE); + + if (dwWait == WAIT_OBJECT_0 + LISTENER_HAVE_DATA_WORKTREE) { + result = recv_rdcw_watch(data->watch_worktree); + if (result == -1) { + /* hard error */ + goto force_error_stop; + } + if (result == -2) { + /* retryable error */ + if (start_rdcw_watch(data, data->watch_worktree) == -1) + goto force_error_stop; + continue; + } + + /* have data */ + if (process_worktree_events(state) == LISTENER_SHUTDOWN) + goto force_shutdown; + if (start_rdcw_watch(data, data->watch_worktree) == -1) + goto force_error_stop; + continue; + } + + if (dwWait == WAIT_OBJECT_0 + LISTENER_HAVE_DATA_GITDIR) { + result = recv_rdcw_watch(data->watch_gitdir); + if (result == -1) { + /* hard error */ + goto force_error_stop; + } + if (result == -2) { + /* retryable error */ + if (start_rdcw_watch(data, data->watch_gitdir) == -1) + goto force_error_stop; + continue; + } + + /* have data */ + if (process_gitdir_events(state) == LISTENER_SHUTDOWN) + goto force_shutdown; + if (start_rdcw_watch(data, data->watch_gitdir) == -1) + goto force_error_stop; + continue; + } + + if (dwWait == WAIT_OBJECT_0 + LISTENER_SHUTDOWN) + goto clean_shutdown; + + error(_("could not read directory changes [GLE %ld]"), + GetLastError()); + goto force_error_stop; + } + +force_error_stop: + state->listen_error_code = -1; + +force_shutdown: + /* + * Tell the IPC thead pool to stop (which completes the await + * in the main thread (which will also signal this thread (if + * we are still alive))). + */ + ipc_server_stop_async(state->ipc_server_data); + +clean_shutdown: + cancel_rdcw_watch(data->watch_worktree); + cancel_rdcw_watch(data->watch_gitdir); +} + +int fsm_listen__ctor(struct fsmonitor_daemon_state *state) +{ + struct fsm_listen_data *data; + + CALLOC_ARRAY(data, 1); + + data->hEventShutdown = CreateEvent(NULL, TRUE, FALSE, NULL); + + data->watch_worktree = create_watch(state, + state->path_worktree_watch.buf); + if (!data->watch_worktree) + goto failed; + + check_for_shortnames(data->watch_worktree); + + if (state->nr_paths_watching > 1) { + data->watch_gitdir = create_watch(state, + state->path_gitdir_watch.buf); + if (!data->watch_gitdir) + goto failed; + } + + data->hListener[LISTENER_SHUTDOWN] = data->hEventShutdown; + data->nr_listener_handles++; + + data->hListener[LISTENER_HAVE_DATA_WORKTREE] = + data->watch_worktree->hEvent; + data->nr_listener_handles++; + + if (data->watch_gitdir) { + data->hListener[LISTENER_HAVE_DATA_GITDIR] = + data->watch_gitdir->hEvent; + data->nr_listener_handles++; + } + + state->listen_data = data; + return 0; + +failed: + CloseHandle(data->hEventShutdown); + destroy_watch(data->watch_worktree); + destroy_watch(data->watch_gitdir); + + return -1; +} + +void fsm_listen__dtor(struct fsmonitor_daemon_state *state) +{ + struct fsm_listen_data *data; + + if (!state || !state->listen_data) + return; + + data = state->listen_data; + + CloseHandle(data->hEventShutdown); + destroy_watch(data->watch_worktree); + destroy_watch(data->watch_gitdir); + + FREE_AND_NULL(state->listen_data); +} diff --git a/compat/fsmonitor/fsm-listen.h b/compat/fsmonitor/fsm-listen.h new file mode 100644 index 0000000..41650bf --- /dev/null +++ b/compat/fsmonitor/fsm-listen.h @@ -0,0 +1,49 @@ +#ifndef FSM_LISTEN_H +#define FSM_LISTEN_H + +/* This needs to be implemented by each backend */ + +#ifdef HAVE_FSMONITOR_DAEMON_BACKEND + +struct fsmonitor_daemon_state; + +/* + * Initialize platform-specific data for the fsmonitor listener thread. + * This will be called from the main thread PRIOR to staring the + * fsmonitor_fs_listener thread. + * + * Returns 0 if successful. + * Returns -1 otherwise. + */ +int fsm_listen__ctor(struct fsmonitor_daemon_state *state); + +/* + * Cleanup platform-specific data for the fsmonitor listener thread. + * This will be called from the main thread AFTER joining the listener. + */ +void fsm_listen__dtor(struct fsmonitor_daemon_state *state); + +/* + * The main body of the platform-specific event loop to watch for + * filesystem events. This will run in the fsmonitor_fs_listen thread. + * + * It should call `ipc_server_stop_async()` if the listener thread + * prematurely terminates (because of a filesystem error or if it + * detects that the .git directory has been deleted). (It should NOT + * do so if the listener thread receives a normal shutdown signal from + * the IPC layer.) + * + * It should set `state->listen_error_code` to -1 if the daemon should exit + * with an error. + */ +void fsm_listen__loop(struct fsmonitor_daemon_state *state); + +/* + * Gently request that the fsmonitor listener thread shutdown. + * It does not wait for it to stop. The caller should do a JOIN + * to wait for it. + */ +void fsm_listen__stop_async(struct fsmonitor_daemon_state *state); + +#endif /* HAVE_FSMONITOR_DAEMON_BACKEND */ +#endif /* FSM_LISTEN_H */ diff --git a/compat/fsmonitor/fsm-settings-darwin.c b/compat/fsmonitor/fsm-settings-darwin.c new file mode 100644 index 0000000..efc732c --- /dev/null +++ b/compat/fsmonitor/fsm-settings-darwin.c @@ -0,0 +1,89 @@ +#include "cache.h" +#include "config.h" +#include "repository.h" +#include "fsmonitor-settings.h" +#include "fsmonitor.h" +#include <sys/param.h> +#include <sys/mount.h> + +/* + * [1] Remote working directories are problematic for FSMonitor. + * + * The underlying file system on the server machine and/or the remote + * mount type (NFS, SAMBA, etc.) dictates whether notification events + * are available at all to remote client machines. + * + * Kernel differences between the server and client machines also + * dictate the how (buffering, frequency, de-dup) the events are + * delivered to client machine processes. + * + * A client machine (such as a laptop) may choose to suspend/resume + * and it is unclear (without lots of testing) whether the watcher can + * resync after a resume. We might be able to treat this as a normal + * "events were dropped by the kernel" event and do our normal "flush + * and resync" --or-- we might need to close the existing (zombie?) + * notification fd and create a new one. + * + * In theory, the above issues need to be addressed whether we are + * using the Hook or IPC API. + * + * For the builtin FSMonitor, we create the Unix domain socket for the + * IPC in the .git directory. If the working directory is remote, + * then the socket will be created on the remote file system. This + * can fail if the remote file system does not support UDS file types + * (e.g. smbfs to a Windows server) or if the remote kernel does not + * allow a non-local process to bind() the socket. (These problems + * could be fixed by moving the UDS out of the .git directory and to a + * well-known local directory on the client machine, but care should + * be taken to ensure that $HOME is actually local and not a managed + * file share.) + * + * So (for now at least), mark remote working directories as + * incompatible. + * + * + * [2] FAT32 and NTFS working directories are problematic too. + * + * The builtin FSMonitor uses a Unix domain socket in the .git + * directory for IPC. These Windows drive formats do not support + * Unix domain sockets, so mark them as incompatible for the daemon. + * + */ +static enum fsmonitor_reason check_volume(struct repository *r) +{ + struct statfs fs; + + if (statfs(r->worktree, &fs) == -1) { + int saved_errno = errno; + trace_printf_key(&trace_fsmonitor, "statfs('%s') failed: %s", + r->worktree, strerror(saved_errno)); + errno = saved_errno; + return FSMONITOR_REASON_ERROR; + } + + trace_printf_key(&trace_fsmonitor, + "statfs('%s') [type 0x%08x][flags 0x%08x] '%s'", + r->worktree, fs.f_type, fs.f_flags, fs.f_fstypename); + + if (!(fs.f_flags & MNT_LOCAL)) + return FSMONITOR_REASON_REMOTE; + + if (!strcmp(fs.f_fstypename, "msdos")) /* aka FAT32 */ + return FSMONITOR_REASON_NOSOCKETS; + + if (!strcmp(fs.f_fstypename, "ntfs")) + return FSMONITOR_REASON_NOSOCKETS; + + return FSMONITOR_REASON_OK; +} + +enum fsmonitor_reason fsm_os__incompatible(struct repository *r) +{ + enum fsmonitor_reason reason; + + reason = check_volume(r); + if (reason != FSMONITOR_REASON_OK) + return reason; + + return FSMONITOR_REASON_OK; +} diff --git a/compat/fsmonitor/fsm-settings-win32.c b/compat/fsmonitor/fsm-settings-win32.c new file mode 100644 index 0000000..9076557 --- /dev/null +++ b/compat/fsmonitor/fsm-settings-win32.c @@ -0,0 +1,137 @@ +#include "cache.h" +#include "config.h" +#include "repository.h" +#include "fsmonitor-settings.h" +#include "fsmonitor.h" + +/* + * VFS for Git is incompatible with FSMonitor. + * + * Granted, core Git does not know anything about VFS for Git and we + * shouldn't make assumptions about a downstream feature, but users + * can install both versions. And this can lead to incorrect results + * from core Git commands. So, without bringing in any of the VFS for + * Git code, do a simple config test for a published config setting. + * (We do not look at the various *_TEST_* environment variables.) + */ +static enum fsmonitor_reason check_vfs4git(struct repository *r) +{ + const char *const_str; + + if (!repo_config_get_value(r, "core.virtualfilesystem", &const_str)) + return FSMONITOR_REASON_VFS4GIT; + + return FSMONITOR_REASON_OK; +} + +/* + * Remote working directories are problematic for FSMonitor. + * + * The underlying file system on the server machine and/or the remote + * mount type dictates whether notification events are available at + * all to remote client machines. + * + * Kernel differences between the server and client machines also + * dictate the how (buffering, frequency, de-dup) the events are + * delivered to client machine processes. + * + * A client machine (such as a laptop) may choose to suspend/resume + * and it is unclear (without lots of testing) whether the watcher can + * resync after a resume. We might be able to treat this as a normal + * "events were dropped by the kernel" event and do our normal "flush + * and resync" --or-- we might need to close the existing (zombie?) + * notification fd and create a new one. + * + * In theory, the above issues need to be addressed whether we are + * using the Hook or IPC API. + * + * So (for now at least), mark remote working directories as + * incompatible. + * + * Notes for testing: + * + * (a) Windows allows a network share to be mapped to a drive letter. + * (This is the normal method to access it.) + * + * $ NET USE Z: \\server\share + * $ git -C Z:/repo status + * + * (b) Windows allows a network share to be referenced WITHOUT mapping + * it to drive letter. + * + * $ NET USE \\server\share\dir + * $ git -C //server/share/repo status + * + * (c) Windows allows "SUBST" to create a fake drive mapping to an + * arbitrary path (which may be remote) + * + * $ SUBST Q: Z:\repo + * $ git -C Q:/ status + * + * (d) Windows allows a directory symlink to be created on a local + * file system that points to a remote repo. + * + * $ mklink /d ./link //server/share/repo + * $ git -C ./link status + */ +static enum fsmonitor_reason check_remote(struct repository *r) +{ + wchar_t wpath[MAX_PATH]; + wchar_t wfullpath[MAX_PATH]; + size_t wlen; + UINT driveType; + + /* + * Do everything in wide chars because the drive letter might be + * a multi-byte sequence. See win32_has_dos_drive_prefix(). + */ + if (xutftowcs_path(wpath, r->worktree) < 0) + return FSMONITOR_REASON_ERROR; + + /* + * GetDriveTypeW() requires a final slash. We assume that the + * worktree pathname points to an actual directory. + */ + wlen = wcslen(wpath); + if (wpath[wlen - 1] != L'\\' && wpath[wlen - 1] != L'/') { + wpath[wlen++] = L'\\'; + wpath[wlen] = 0; + } + + /* + * Normalize the path. If nothing else, this converts forward + * slashes to backslashes. This is essential to get GetDriveTypeW() + * correctly handle some UNC "\\server\share\..." paths. + */ + if (!GetFullPathNameW(wpath, MAX_PATH, wfullpath, NULL)) + return FSMONITOR_REASON_ERROR; + + driveType = GetDriveTypeW(wfullpath); + trace_printf_key(&trace_fsmonitor, + "DriveType '%s' L'%ls' (%u)", + r->worktree, wfullpath, driveType); + + if (driveType == DRIVE_REMOTE) { + trace_printf_key(&trace_fsmonitor, + "check_remote('%s') true", + r->worktree); + return FSMONITOR_REASON_REMOTE; + } + + return FSMONITOR_REASON_OK; +} + +enum fsmonitor_reason fsm_os__incompatible(struct repository *r) +{ + enum fsmonitor_reason reason; + + reason = check_vfs4git(r); + if (reason != FSMONITOR_REASON_OK) + return reason; + + reason = check_remote(r); + if (reason != FSMONITOR_REASON_OK) + return reason; + + return FSMONITOR_REASON_OK; +} diff --git a/compat/mingw.c b/compat/mingw.c index 58f347d..b550299 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -1,5 +1,6 @@ #include "../git-compat-util.h" #include "win32.h" +#include <aclapi.h> #include <conio.h> #include <wchar.h> #include "../strbuf.h" @@ -1058,10 +1059,7 @@ char *mingw_mktemp(char *template) int mkstemp(char *template) { - char *filename = mktemp(template); - if (filename == NULL) - return -1; - return open(filename, O_RDWR | O_CREAT, 0600); + return git_mkstemp_mode(template, 0600); } int gettimeofday(struct timeval *tv, void *tz) @@ -2331,7 +2329,7 @@ int setitimer(int type, struct itimerval *in, struct itimerval *out) static const struct timeval zero; static int atexit_done; - if (out != NULL) + if (out) return errno = EINVAL, error("setitimer param 3 != NULL not implemented"); if (!is_timeval_eq(&in->it_interval, &zero) && @@ -2360,7 +2358,7 @@ int sigaction(int sig, struct sigaction *in, struct sigaction *out) if (sig != SIGALRM) return errno = EINVAL, error("sigaction only implemented for SIGALRM"); - if (out != NULL) + if (out) return errno = EINVAL, error("sigaction: param 3 != NULL not implemented"); @@ -2645,6 +2643,92 @@ static void setup_windows_environment(void) } } +static PSID get_current_user_sid(void) +{ + HANDLE token; + DWORD len = 0; + PSID result = NULL; + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) + return NULL; + + if (!GetTokenInformation(token, TokenUser, NULL, 0, &len)) { + TOKEN_USER *info = xmalloc((size_t)len); + if (GetTokenInformation(token, TokenUser, info, len, &len)) { + len = GetLengthSid(info->User.Sid); + result = xmalloc(len); + if (!CopySid(len, result, info->User.Sid)) { + error(_("failed to copy SID (%ld)"), + GetLastError()); + FREE_AND_NULL(result); + } + } + FREE_AND_NULL(info); + } + CloseHandle(token); + + return result; +} + +int is_path_owned_by_current_sid(const char *path) +{ + WCHAR wpath[MAX_PATH]; + PSID sid = NULL; + PSECURITY_DESCRIPTOR descriptor = NULL; + DWORD err; + + static wchar_t home[MAX_PATH]; + + int result = 0; + + if (xutftowcs_path(wpath, path) < 0) + return 0; + + /* + * On Windows, the home directory is owned by the administrator, but for + * all practical purposes, it belongs to the user. Do pretend that it is + * owned by the user. + */ + if (!*home) { + DWORD size = ARRAY_SIZE(home); + DWORD len = GetEnvironmentVariableW(L"HOME", home, size); + if (!len || len > size) + wcscpy(home, L"::N/A::"); + } + if (!wcsicmp(wpath, home)) + return 1; + + /* Get the owner SID */ + err = GetNamedSecurityInfoW(wpath, SE_FILE_OBJECT, + OWNER_SECURITY_INFORMATION | + DACL_SECURITY_INFORMATION, + &sid, NULL, NULL, NULL, &descriptor); + + if (err != ERROR_SUCCESS) + error(_("failed to get owner for '%s' (%ld)"), path, err); + else if (sid && IsValidSid(sid)) { + /* Now, verify that the SID matches the current user's */ + static PSID current_user_sid; + + if (!current_user_sid) + current_user_sid = get_current_user_sid(); + + if (current_user_sid && + IsValidSid(current_user_sid) && + EqualSid(sid, current_user_sid)) + result = 1; + } + + /* + * We can release the security descriptor struct only now because `sid` + * actually points into this struct. + */ + if (descriptor) + LocalFree(descriptor); + + return result; +} + int is_valid_win32_path(const char *path, int allow_literal_nul) { const char *p = path; @@ -2743,7 +2827,7 @@ not_a_reserved_name: } c = path[i]; - if (c && c != '.' && c != ':' && c != '/' && c != '\\') + if (c && c != '.' && c != ':' && !is_xplatform_dir_sep(c)) goto not_a_reserved_name; /* contains reserved name */ diff --git a/compat/mingw.h b/compat/mingw.h index 6074a3d..a74da68 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -332,6 +332,9 @@ int mingw_getpagesize(void); int win32_fsync_no_flush(int fd); #define fsync_no_flush win32_fsync_no_flush +#define FSYNC_COMPONENTS_PLATFORM_DEFAULT (FSYNC_COMPONENTS_DEFAULT | FSYNC_COMPONENT_LOOSE_OBJECT) +#define FSYNC_METHOD_DEFAULT (FSYNC_METHOD_BATCH) + struct rlimit { unsigned int rlim_cur; }; @@ -457,6 +460,13 @@ char *mingw_query_user_email(void); #endif /** + * Verifies that the specified path is owned by the user running the + * current process. + */ +int is_path_owned_by_current_sid(const char *path); +#define is_path_owned_by_current_user is_path_owned_by_current_sid + +/** * Verifies that the given path is a valid one on Windows. * * In particular, path segments are disallowed which diff --git a/compat/mkdir.c b/compat/mkdir.c index 9e253fb..02aea3b 100644 --- a/compat/mkdir.c +++ b/compat/mkdir.c @@ -9,7 +9,7 @@ int compat_mkdir_wo_trailing_slash(const char *dir, mode_t mode) size_t len = strlen(dir); if (len && dir[len-1] == '/') { - if ((tmp_dir = strdup(dir)) == NULL) + if (!(tmp_dir = strdup(dir))) return -1; tmp_dir[len-1] = '\0'; } diff --git a/compat/mmap.c b/compat/mmap.c index 8d6c02d..2fe1c77 100644 --- a/compat/mmap.c +++ b/compat/mmap.c @@ -13,7 +13,7 @@ void *git_mmap(void *start, size_t length, int prot, int flags, int fd, off_t of } start = malloc(length); - if (start == NULL) { + if (!start) { errno = ENOMEM; return MAP_FAILED; } diff --git a/compat/nedmalloc/nedmalloc.c b/compat/nedmalloc/nedmalloc.c index edb438a..2c0ace7 100644 --- a/compat/nedmalloc/nedmalloc.c +++ b/compat/nedmalloc/nedmalloc.c @@ -323,7 +323,6 @@ static NOINLINE void RemoveCacheEntries(nedpool *p, threadcache *tc, unsigned in } static void DestroyCaches(nedpool *p) THROWSPEC { - if(p->caches) { threadcache *tc; int n; diff --git a/compat/terminal.c b/compat/terminal.c index 3620184..7db330c 100644 --- a/compat/terminal.c +++ b/compat/terminal.c @@ -1,4 +1,4 @@ -#include "git-compat-util.h" +#include "cache.h" #include "compat/terminal.h" #include "sigchain.h" #include "strbuf.h" @@ -20,39 +20,171 @@ static void restore_term_on_signal(int sig) #define INPUT_PATH "/dev/tty" #define OUTPUT_PATH "/dev/tty" +static volatile sig_atomic_t term_fd_needs_closing; static int term_fd = -1; static struct termios old_term; +static const char *background_resume_msg; +static const char *restore_error_msg; +static volatile sig_atomic_t ttou_received; + +/* async safe error function for use by signal handlers. */ +static void write_err(const char *msg) +{ + write_in_full(2, "error: ", strlen("error: ")); + write_in_full(2, msg, strlen(msg)); + write_in_full(2, "\n", 1); +} + +static void print_background_resume_msg(int signo) +{ + int saved_errno = errno; + sigset_t mask; + struct sigaction old_sa; + struct sigaction sa = { .sa_handler = SIG_DFL }; + + ttou_received = 1; + write_err(background_resume_msg); + sigaction(signo, &sa, &old_sa); + raise(signo); + sigemptyset(&mask); + sigaddset(&mask, signo); + sigprocmask(SIG_UNBLOCK, &mask, NULL); + /* Stopped here */ + sigprocmask(SIG_BLOCK, &mask, NULL); + sigaction(signo, &old_sa, NULL); + errno = saved_errno; +} + +static void restore_terminal_on_suspend(int signo) +{ + int saved_errno = errno; + int res; + struct termios t; + sigset_t mask; + struct sigaction old_sa; + struct sigaction sa = { .sa_handler = SIG_DFL }; + int can_restore = 1; + + if (tcgetattr(term_fd, &t) < 0) + can_restore = 0; + + if (tcsetattr(term_fd, TCSAFLUSH, &old_term) < 0) + write_err(restore_error_msg); + + sigaction(signo, &sa, &old_sa); + raise(signo); + sigemptyset(&mask); + sigaddset(&mask, signo); + sigprocmask(SIG_UNBLOCK, &mask, NULL); + /* Stopped here */ + sigprocmask(SIG_BLOCK, &mask, NULL); + sigaction(signo, &old_sa, NULL); + if (!can_restore) { + write_err(restore_error_msg); + goto out; + } + /* + * If we resume in the background then we receive SIGTTOU when calling + * tcsetattr() below. Set up a handler to print an error message in that + * case. + */ + sigemptyset(&mask); + sigaddset(&mask, SIGTTOU); + sa.sa_mask = old_sa.sa_mask; + sa.sa_handler = print_background_resume_msg; + sa.sa_flags = SA_RESTART; + sigaction(SIGTTOU, &sa, &old_sa); + again: + ttou_received = 0; + sigprocmask(SIG_UNBLOCK, &mask, NULL); + res = tcsetattr(term_fd, TCSAFLUSH, &t); + sigprocmask(SIG_BLOCK, &mask, NULL); + if (ttou_received) + goto again; + else if (res < 0) + write_err(restore_error_msg); + sigaction(SIGTTOU, &old_sa, NULL); + out: + errno = saved_errno; +} + +static void reset_job_signals(void) +{ + if (restore_error_msg) { + signal(SIGTTIN, SIG_DFL); + signal(SIGTTOU, SIG_DFL); + signal(SIGTSTP, SIG_DFL); + restore_error_msg = NULL; + background_resume_msg = NULL; + } +} + +static void close_term_fd(void) +{ + if (term_fd_needs_closing) + close(term_fd); + term_fd_needs_closing = 0; + term_fd = -1; +} + void restore_term(void) { if (term_fd < 0) return; tcsetattr(term_fd, TCSAFLUSH, &old_term); - close(term_fd); - term_fd = -1; + close_term_fd(); sigchain_pop_common(); + reset_job_signals(); } -int save_term(int full_duplex) +int save_term(enum save_term_flags flags) { + struct sigaction sa; + if (term_fd < 0) - term_fd = open("/dev/tty", O_RDWR); + term_fd = ((flags & SAVE_TERM_STDIN) + ? 0 + : open("/dev/tty", O_RDWR)); if (term_fd < 0) return -1; - if (tcgetattr(term_fd, &old_term) < 0) + term_fd_needs_closing = !(flags & SAVE_TERM_STDIN); + if (tcgetattr(term_fd, &old_term) < 0) { + close_term_fd(); return -1; + } sigchain_push_common(restore_term_on_signal); + /* + * If job control is disabled then the shell will have set the + * disposition of SIGTSTP to SIG_IGN. + */ + sigaction(SIGTSTP, NULL, &sa); + if (sa.sa_handler == SIG_IGN) + return 0; + + /* avoid calling gettext() from signal handler */ + background_resume_msg = _("cannot resume in the background, please use 'fg' to resume"); + restore_error_msg = _("cannot restore terminal settings"); + sa.sa_handler = restore_terminal_on_suspend; + sa.sa_flags = SA_RESTART; + sigemptyset(&sa.sa_mask); + sigaddset(&sa.sa_mask, SIGTSTP); + sigaddset(&sa.sa_mask, SIGTTIN); + sigaddset(&sa.sa_mask, SIGTTOU); + sigaction(SIGTSTP, &sa, NULL); + sigaction(SIGTTIN, &sa, NULL); + sigaction(SIGTTOU, &sa, NULL); return 0; } -static int disable_bits(tcflag_t bits) +static int disable_bits(enum save_term_flags flags, tcflag_t bits) { struct termios t; - if (save_term(0) < 0) - goto error; + if (save_term(flags) < 0) + return -1; t = old_term; @@ -65,20 +197,50 @@ static int disable_bits(tcflag_t bits) return 0; sigchain_pop_common(); -error: - close(term_fd); - term_fd = -1; + reset_job_signals(); + close_term_fd(); return -1; } -static int disable_echo(void) +static int disable_echo(enum save_term_flags flags) { - return disable_bits(ECHO); + return disable_bits(flags, ECHO); } -static int enable_non_canonical(void) +static int enable_non_canonical(enum save_term_flags flags) { - return disable_bits(ICANON | ECHO); + return disable_bits(flags, ICANON | ECHO); +} + +/* + * On macos it is not possible to use poll() with a terminal so use select + * instead. + */ +static int getchar_with_timeout(int timeout) +{ + struct timeval tv, *tvp = NULL; + fd_set readfds; + int res; + + again: + if (timeout >= 0) { + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout % 1000) * 1000; + tvp = &tv; + } + + FD_ZERO(&readfds); + FD_SET(0, &readfds); + res = select(1, &readfds, NULL, NULL, tvp); + if (!res) + return EOF; + if (res < 0) { + if (errno == EINTR) + goto again; + else + return EOF; + } + return getchar(); } #elif defined(GIT_WINDOWS_NATIVE) @@ -126,7 +288,7 @@ void restore_term(void) hconin = hconout = INVALID_HANDLE_VALUE; } -int save_term(int full_duplex) +int save_term(enum save_term_flags flags) { hconin = CreateFileA("CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, @@ -134,7 +296,7 @@ int save_term(int full_duplex) if (hconin == INVALID_HANDLE_VALUE) return -1; - if (full_duplex) { + if (flags & SAVE_TERM_DUPLEX) { hconout = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); @@ -154,7 +316,7 @@ error: return -1; } -static int disable_bits(DWORD bits) +static int disable_bits(enum save_term_flags flags, DWORD bits) { if (use_stty) { struct child_process cp = CHILD_PROCESS_INIT; @@ -191,7 +353,7 @@ static int disable_bits(DWORD bits) use_stty = 0; } - if (save_term(0) < 0) + if (save_term(flags) < 0) return -1; if (!SetConsoleMode(hconin, cmode_in & ~bits)) { @@ -204,14 +366,15 @@ static int disable_bits(DWORD bits) return 0; } -static int disable_echo(void) +static int disable_echo(enum save_term_flags flags) { - return disable_bits(ENABLE_ECHO_INPUT); + return disable_bits(flags, ENABLE_ECHO_INPUT); } -static int enable_non_canonical(void) +static int enable_non_canonical(enum save_term_flags flags) { - return disable_bits(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); + return disable_bits(flags, + ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); } /* @@ -245,6 +408,16 @@ static int mingw_getchar(void) } #define getchar mingw_getchar +static int getchar_with_timeout(int timeout) +{ + struct pollfd pfd = { .fd = 0, .events = POLLIN }; + + if (poll(&pfd, 1, timeout) < 1) + return EOF; + + return getchar(); +} + #endif #ifndef FORCE_TEXT @@ -267,7 +440,7 @@ char *git_terminal_prompt(const char *prompt, int echo) return NULL; } - if (!echo && disable_echo()) { + if (!echo && disable_echo(0)) { fclose(input_fh); fclose(output_fh); return NULL; @@ -361,7 +534,7 @@ int read_key_without_echo(struct strbuf *buf) static int warning_displayed; int ch; - if (warning_displayed || enable_non_canonical() < 0) { + if (warning_displayed || enable_non_canonical(SAVE_TERM_STDIN) < 0) { if (!warning_displayed) { warning("reading single keystrokes not supported on " "this platform; reading line instead"); @@ -395,12 +568,7 @@ int read_key_without_echo(struct strbuf *buf) * half a second when we know that the sequence is complete. */ while (!is_known_escape_sequence(buf->buf)) { - struct pollfd pfd = { .fd = 0, .events = POLLIN }; - - if (poll(&pfd, 1, 500) < 1) - break; - - ch = getchar(); + ch = getchar_with_timeout(500); if (ch == EOF) break; strbuf_addch(buf, ch); @@ -413,10 +581,10 @@ int read_key_without_echo(struct strbuf *buf) #else -int save_term(int full_duplex) +int save_term(enum save_term_flags flags) { - /* full_duplex == 1, but no support available */ - return -full_duplex; + /* no duplex support available */ + return -!!(flags & SAVE_TERM_DUPLEX); } void restore_term(void) diff --git a/compat/terminal.h b/compat/terminal.h index 0fb9fa1..79ed00c 100644 --- a/compat/terminal.h +++ b/compat/terminal.h @@ -1,6 +1,13 @@ #ifndef COMPAT_TERMINAL_H #define COMPAT_TERMINAL_H +enum save_term_flags { + /* Save input and output settings */ + SAVE_TERM_DUPLEX = 1 << 0, + /* Save stdin rather than /dev/tty (fails if stdin is not a terminal) */ + SAVE_TERM_STDIN = 1 << 1, +}; + /* * Save the terminal attributes so they can be restored later by a * call to restore_term(). Note that every successful call to @@ -8,7 +15,7 @@ * attributes have not been changed. Returns 0 on success, -1 on * failure. */ -int save_term(int full_duplex); +int save_term(enum save_term_flags flags); /* Restore the terminal attributes that were saved with save_term() */ void restore_term(void); diff --git a/compat/win32/path-utils.h b/compat/win32/path-utils.h index bba2b64..65fa3b9 100644 --- a/compat/win32/path-utils.h +++ b/compat/win32/path-utils.h @@ -6,11 +6,7 @@ int win32_has_dos_drive_prefix(const char *path); int win32_skip_dos_drive_prefix(char **path); #define skip_dos_drive_prefix win32_skip_dos_drive_prefix -static inline int win32_is_dir_sep(int c) -{ - return c == '/' || c == '\\'; -} -#define is_dir_sep win32_is_dir_sep +#define is_dir_sep is_xplatform_dir_sep static inline char *win32_find_last_dir_sep(const char *path) { char *ret = NULL; diff --git a/compat/win32/syslog.c b/compat/win32/syslog.c index 161978d..0af18d8 100644 --- a/compat/win32/syslog.c +++ b/compat/win32/syslog.c @@ -43,6 +43,8 @@ void syslog(int priority, const char *fmt, ...) va_end(ap); while ((pos = strstr(str, "%1")) != NULL) { + size_t offset = pos - str; + char *new_pos; char *oldstr = str; str = realloc(str, st_add(++str_len, 1)); if (!str) { @@ -50,8 +52,9 @@ void syslog(int priority, const char *fmt, ...) warning_errno("realloc failed"); return; } - memmove(pos + 2, pos + 1, strlen(pos)); - pos[1] = ' '; + new_pos = str + offset; + memmove(new_pos + 2, new_pos + 1, strlen(new_pos)); + new_pos[1] = ' '; } switch (priority) { |