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