extract method: logging for possible NPE later
[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 = (int)(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 *command;
264     if (info->Action == FILE_ACTION_ADDED || info->Action == FILE_ACTION_RENAMED_OLD_NAME) {
265         command = "CREATE";
266     }
267     else if (info->Action == FILE_ACTION_REMOVED || info->Action == FILE_ACTION_RENAMED_OLD_NAME) {
268         command = "DELETE";
269     }
270     else if (info->Action == FILE_ACTION_MODIFIED) {
271         command = "CHANGE";
272     }
273     else {
274         return;  // unknown command
275     }
276
277     char FileNameBuffer[2*_MAX_PATH + 1];
278     int converted = WideCharToMultiByte(CP_UTF8, 0, info->FileName, info->FileNameLength / sizeof(WCHAR), FileNameBuffer, 2*_MAX_PATH, NULL, NULL);
279     FileNameBuffer[converted] = '\0';
280
281     EnterCriticalSection(&csOutput);
282     puts(command);
283     printf("%s", rootPath);
284     puts(FileNameBuffer);
285     fflush(stdout);
286     LeaveCriticalSection(&csOutput);
287 }
288
289 void PrintEverythingChangedUnderRoot(char *rootPath) {
290     EnterCriticalSection(&csOutput);
291     puts("RECDIRTY");
292     puts(rootPath);
293     fflush(stdout);
294     LeaveCriticalSection(&csOutput);
295 }
296
297 DWORD WINAPI WatcherThread(void *param) {
298     WatchRootInfo *info = (WatchRootInfo *) param;
299
300     OVERLAPPED overlapped;
301     memset(&overlapped, 0, sizeof(overlapped));
302     overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
303
304     char rootPath[8];
305     sprintf_s(rootPath, 8, "%c:\\", info->driveLetter);
306     HANDLE hRootDir = CreateFileA(rootPath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
307             NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL);
308
309     int buffer_size = 1024 * 1024;
310     char *buffer = new char[buffer_size];
311
312     HANDLE handles[2];
313     handles[0] = info->hStopEvent;
314     handles[1] = overlapped.hEvent;
315     while (true) {
316         int rcDir = ReadDirectoryChangesW(hRootDir, buffer, buffer_size, TRUE,
317                 FILE_NOTIFY_CHANGE_FILE_NAME |
318                         FILE_NOTIFY_CHANGE_DIR_NAME |
319                         FILE_NOTIFY_CHANGE_ATTRIBUTES |
320                         FILE_NOTIFY_CHANGE_SIZE |
321                         FILE_NOTIFY_CHANGE_LAST_WRITE,
322                 NULL,
323                 &overlapped,
324                 NULL);
325         if (rcDir == 0) {
326             info->bFailed = true;
327             break;
328         }
329
330         int rc = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
331         if (rc == WAIT_OBJECT_0) {
332             break;
333         }
334         if (rc == WAIT_OBJECT_0 + 1) {
335             DWORD dwBytesReturned;
336             if (!GetOverlappedResult(hRootDir, &overlapped, &dwBytesReturned, FALSE)) {
337                 info->bFailed = true;
338                 break;
339             }
340
341             if (dwBytesReturned == 0) {
342                 // don't send dirty too much, everything is changed anyway
343                 if (WaitForSingleObject(info->hStopEvent, 500) == WAIT_OBJECT_0)
344                     break;
345
346                 // Got a buffer overflow => current changes lost => send RECDIRTY on root
347                 PrintEverythingChangedUnderRoot(rootPath);
348             } else {
349                 FILE_NOTIFY_INFORMATION *info = (FILE_NOTIFY_INFORMATION *) buffer;
350                 while (true) {
351                     PrintChangeInfo(rootPath, info);
352                     if (!info->NextEntryOffset)
353                         break;
354                     info = (FILE_NOTIFY_INFORMATION *)((char *) info + info->NextEntryOffset);
355                 }
356             }
357         }
358     }
359     CloseHandle(overlapped.hEvent);
360     CloseHandle(hRootDir);
361     delete[] buffer;
362     return 0;
363 }
364
365 // -- Roots update ------------------------------------------------------------
366
367 void MarkAllRootsUnused() {
368     for (int i = 0; i < ROOT_COUNT; i++) {
369         watchRootInfos[i].bUsed = false;
370     }
371 }
372
373 void StartRoot(WatchRootInfo *info) {
374     info->hStopEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
375     info->hThread = CreateThread(NULL, 0, &WatcherThread, info, 0, NULL);
376     info->bInitialized = true;
377 }
378
379 void StopRoot(WatchRootInfo *info) {
380     SetEvent(info->hStopEvent);
381     WaitForSingleObject(info->hThread, INFINITE);
382     CloseHandle(info->hThread);
383     CloseHandle(info->hStopEvent);
384     info->bInitialized = false;
385 }
386
387 void UpdateRoots(bool report) {
388     char infoBuffer[256];
389     strcpy_s(infoBuffer, "UNWATCHEABLE\n");
390     for (int i = 0; i < ROOT_COUNT; i++) {
391         if (watchRootInfos[i].bInitialized && (!watchRootInfos[i].bUsed || watchRootInfos[i].bFailed)) {
392             StopRoot(&watchRootInfos[i]);
393             watchRootInfos[i].bFailed = false;
394         }
395         if (watchRootInfos[i].bUsed) {
396             char rootPath[8];
397             sprintf_s(rootPath, 8, "%c:\\", watchRootInfos[i].driveLetter);
398             if (!IsWatchable(rootPath)) {
399                 strcat_s(infoBuffer, rootPath);
400                 strcat_s(infoBuffer, "\n");
401                 continue;
402             }
403             if (!watchRootInfos[i].bInitialized) {
404                 StartRoot(&watchRootInfos[i]);
405             }
406         }
407     }
408
409     if (!report) {
410         return;
411     }
412
413     EnterCriticalSection(&csOutput);
414     fprintf(stdout, "%s", infoBuffer);
415     puts("#\nREMAP");
416     PrintRemapForSubstDrives();
417     bool printedMountPoints = true;
418     for (int i = 0; i < ROOT_COUNT; i++) {
419         if (watchRootInfos[i].bUsed) {
420             char rootPath[8];
421             sprintf_s(rootPath, 8, "%c:\\", watchRootInfos[i].driveLetter);
422             if (!PrintMountPoints(rootPath))
423                 printedMountPoints = false;
424         }
425     }
426     if (!printedMountPoints) {
427         PrintWatchRootReparsePoints();
428     }
429     puts("#");
430     fflush(stdout);
431     LeaveCriticalSection(&csOutput);
432 }
433
434 void AddWatchRoot(const char *path) {
435     WatchRoot *watchRoot = (WatchRoot *) malloc(sizeof(WatchRoot));
436     watchRoot->next = NULL;
437     watchRoot->path = _strdup(path);
438     watchRoot->next = firstWatchRoot;
439     firstWatchRoot = watchRoot;
440 }
441
442 void FreeWatchRootsList() {
443     WatchRoot *pWatchRoot = firstWatchRoot;
444     WatchRoot *pNext;
445     while (pWatchRoot) {
446         pNext = pWatchRoot->next;
447         free(pWatchRoot->path);
448         free(pWatchRoot);
449         pWatchRoot = pNext;
450     }
451     firstWatchRoot = NULL;
452 }
453
454 // -- Main - filewatcher protocol ---------------------------------------------
455
456 int _tmain(int argc, _TCHAR *argv[]) {
457     InitializeCriticalSection(&csOutput);
458
459     for (int i = 0; i < 26; i++) {
460         watchRootInfos[i].driveLetter = 'A' + i;
461         watchRootInfos[i].bInitialized = false;
462         watchRootInfos[i].bUsed = false;
463     }
464
465     char buffer[8192];
466     while (true) {
467         if (!gets_s(buffer, sizeof(buffer) - 1))
468             break;
469
470         if (!strcmp(buffer, "ROOTS")) {
471             MarkAllRootsUnused();
472             FreeWatchRootsList();
473             bool failed = false;
474             while (true) {
475                 if (!gets_s(buffer, sizeof(buffer) - 1)) {
476                     failed = true;
477                     break;
478                 }
479                 if (buffer[0] == '#')
480                     break;
481                 int driveLetterPos = 0;
482                 char *pDriveLetter = buffer;
483                 if (*pDriveLetter == '|')
484                     pDriveLetter++;
485
486                 AddWatchRoot(pDriveLetter);
487
488                 _strupr_s(buffer, sizeof(buffer) - 1);
489                 char driveLetter = *pDriveLetter;
490                 if (driveLetter >= 'A' && driveLetter <= 'Z') {
491                     watchRootInfos[driveLetter - 'A'].bUsed = true;
492                 }
493             }
494             if (failed)
495                 break;
496
497             UpdateRoots(true);
498         }
499
500         if (!strcmp(buffer, "EXIT"))
501             break;
502     }
503
504     MarkAllRootsUnused();
505     UpdateRoots(false);
506
507     DeleteCriticalSection(&csOutput);
508 }