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