/* Small external watch supervisor. Each build runs in a fresh child process. */ #define WIN32_LEAN_AND_MEAN #include #include #include #include #include #include struct WatchState { HANDLE directory; HANDLE changed; volatile LONG generation; }; /* Exclude generated object directories, caches and build logs. */ static int IgnoreChange(const WCHAR *name, DWORD length) { WCHAR component[260]; DWORD i = 0; while (i < length) { DWORD j = 0; while (i < length && (name[i] == L'\\' || name[i] == L'/')) ++i; while (i < length && name[i] != L'\\' && name[i] != L'/') { if (j + 1 < 260) component[j++] = towlower(name[i]); ++i; } component[j] = 0; if (!wcscmp(component, L"obj") || !wcscmp(component, L"objd") || !wcscmp(component, L"out") || !wcscmp(component, L".git") || !wcscmp(component, L".vs")) return 1; if (i >= length) { if (!wcscmp(component, L"build.dat") || !wcscmp(component, L"build.log") || !wcscmp(component, L"build.err") || !wcscmp(component, L"build.wrn") || !wcscmp(component, L"build.inc")) return 1; } } return 0; } static DWORD WINAPI WatchThread(void *param) { struct WatchState *state = (struct WatchState *)param; BYTE buffer[32768]; DWORD bytes; for (;;) { if (!ReadDirectoryChangesW(state->directory, buffer, sizeof(buffer), TRUE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION, &bytes, NULL, NULL)) return 1; if (bytes) { BYTE *cursor = buffer; for (;;) { FILE_NOTIFY_INFORMATION *info = (FILE_NOTIFY_INFORMATION *)cursor; if (!IgnoreChange(info->FileName, info->FileNameLength / sizeof(WCHAR))) { InterlockedIncrement(&state->generation); SetEvent(state->changed); break; } if (!info->NextEntryOffset) break; cursor += info->NextEntryOffset; } } } } /* Quote an individual argument according to the Win32 CRT argv rules. */ static void AddQuoted(WCHAR *dst, const WCHAR *src, size_t *position) { size_t slash = 0; dst[(*position)++] = L'"'; while (*src) { if (*src == L'\\') { ++slash; ++src; continue; } if (*src == L'"') { while (slash--) dst[(*position)++] = L'\\'; dst[(*position)++] = L'\\'; dst[(*position)++] = L'"'; slash = 0; ++src; continue; } while (slash) { dst[(*position)++] = L'\\'; --slash; } dst[(*position)++] = *src++; } while (slash--) { dst[(*position)++] = L'\\'; dst[(*position)++] = L'\\'; } dst[(*position)++] = L'"'; dst[*position] = 0; } static WCHAR *MakeChildCommand(int argc, WCHAR **argv) { size_t capacity = 4, pos = 0; WCHAR *command; int i; for (i = 0; i < argc; ++i) capacity += wcslen(argv[i]) * 2 + 5; command = (WCHAR *)calloc(capacity, sizeof(WCHAR)); if (!command) return NULL; for (i = 0; i < argc; ++i) { if (!argv[i] || !argv[i][0]) continue; if (pos) command[pos++] = L' '; AddQuoted(command, argv[i], &pos); } return command; } static int RunChild(WCHAR *command) { PROCESS_INFORMATION pi = {0}; STARTUPINFOW si = {0}; DWORD status = 1; si.cb = sizeof(si); if (!CreateProcessW(NULL, command, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { fwprintf(stderr, L"BUILD: error: cannot launch build (%lu)\n", GetLastError()); return 1; } WaitForSingleObject(pi.hProcess, INFINITE); GetExitCodeProcess(pi.hProcess, &status); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return (int)status; } /* Return -1 for a normal build; otherwise supervise -w until Ctrl+C. */ int TryWatchMode(void) { int argc, i, watch = 0; WCHAR **argv = CommandLineToArgvW(GetCommandLineW(), &argc); WCHAR *command; struct WatchState state = {0}; HANDLE worker; DWORD threadId; if (!argv) return -1; for (i = 1; i < argc; ++i) { WCHAR *arg = argv[i]; size_t j, k; if (arg[0] != L'-' && arg[0] != L'/') continue; if (!_wcsicmp(arg + 1, L"why")) continue; /* Keep whole-word and target switches unchanged. */ if (wcslen(arg) > 1 && wcschr(L"aAcCnN", arg[1]) && (wcslen(arg) > 3)) continue; for (j = 1, k = 1; arg[j]; ++j) { if (arg[j] == L'w' || arg[j] == L'W') { watch = 1; } else { arg[k++] = arg[j]; } } arg[k] = 0; if (k == 1) argv[i][0] = 0; } if (!watch) { LocalFree(argv); return -1; } command = MakeChildCommand(argc, argv); LocalFree(argv); if (!command) return 1; state.changed = CreateEventW(NULL, FALSE, FALSE, NULL); state.directory = CreateFileW(L".", FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); if (!state.changed || state.directory == INVALID_HANDLE_VALUE) { fprintf(stderr, "BUILD: error: unable to watch project directory\n"); free(command); return 1; } worker = CreateThread(NULL, 0, WatchThread, &state, 0, &threadId); if (!worker) { fprintf(stderr, "BUILD: error: unable to start watcher\n"); free(command); return 1; } for (;;) { LONG before = InterlockedCompareExchange(&state.generation, 0, 0); RunChild(command); if (before == InterlockedCompareExchange(&state.generation, 0, 0)) { printf("BUILD: Watching...\n"); fflush(stdout); { HANDLE signals[2] = { state.changed, worker }; if (WaitForMultipleObjects(2, signals, FALSE, INFINITE) != WAIT_OBJECT_0) { fprintf(stderr, "BUILD: error: watcher stopped\n"); free(command); return 1; } } } Sleep(200); /* Drain coalesced notifications before next build. */ ResetEvent(state.changed); } }