platform: dropped unneeded reporting on exit by Windows file watcher
[idea/community.git] / native / fileWatcher / fileWatcher3.cpp
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 #include <process.h>
18 #include <stdio.h>
19 #include <tchar.h>
20 #include <windows.h>
21
22 struct WatchRootInfo {
23     char driveLetter;
24     HANDLE hThread;
25     HANDLE hStopEvent;
26     bool bInitialized;
27     bool bUsed;
28     bool bFailed;
29 };
30
31 struct WatchRoot {
32     char *path;
33     WatchRoot *next;
34 };
35
36 const int ROOT_COUNT = 26;
37
38 WatchRootInfo watchRootInfos[ROOT_COUNT];
39
40 WatchRoot *firstWatchRoot = NULL;
41
42 CRITICAL_SECTION csOutput;
43
44 void NormalizeSlashes(char *path, char slash) {
45     for (char *p = path; *p; p++)
46         if (*p == '\\' || *p == '/')
47             *p = slash;
48 }
49
50 // -- Watchable root checks ---------------------------------------------------
51
52 bool IsNetworkDrive(const char *name) {
53     const int BUF_SIZE = 1024;
54     char buffer[BUF_SIZE];
55     UNIVERSAL_NAME_INFO *uni = (UNIVERSAL_NAME_INFO *) buffer;
56     DWORD size = BUF_SIZE;
57
58     DWORD result = WNetGetUniversalNameA(
59             name,  // path for network resource
60             UNIVERSAL_NAME_INFO_LEVEL, // level of information
61             buffer, // name buffer
62             &size // size of buffer
63     );
64
65     return result == NO_ERROR;
66 }
67
68 bool IsUnwatchableFS(const char *path) {
69     char volumeName[MAX_PATH];
70     char fsName[MAX_PATH];
71     DWORD fsFlags;
72     DWORD maxComponentLength;
73     SetErrorMode(SEM_FAILCRITICALERRORS);
74     if (!GetVolumeInformationA(path, volumeName, MAX_PATH - 1, NULL, &maxComponentLength, &fsFlags, fsName, MAX_PATH - 1))
75         return false;
76     if (strcmp(fsName, "NTFS") && strcmp(fsName, "FAT") && strcmp(fsName, "FAT32") && _stricmp(fsName, "exFAT") && _stricmp(fsName, "reFS"))
77         return true;
78
79     if (!strcmp(fsName, "NTFS") && maxComponentLength != 255 && !(fsFlags & FILE_SUPPORTS_REPARSE_POINTS)) {
80         // SAMBA reports itself as NTFS
81         return true;
82     }
83
84     return false;
85 }
86
87 bool IsWatchable(const char *path) {
88     if (IsNetworkDrive(path))
89         return false;
90     if (IsUnwatchableFS(path))
91         return false;
92     return true;
93 }
94
95 // -- Substed drive checks ----------------------------------------------------
96
97 void PrintRemapForSubstDrive(char driveLetter) {
98     const int BUF_SIZE = 1024;
99     char targetPath[BUF_SIZE];
100
101     char rootPath[8];
102     sprintf_s(rootPath, 8, "%c:", driveLetter);
103
104     DWORD result = QueryDosDeviceA(rootPath, targetPath, BUF_SIZE);
105     if (result == 0) {
106         return;
107     }
108     else {
109         if (targetPath[0] == '\\' && targetPath[1] == '?' && targetPath[2] == '?' && targetPath[3] == '\\') {
110             // example path: \??\C:\jetbrains\idea
111             NormalizeSlashes(targetPath, '/');
112             printf("%c:\n%s\n", driveLetter, targetPath + 4);
113         }
114     }
115 }
116
117 void PrintRemapForSubstDrives() {
118     for (int i = 0; i < ROOT_COUNT; i++) {
119         if (watchRootInfos[i].bUsed) {
120             PrintRemapForSubstDrive(watchRootInfos[i].driveLetter);
121         }
122     }
123 }
124
125 // -- Mount point enumeration -------------------------------------------------
126
127 const int BUFSIZE = 1024;
128
129 void PrintDirectoryReparsePoint(const char *path) {
130     int size = strlen(path) + 2;
131     char *directory = (char *) malloc(size);
132     strcpy_s(directory, size, path);
133     NormalizeSlashes(directory, '\\');
134     if (directory[strlen(directory) - 1] != '\\')
135         strcat_s(directory, size, "\\");
136
137     char volumeName[_MAX_PATH];
138     int rc = GetVolumeNameForVolumeMountPointA(directory, volumeName, sizeof(volumeName));
139     if (rc) {
140         char volumePathNames[_MAX_PATH];
141         DWORD returnLength;
142         rc = GetVolumePathNamesForVolumeNameA(volumeName, volumePathNames, sizeof(volumePathNames), &returnLength);
143         if (rc) {
144             char *p = volumePathNames;
145             while (*p) {
146                 if (_stricmp(p, directory))   // if it's not the path we've already found
147                 {
148                     NormalizeSlashes(directory, '/');
149                     NormalizeSlashes(p, '/');
150                     puts(directory);
151                     puts(p);
152                 }
153                 p += strlen(p) + 1;
154             }
155         }
156     }
157     free(directory);
158 }
159
160 bool PrintMountPointsForVolume(HANDLE hVol, const char *volumePath, char *Buf) {
161     HANDLE hPt;                  // handle for mount point scan
162     char Path[BUFSIZE];          // string buffer for mount points
163     DWORD dwSysFlags;            // flags that describe the file system
164     char FileSysNameBuf[BUFSIZE];
165
166     // Is this volume NTFS?
167     GetVolumeInformationA(Buf, NULL, 0, NULL, NULL, &dwSysFlags, FileSysNameBuf, BUFSIZE);
168
169     // Detect support for reparse points, and therefore for volume
170     // mount points, which are implemented using reparse points.
171
172     if (!(dwSysFlags & FILE_SUPPORTS_REPARSE_POINTS)) {
173         return true;
174     }
175
176     // Start processing mount points on this volume.
177     hPt = FindFirstVolumeMountPointA(
178             Buf, // root path of volume to be scanned
179             Path, // pointer to output string
180             BUFSIZE // size of output buffer
181     );
182
183     // Shall we error out?
184     if (hPt == INVALID_HANDLE_VALUE) {
185         return GetLastError() != ERROR_ACCESS_DENIED;
186     }
187
188     // Process the volume mount point.
189     char *buf = new char[MAX_PATH];
190     do {
191         strcpy_s(buf, MAX_PATH, volumePath);
192         strcat_s(buf, MAX_PATH, Path);
193         PrintDirectoryReparsePoint(buf);
194     } while (FindNextVolumeMountPointA(hPt, Path, BUFSIZE));
195
196     FindVolumeMountPointClose(hPt);
197     return true;
198 }
199
200 bool PrintMountPoints(const char *path) {
201     char volumeUniqueName[128];
202     BOOL res = GetVolumeNameForVolumeMountPointA(path, volumeUniqueName, 128);
203     if (!res) {
204         return false;
205     }
206
207     char buf[BUFSIZE];            // buffer for unique volume identifiers
208     HANDLE hVol;                  // handle for the volume scan
209
210     // Open a scan for volumes.
211     hVol = FindFirstVolumeA(buf, BUFSIZE);
212
213     // Shall we error out?
214     if (hVol == INVALID_HANDLE_VALUE) {
215         return false;
216     }
217
218     bool success = true;
219     do {
220         if (!strcmp(buf, volumeUniqueName)) {
221             success = PrintMountPointsForVolume(hVol, path, buf);
222             if (!success) break;
223         }
224     } while (FindNextVolumeA(hVol, buf, BUFSIZE));
225
226     FindVolumeClose(hVol);
227     return success;
228 }
229
230 // -- Searching for mount points in watch roots (fallback) --------------------
231
232 void PrintDirectoryReparsePoints(const char *path) {
233     char *const buf = _strdup(path);
234     while (strchr(buf, '/')) {
235         DWORD attributes = GetFileAttributesA(buf);
236         if (attributes == INVALID_FILE_ATTRIBUTES)
237             break;
238         if (attributes & FILE_ATTRIBUTE_REPARSE_POINT) {
239             PrintDirectoryReparsePoint(buf);
240         }
241         char *pSlash = strrchr(buf, '/');
242         if (pSlash) {
243             *pSlash = '\0';
244         }
245     }
246     free(buf);
247 }
248
249 // This is called if we got an ERROR_ACCESS_DENIED when trying to enumerate all mount points for volume.
250 // In this case, we walk the directory tree up from each watch root, and look at each parent directory
251 // to check whether it's a reparse point.
252 void PrintWatchRootReparsePoints() {
253     WatchRoot *pWatchRoot = firstWatchRoot;
254     while (pWatchRoot) {
255         PrintDirectoryReparsePoints(pWatchRoot->path);
256         pWatchRoot = pWatchRoot->next;
257     }
258 }
259
260 // -- Watcher thread ----------------------------------------------------------
261
262 void PrintChangeInfo(char *rootPath, FILE_NOTIFY_INFORMATION *info) {
263     char FileNameBuffer[_MAX_PATH];
264     int converted = WideCharToMultiByte(CP_ACP, 0, info->FileName, info->FileNameLength / sizeof(WCHAR), FileNameBuffer, _MAX_PATH - 1, NULL, NULL);
265     FileNameBuffer[converted] = '\0';
266     char *command;
267     if (info->Action == FILE_ACTION_ADDED || info->Action == FILE_ACTION_RENAMED_OLD_NAME) {
268         command = "CREATE";
269     }
270     else if (info->Action == FILE_ACTION_REMOVED || info->Action == FILE_ACTION_RENAMED_OLD_NAME) {
271         command = "DELETE";
272     }
273     else if (info->Action == FILE_ACTION_MODIFIED) {
274         command = "CHANGE";
275     }
276     else {
277         return;  // unknown command
278     }
279
280     EnterCriticalSection(&csOutput);
281     puts(command);
282     printf("%s", rootPath);
283     puts(FileNameBuffer);
284     fflush(stdout);
285     LeaveCriticalSection(&csOutput);
286 }
287
288 void PrintEverythingChangedUnderRoot(char *rootPath) {
289     EnterCriticalSection(&csOutput);
290     puts("RECDIRTY");
291     puts(rootPath);
292     fflush(stdout);
293     LeaveCriticalSection(&csOutput);
294 }
295
296 DWORD WINAPI WatcherThread(void *param) {
297     WatchRootInfo *info = (WatchRootInfo *) param;
298
299     OVERLAPPED overlapped;
300     memset(&overlapped, 0, sizeof(overlapped));
301     overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
302
303     char rootPath[8];
304     sprintf_s(rootPath, 8, "%c:\\", info->driveLetter);
305     HANDLE hRootDir = CreateFileA(rootPath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
306             NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL);
307
308     int buffer_size = 10240;
309     char *buffer = new char[buffer_size];
310
311     HANDLE handles[2];
312     handles[0] = info->hStopEvent;
313     handles[1] = overlapped.hEvent;
314     while (true) {
315         int rcDir = ReadDirectoryChangesW(hRootDir, buffer, buffer_size, TRUE,
316                 FILE_NOTIFY_CHANGE_FILE_NAME |
317                         FILE_NOTIFY_CHANGE_DIR_NAME |
318                         FILE_NOTIFY_CHANGE_ATTRIBUTES |
319                         FILE_NOTIFY_CHANGE_SIZE |
320                         FILE_NOTIFY_CHANGE_LAST_WRITE,
321                 NULL,
322                 &overlapped,
323                 NULL);
324         if (rcDir == 0) {
325             info->bFailed = true;
326             break;
327         }
328
329         int rc = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
330         if (rc == WAIT_OBJECT_0) {
331             break;
332         }
333         if (rc == WAIT_OBJECT_0 + 1) {
334             DWORD dwBytesReturned;
335             if (!GetOverlappedResult(hRootDir, &overlapped, &dwBytesReturned, FALSE)) {
336                 info->bFailed = true;
337                 break;
338             }
339
340             if (dwBytesReturned == 0) {
341                 // don't send dirty too much, everything is changed anyway
342                 if (WaitForSingleObject(info->hStopEvent, 500) == WAIT_OBJECT_0)
343                     break;
344
345                 // Got a buffer overflow => current changes lost => send RECDIRTY on root
346                 PrintEverythingChangedUnderRoot(rootPath);
347             } else {
348                 FILE_NOTIFY_INFORMATION *info = (FILE_NOTIFY_INFORMATION *) buffer;
349                 while (true) {
350                     PrintChangeInfo(rootPath, info);
351                     if (!info->NextEntryOffset)
352                         break;
353                     info = (FILE_NOTIFY_INFORMATION *)((char *) info + info->NextEntryOffset);
354                 }
355             }
356         }
357     }
358     CloseHandle(overlapped.hEvent);
359     CloseHandle(hRootDir);
360     delete[] buffer;
361     return 0;
362 }
363
364 // -- Roots update ------------------------------------------------------------
365
366 void MarkAllRootsUnused() {
367     for (int i = 0; i < ROOT_COUNT; i++) {
368         watchRootInfos[i].bUsed = false;
369     }
370 }
371
372 void StartRoot(WatchRootInfo *info) {
373     info->hStopEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
374     info->hThread = CreateThread(NULL, 0, &WatcherThread, info, 0, NULL);
375     info->bInitialized = true;
376 }
377
378 void StopRoot(WatchRootInfo *info) {
379     SetEvent(info->hStopEvent);
380     WaitForSingleObject(info->hThread, INFINITE);
381     CloseHandle(info->hThread);
382     CloseHandle(info->hStopEvent);
383     info->bInitialized = false;
384 }
385
386 void UpdateRoots(bool report) {
387     char infoBuffer[256];
388     strcpy_s(infoBuffer, "UNWATCHEABLE\n");
389     for (int i = 0; i < ROOT_COUNT; i++) {
390         if (watchRootInfos[i].bInitialized && (!watchRootInfos[i].bUsed || watchRootInfos[i].bFailed)) {
391             StopRoot(&watchRootInfos[i]);
392             watchRootInfos[i].bFailed = false;
393         }
394         if (watchRootInfos[i].bUsed) {
395             char rootPath[8];
396             sprintf_s(rootPath, 8, "%c:\\", watchRootInfos[i].driveLetter);
397             if (!IsWatchable(rootPath)) {
398                 strcat_s(infoBuffer, rootPath);
399                 strcat_s(infoBuffer, "\n");
400                 continue;
401             }
402             if (!watchRootInfos[i].bInitialized) {
403                 StartRoot(&watchRootInfos[i]);
404             }
405         }
406     }
407
408     if (!report) {
409         return;
410     }
411
412     EnterCriticalSection(&csOutput);
413     fprintf(stdout, "%s", infoBuffer);
414     puts("#\nREMAP");
415     PrintRemapForSubstDrives();
416     bool printedMountPoints = true;
417     for (int i = 0; i < ROOT_COUNT; i++) {
418         if (watchRootInfos[i].bUsed) {
419             char rootPath[8];
420             sprintf_s(rootPath, 8, "%c:\\", watchRootInfos[i].driveLetter);
421             if (!PrintMountPoints(rootPath))
422                 printedMountPoints = false;
423         }
424     }
425     if (!printedMountPoints) {
426         PrintWatchRootReparsePoints();
427     }
428     puts("#");
429     fflush(stdout);
430     LeaveCriticalSection(&csOutput);
431 }
432
433 void AddWatchRoot(const char *path) {
434     WatchRoot *watchRoot = (WatchRoot *) malloc(sizeof(WatchRoot));
435     watchRoot->next = NULL;
436     watchRoot->path = _strdup(path);
437     watchRoot->next = firstWatchRoot;
438     firstWatchRoot = watchRoot;
439 }
440
441 void FreeWatchRootsList() {
442     WatchRoot *pWatchRoot = firstWatchRoot;
443     WatchRoot *pNext;
444     while (pWatchRoot) {
445         pNext = pWatchRoot->next;
446         free(pWatchRoot->path);
447         free(pWatchRoot);
448         pWatchRoot = pNext;
449     }
450     firstWatchRoot = NULL;
451 }
452
453 // -- Main - filewatcher protocol ---------------------------------------------
454
455 int _tmain(int argc, _TCHAR *argv[]) {
456     InitializeCriticalSection(&csOutput);
457
458     for (int i = 0; i < 26; i++) {
459         watchRootInfos[i].driveLetter = 'A' + i;
460         watchRootInfos[i].bInitialized = false;
461         watchRootInfos[i].bUsed = false;
462     }
463
464     char buffer[8192];
465     while (true) {
466         if (!gets_s(buffer, sizeof(buffer) - 1))
467             break;
468
469         if (!strcmp(buffer, "ROOTS")) {
470             MarkAllRootsUnused();
471             FreeWatchRootsList();
472             bool failed = false;
473             while (true) {
474                 if (!gets_s(buffer, sizeof(buffer) - 1)) {
475                     failed = true;
476                     break;
477                 }
478                 if (buffer[0] == '#')
479                     break;
480                 int driveLetterPos = 0;
481                 char *pDriveLetter = buffer;
482                 if (*pDriveLetter == '|')
483                     pDriveLetter++;
484
485                 AddWatchRoot(pDriveLetter);
486
487                 _strupr_s(buffer, sizeof(buffer) - 1);
488                 char driveLetter = *pDriveLetter;
489                 if (driveLetter >= 'A' && driveLetter <= 'Z') {
490                     watchRootInfos[driveLetter - 'A'].bUsed = true;
491                 }
492             }
493             if (failed)
494                 break;
495
496             UpdateRoots(true);
497         }
498
499         if (!strcmp(buffer, "EXIT"))
500             break;
501     }
502
503     MarkAllRootsUnused();
504     UpdateRoots(false);
505
506     DeleteCriticalSection(&csOutput);
507 }