192 lines
6.4 KiB
C
192 lines
6.4 KiB
C
/* Small external watch supervisor. Each build runs in a fresh child process. */
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
#include <shellapi.h>
|
|
#include <wchar.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <wctype.h>
|
|
|
|
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);
|
|
}
|
|
}
|