xref: /reactos/base/system/autochk/autochk.c (revision 9be382ec)
1 /*
2  * PROJECT:     ReactOS AutoChk
3  * LICENSE:     GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
4  * PURPOSE:     FileSystem checker in Native mode.
5  * COPYRIGHT:   Copyright 2002-2018 Eric Kohl
6  *              Copyright 2006-2018 Aleksey Bragin
7  *              Copyright 2006-2018 Hervé Poussineau
8  *              Copyright 2008-2018 Pierre Schweitzer
9  *              Copyright 2018 Hermes Belusca-Maito
10  */
11 
12 /* INCLUDES *****************************************************************/
13 
14 #include <stdio.h>
15 
16 #define WIN32_NO_STATUS
17 #include <windef.h>
18 #include <winbase.h>
19 #include <ntddkbd.h>
20 
21 #define NTOS_MODE_USER
22 #include <ndk/exfuncs.h>
23 #include <ndk/iofuncs.h>
24 #include <ndk/obfuncs.h>
25 #include <ndk/psfuncs.h>
26 #include <ndk/rtlfuncs.h>
27 #include <fmifs/fmifs.h>
28 
29 #include <fslib/vfatlib.h>
30 #include <fslib/vfatxlib.h>
31 #include <fslib/ntfslib.h>
32 #include <fslib/btrfslib.h>
33 #include <fslib/ext2lib.h>
34 #include <fslib/cdfslib.h>
35 
36 #define NDEBUG
37 #include <debug.h>
38 
39 /* DEFINES ******************************************************************/
40 
41 typedef struct _FILESYSTEM_CHKDSK
42 {
43     WCHAR Name[10];
44     PULIB_CHKDSK ChkdskFunc;
45 } FILESYSTEM_CHKDSK, *PFILESYSTEM_CHKDSK;
46 
47 FILESYSTEM_CHKDSK FileSystems[] =
48 {
49     { L"FAT", VfatChkdsk },
50     { L"FAT32", VfatChkdsk },
51     { L"FATX" , VfatxChkdsk },
52     { L"NTFS", NtfsChkdsk },
53     { L"BTRFS", BtrfsChkdsk },
54     { L"EXT2", Ext2Chkdsk },
55     { L"EXT3", Ext2Chkdsk },
56     { L"EXT4", Ext2Chkdsk },
57     { L"CDFS", CdfsChkdsk },
58 };
59 
60 HANDLE KeyboardHandle = NULL;
61 
62 /* FUNCTIONS ****************************************************************/
63 
64 //
65 // FMIFS function
66 //
67 
68 static INT
69 PrintString(char* fmt,...)
70 {
71     INT Len;
72     char buffer[512];
73     va_list ap;
74     UNICODE_STRING UnicodeString;
75     ANSI_STRING AnsiString;
76 
77     va_start(ap, fmt);
78     Len = vsprintf(buffer, fmt, ap);
79     va_end(ap);
80 
81     RtlInitAnsiString(&AnsiString, buffer);
82     RtlAnsiStringToUnicodeString(&UnicodeString,
83                                  &AnsiString,
84                                  TRUE);
85     NtDisplayString(&UnicodeString);
86     RtlFreeUnicodeString(&UnicodeString);
87 
88     return Len;
89 }
90 
91 static VOID
92 EraseLine(
93     IN INT LineLength)
94 {
95     INT Len;
96     UNICODE_STRING UnicodeString;
97     WCHAR buffer[512];
98 
99     if (LineLength <= 0)
100         return;
101 
102     /* Go to the beginning of the line */
103     RtlInitUnicodeString(&UnicodeString, L"\r");
104     NtDisplayString(&UnicodeString);
105 
106     /* Fill the buffer chunk with spaces */
107     Len = min(LineLength, ARRAYSIZE(buffer));
108     while (Len > 0) buffer[--Len] = L' ';
109 
110     RtlInitEmptyUnicodeString(&UnicodeString, buffer, sizeof(buffer));
111     while (LineLength > 0)
112     {
113         /* Display the buffer */
114         Len = min(LineLength, ARRAYSIZE(buffer));
115         LineLength -= Len;
116         UnicodeString.Length = Len * sizeof(WCHAR);
117         NtDisplayString(&UnicodeString);
118     }
119 
120     /* Go to the beginning of the line */
121     RtlInitUnicodeString(&UnicodeString, L"\r");
122     NtDisplayString(&UnicodeString);
123 }
124 
125 static NTSTATUS
126 OpenKeyboard(
127     OUT PHANDLE KeyboardHandle)
128 {
129     OBJECT_ATTRIBUTES ObjectAttributes;
130     IO_STATUS_BLOCK IoStatusBlock;
131     UNICODE_STRING KeyboardName = RTL_CONSTANT_STRING(L"\\Device\\KeyboardClass0");
132 
133     /* Just open the class driver */
134     InitializeObjectAttributes(&ObjectAttributes,
135                                &KeyboardName,
136                                0,
137                                NULL,
138                                NULL);
139     return NtOpenFile(KeyboardHandle,
140                       FILE_ALL_ACCESS,
141                       &ObjectAttributes,
142                       &IoStatusBlock,
143                       FILE_OPEN,
144                       0);
145 }
146 
147 static NTSTATUS
148 WaitForKeyboard(
149     IN HANDLE KeyboardHandle,
150     IN LONG TimeOut)
151 {
152     NTSTATUS Status;
153     IO_STATUS_BLOCK IoStatusBlock;
154     LARGE_INTEGER Offset, Timeout;
155     KEYBOARD_INPUT_DATA InputData;
156     INT Len = 0;
157 
158     /* Skip the wait if there is no timeout */
159     if (TimeOut <= 0)
160         return STATUS_TIMEOUT;
161 
162     /* Attempt to read a down key-press from the keyboard */
163     do
164     {
165         Offset.QuadPart = 0;
166         Status = NtReadFile(KeyboardHandle,
167                             NULL,
168                             NULL,
169                             NULL,
170                             &IoStatusBlock,
171                             &InputData,
172                             sizeof(KEYBOARD_INPUT_DATA),
173                             &Offset,
174                             NULL);
175         if (!NT_SUCCESS(Status))
176         {
177             /* Something failed, bail out */
178             return Status;
179         }
180         if ((Status != STATUS_PENDING) && !(InputData.Flags & KEY_BREAK))
181         {
182             /* We have a down key-press, return */
183             return Status;
184         }
185     } while (Status != STATUS_PENDING);
186 
187     /* Perform the countdown of TimeOut seconds */
188     Status = STATUS_TIMEOUT;
189     while (TimeOut > 0)
190     {
191         /*
192          * Display the countdown string.
193          * Note that we only do a carriage return to go back to the
194          * beginning of the line to possibly erase it. Note also that
195          * we display a trailing space to erase any last character
196          * when the string length decreases.
197          */
198         Len = PrintString("Press any key within %d second(s) to cancel and resume startup. \r", TimeOut);
199 
200         /* Decrease the countdown */
201         --TimeOut;
202 
203         /* Wait one second for a key press */
204         Timeout.QuadPart = -10000000;
205         Status = NtWaitForSingleObject(KeyboardHandle, FALSE, &Timeout);
206         if (Status != STATUS_TIMEOUT)
207             break;
208     }
209 
210     /* Erase the countdown string */
211     EraseLine(Len);
212 
213     if (Status == STATUS_TIMEOUT)
214     {
215         /* The user did not press any key, cancel the read */
216         NtCancelIoFile(KeyboardHandle, &IoStatusBlock);
217     }
218     else
219     {
220         /* Otherwise, return some status */
221         Status = IoStatusBlock.Status;
222     }
223 
224     return Status;
225 }
226 
227 static NTSTATUS
228 GetFileSystem(
229     IN PUNICODE_STRING VolumePathU,
230     IN OUT PWSTR FileSystemName,
231     IN SIZE_T FileSystemNameSize)
232 {
233     NTSTATUS Status;
234     OBJECT_ATTRIBUTES ObjectAttributes;
235     HANDLE FileHandle;
236     IO_STATUS_BLOCK IoStatusBlock;
237     PFILE_FS_ATTRIBUTE_INFORMATION FileFsAttribute;
238     UCHAR Buffer[sizeof(FILE_FS_ATTRIBUTE_INFORMATION) + MAX_PATH * sizeof(WCHAR)];
239 
240     FileFsAttribute = (PFILE_FS_ATTRIBUTE_INFORMATION)Buffer;
241 
242     InitializeObjectAttributes(&ObjectAttributes,
243                                VolumePathU,
244                                OBJ_CASE_INSENSITIVE,
245                                NULL,
246                                NULL);
247 
248     Status = NtCreateFile(&FileHandle,
249                           FILE_GENERIC_READ,
250                           &ObjectAttributes,
251                           &IoStatusBlock,
252                           NULL,
253                           0,
254                           FILE_SHARE_READ | FILE_SHARE_WRITE,
255                           FILE_OPEN,
256                           0,
257                           NULL,
258                           0);
259     if (!NT_SUCCESS(Status))
260     {
261         DPRINT1("Could not open volume '%wZ' to obtain its file system, Status 0x%08lx\n",
262                 VolumePathU, Status);
263         return Status;
264     }
265 
266     Status = NtQueryVolumeInformationFile(FileHandle,
267                                           &IoStatusBlock,
268                                           FileFsAttribute,
269                                           sizeof(Buffer),
270                                           FileFsAttributeInformation);
271     NtClose(FileHandle);
272 
273     if (!NT_SUCCESS(Status))
274     {
275         DPRINT1("NtQueryVolumeInformationFile() failed, Status 0x%08lx\n", Status);
276         return Status;
277     }
278 
279     if (FileSystemNameSize >= FileFsAttribute->FileSystemNameLength + sizeof(WCHAR))
280     {
281         RtlCopyMemory(FileSystemName,
282                       FileFsAttribute->FileSystemName,
283                       FileFsAttribute->FileSystemNameLength);
284         FileSystemName[FileFsAttribute->FileSystemNameLength / sizeof(WCHAR)] = UNICODE_NULL;
285     }
286     else
287     {
288         return STATUS_BUFFER_TOO_SMALL;
289     }
290 
291     return STATUS_SUCCESS;
292 }
293 
294 /* This is based on SysInternals' ChkDsk application */
295 static BOOLEAN
296 NTAPI
297 ChkdskCallback(
298     IN CALLBACKCOMMAND Command,
299     IN ULONG Modifier,
300     IN PVOID Argument)
301 {
302     PDWORD      Percent;
303     PBOOLEAN    Status;
304     PTEXTOUTPUT Output;
305 
306     //
307     // We get other types of commands,
308     // but we don't have to pay attention to them
309     //
310     switch(Command)
311     {
312     case UNKNOWN2:
313         DPRINT("UNKNOWN2\n");
314         break;
315 
316     case UNKNOWN3:
317         DPRINT("UNKNOWN3\n");
318         break;
319 
320     case UNKNOWN4:
321         DPRINT("UNKNOWN4\n");
322         break;
323 
324     case UNKNOWN5:
325         DPRINT("UNKNOWN5\n");
326         break;
327 
328     case UNKNOWN9:
329         DPRINT("UNKNOWN9\n");
330         break;
331 
332     case UNKNOWNA:
333         DPRINT("UNKNOWNA\n");
334         break;
335 
336     case UNKNOWNC:
337         DPRINT("UNKNOWNC\n");
338         break;
339 
340     case UNKNOWND:
341         DPRINT("UNKNOWND\n");
342         break;
343 
344     case INSUFFICIENTRIGHTS:
345         DPRINT("INSUFFICIENTRIGHTS\n");
346         break;
347 
348     case FSNOTSUPPORTED:
349         DPRINT("FSNOTSUPPORTED\n");
350         break;
351 
352     case VOLUMEINUSE:
353         DPRINT("VOLUMEINUSE\n");
354         break;
355 
356     case STRUCTUREPROGRESS:
357         DPRINT("STRUCTUREPROGRESS\n");
358         break;
359 
360     case DONEWITHSTRUCTURE:
361         DPRINT("DONEWITHSTRUCTURE\n");
362         break;
363 
364     case CLUSTERSIZETOOSMALL:
365         DPRINT("CLUSTERSIZETOOSMALL\n");
366         break;
367 
368     case PROGRESS:
369         Percent = (PDWORD) Argument;
370         PrintString("%d percent completed.\r", *Percent);
371         break;
372 
373     case OUTPUT:
374         Output = (PTEXTOUTPUT) Argument;
375         PrintString("%s", Output->Output);
376         break;
377 
378     case DONE:
379         Status = (PBOOLEAN)Argument;
380         if (*Status == FALSE)
381         {
382             PrintString("The file system check was unable to complete successfully.\r\n\r\n");
383             // Error = TRUE;
384         }
385         break;
386     }
387     return TRUE;
388 }
389 
390 static NTSTATUS
391 CheckVolume(
392     IN PCWSTR VolumePath,
393     IN LONG TimeOut,
394     IN BOOLEAN CheckOnlyIfDirty)
395 {
396     NTSTATUS Status;
397     BOOLEAN Success;
398     PCWSTR DisplayName;
399     UNICODE_STRING VolumePathU;
400     ULONG Count;
401     WCHAR FileSystem[128];
402 
403     RtlInitUnicodeString(&VolumePathU, VolumePath);
404 
405     /* Get a drive string for display purposes only */
406     if (wcslen(VolumePath) == 6 &&
407         VolumePath[0] == L'\\'  &&
408         VolumePath[1] == L'?'   &&
409         VolumePath[2] == L'?'   &&
410         VolumePath[3] == L'\\'  &&
411         VolumePath[5] == L':')
412     {
413         /* DOS drive */
414         DisplayName = &VolumePath[4];
415     }
416     else
417     {
418         DisplayName = VolumePath;
419     }
420 
421     DPRINT1("AUTOCHK: Checking %wZ\n", &VolumePathU);
422     PrintString("Verifying the file system on %S\r\n", DisplayName);
423 
424     /* Get the file system */
425     Status = GetFileSystem(&VolumePathU,
426                            FileSystem,
427                            sizeof(FileSystem));
428     if (!NT_SUCCESS(Status))
429     {
430         DPRINT1("GetFileSystem() failed, Status 0x%08lx\n", Status);
431         PrintString("  Unable to detect the file system of volume %S\r\n", DisplayName);
432         goto Quit;
433     }
434 
435     PrintString("  The file system type is %S.\r\n\r\n", FileSystem);
436 
437     /* Find a suitable file system provider */
438     for (Count = 0; Count < RTL_NUMBER_OF(FileSystems); ++Count)
439     {
440         if (wcscmp(FileSystem, FileSystems[Count].Name) == 0)
441             break;
442     }
443     if (Count >= RTL_NUMBER_OF(FileSystems))
444     {
445         DPRINT1("File system %S not supported\n", FileSystem);
446         PrintString("  Unable to verify the volume. The %S file system is not supported.\r\n", FileSystem);
447         Status = STATUS_DLL_NOT_FOUND;
448         goto Quit;
449     }
450 
451     /* Check whether the volume is dirty */
452     Status = STATUS_SUCCESS;
453     Success = FileSystems[Count].ChkdskFunc(&VolumePathU,
454                                             ChkdskCallback,
455                                             FALSE, // FixErrors
456                                             TRUE,  // Verbose
457                                             TRUE,  // CheckOnlyIfDirty
458                                             FALSE, // ScanDrive
459                                             NULL,
460                                             NULL,
461                                             NULL,
462                                             NULL,
463                                             (PULONG)&Status);
464 
465     /* Perform the check either when the volume is dirty or a check is forced */
466     if (Success && ((Status == STATUS_DISK_CORRUPT_ERROR) || !CheckOnlyIfDirty))
467     {
468         /* Let the user decide whether to repair */
469         if (Status == STATUS_DISK_CORRUPT_ERROR)
470         {
471             PrintString("The file system on volume %S needs to be checked for problems.\r\n", DisplayName);
472             PrintString("You may cancel this check, but it is recommended that you continue.\r\n");
473         }
474         else
475         {
476             PrintString("A volume check has been scheduled.\r\n");
477         }
478 
479         if (!KeyboardHandle || WaitForKeyboard(KeyboardHandle, TimeOut) == STATUS_TIMEOUT)
480         {
481             PrintString("The system will now check the file system.\r\n\r\n");
482             Status = STATUS_SUCCESS;
483             Success = FileSystems[Count].ChkdskFunc(&VolumePathU,
484                                                     ChkdskCallback,
485                                                     TRUE,  // FixErrors
486                                                     TRUE,  // Verbose
487                                                     CheckOnlyIfDirty,
488                                                     FALSE, // ScanDrive
489                                                     NULL,
490                                                     NULL,
491                                                     NULL,
492                                                     NULL,
493                                                     (PULONG)&Status);
494         }
495         else
496         {
497             PrintString("The file system check has been skipped.\r\n");
498         }
499     }
500 
501 Quit:
502     PrintString("\r\n\r\n");
503     return Status;
504 }
505 
506 static VOID
507 QueryTimeout(
508     IN OUT PLONG TimeOut)
509 {
510     RTL_QUERY_REGISTRY_TABLE QueryTable[2];
511 
512     RtlZeroMemory(QueryTable, sizeof(QueryTable));
513     QueryTable[0].Flags = RTL_QUERY_REGISTRY_DIRECT;
514     QueryTable[0].Name = L"AutoChkTimeOut";
515     QueryTable[0].EntryContext = TimeOut;
516 
517     RtlQueryRegistryValues(RTL_REGISTRY_CONTROL, L"Session Manager", QueryTable, NULL, NULL);
518     /* See: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/autochk */
519     *TimeOut = min(max(*TimeOut, 0), 259200);
520 }
521 
522 INT
523 __cdecl
524 _main(
525     IN INT argc,
526     IN PCHAR argv[],
527     IN PCHAR envp[],
528     IN ULONG DebugFlag)
529 {
530     NTSTATUS Status;
531     LONG TimeOut;
532     ULONG i;
533     BOOLEAN CheckAllVolumes = FALSE;
534     BOOLEAN CheckOnlyIfDirty = TRUE;
535     PROCESS_DEVICEMAP_INFORMATION DeviceMap;
536     PCHAR SkipDrives = NULL;
537     ANSI_STRING VolumePathA;
538     UNICODE_STRING VolumePathU;
539     WCHAR VolumePath[128] = L"";
540 
541     /*
542      * Parse the command-line: optional command switches,
543      * then the NT volume name (or drive letter) to be analysed.
544      * If "*" is passed this means that we check all the volumes.
545      */
546     if (argc <= 1)
547     {
548         /* Only one parameter (the program name), bail out */
549         return 1;
550     }
551     for (i = 1; i < argc; ++i)
552     {
553         if (argv[i][0] == '/' || argv[i][0] == '-')
554         {
555             DPRINT("Parameter %d: %s\n", i, argv[i]);
556             switch (toupper(argv[i][1]))
557             {
558                 case 'K': /* List of drive letters to skip */
559                 {
560                     /* Check for the separator */
561                     if (argv[i][2] != ':')
562                         goto Default;
563 
564                     SkipDrives = &argv[i][3];
565                     break;
566                 }
567 
568                 case 'R': /* Repair the errors, implies /P */
569                 case 'P': /* Check even if not dirty */
570                 {
571                     if (argv[i][2] != ANSI_NULL)
572                         goto Default;
573 
574                     CheckOnlyIfDirty = FALSE;
575                     break;
576                 }
577 
578                 default: Default:
579                     DPRINT1("Unknown switch %d: %s\n", i, argv[i]);
580                     break;
581             }
582         }
583         else
584         {
585             DPRINT("Parameter %d - Volume specification: %s\n", i, argv[i]);
586             if (strcmp(argv[i], "*") == 0)
587             {
588                 CheckAllVolumes = TRUE;
589             }
590             else
591             {
592                 RtlInitEmptyUnicodeString(&VolumePathU,
593                                           VolumePath,
594                                           sizeof(VolumePath));
595                 RtlInitAnsiString(&VolumePathA, argv[i]);
596                 Status = RtlAnsiStringToUnicodeString(&VolumePathU,
597                                                       &VolumePathA,
598                                                       FALSE);
599             }
600 
601             /* Stop the parsing now */
602             break;
603         }
604     }
605 
606     /*
607      * FIXME: We should probably use here the mount manager to be
608      * able to check volumes which don't have a drive letter.
609      */
610     Status = NtQueryInformationProcess(NtCurrentProcess(),
611                                        ProcessDeviceMap,
612                                        &DeviceMap.Query,
613                                        sizeof(DeviceMap.Query),
614                                        NULL);
615     if (!NT_SUCCESS(Status))
616     {
617         DPRINT1("NtQueryInformationProcess() failed, Status 0x%08lx\n", Status);
618         return 1;
619     }
620 
621     /* Filter out the skipped drives from the map */
622     if (SkipDrives && *SkipDrives)
623     {
624         DPRINT1("Skipping drives:");
625         while (*SkipDrives)
626         {
627 #if DBG
628             DbgPrint(" %c:", *SkipDrives);
629 #endif
630             /* Retrieve the index and filter the drive out */
631             i = toupper(*SkipDrives) - 'A';
632             if (0 <= i && i <= 'Z'-'A')
633                 DeviceMap.Query.DriveMap &= ~(1 << i);
634 
635             /* Go to the next drive letter */
636             ++SkipDrives;
637         }
638         DbgPrint("\n");
639     }
640 
641     /* Query the timeout */
642     TimeOut = 3;
643     QueryTimeout(&TimeOut);
644 
645     /* Open the keyboard */
646     Status = OpenKeyboard(&KeyboardHandle);
647     if (!NT_SUCCESS(Status))
648     {
649         DPRINT1("OpenKeyboard() failed, Status 0x%08lx\n", Status);
650         /* Ignore keyboard interaction */
651     }
652 
653     if (CheckAllVolumes)
654     {
655         /* Enumerate and check each of the available volumes */
656         for (i = 0; i <= 'Z'-'A'; i++)
657         {
658             if ((DeviceMap.Query.DriveMap & (1 << i)) &&
659                 (DeviceMap.Query.DriveType[i] == DOSDEVICE_DRIVE_FIXED))
660             {
661                 swprintf(VolumePath, L"\\??\\%c:", L'A' + i);
662                 CheckVolume(VolumePath, TimeOut, CheckOnlyIfDirty);
663             }
664         }
665     }
666     else
667     {
668         /* Retrieve our index and analyse the volume */
669         if (wcslen(VolumePath) == 6 &&
670             VolumePath[0] == L'\\'  &&
671             VolumePath[1] == L'?'   &&
672             VolumePath[2] == L'?'   &&
673             VolumePath[3] == L'\\'  &&
674             VolumePath[5] == L':')
675         {
676             i = toupper(VolumePath[4]) - 'A';
677             if ((DeviceMap.Query.DriveMap & (1 << i)) &&
678                 (DeviceMap.Query.DriveType[i] == DOSDEVICE_DRIVE_FIXED))
679             {
680                 CheckVolume(VolumePath, TimeOut, CheckOnlyIfDirty);
681             }
682         }
683         else
684         {
685             /* Just perform the check on the specified volume */
686             // TODO: Check volume type using NtQueryVolumeInformationFile(FileFsDeviceInformation)
687             CheckVolume(VolumePath, TimeOut, CheckOnlyIfDirty);
688         }
689     }
690 
691     /* Close the keyboard */
692     if (KeyboardHandle)
693         NtClose(KeyboardHandle);
694 
695     // PrintString("Done\r\n\r\n");
696     return 0;
697 }
698 
699 /* EOF */
700