source: branches/eraser6/Manager/DirectExecutor.cs @ 898

Revision 898, 43.0 KB checked in by lowjoel, 6 years ago (diff)

Stylistic corrections: use internal set in properties instead of using interally-visible member variables

  • Property svn:keywords set to Id
Line 
1/*
2 * $Id$
3 * Copyright 2008 The Eraser Project
4 * Original Author: Joel Low <lowjoel@users.sourceforge.net>
5 * Modified By: Kasra Nassiri <cjax@users.sourceforge.net> @17/10/2008
6 * Modified By:
7 *
8 * This file is part of Eraser.
9 *
10 * Eraser is free software: you can redistribute it and/or modify it under the
11 * terms of the GNU General Public License as published by the Free Software
12 * Foundation, either version 3 of the License, or (at your option) any later
13 * version.
14 *
15 * Eraser is distributed in the hope that it will be useful, but WITHOUT ANY
16 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
17 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
18 *
19 * A copy of the GNU General Public License can be found at
20 * <http://www.gnu.org/licenses/>.
21 */
22
23using System;
24using System.Collections.Generic;
25using System.Collections.Specialized;
26using System.Text;
27using System.Threading;
28using System.IO;
29
30using Eraser.Util;
31using System.Security.Principal;
32using System.Runtime.Serialization;
33using System.Runtime.Serialization.Formatters.Binary;
34
35namespace Eraser.Manager
36{
37    /// <summary>
38    /// The DirectExecutor class is used by the Eraser GUI directly when the program
39    /// is run without the help of a Service.
40    /// </summary>
41    public class DirectExecutor : Executor
42    {
43        public DirectExecutor()
44        {
45            thread = new Thread(Main);
46        }
47
48        public override void Dispose()
49        {
50            thread.Abort();
51            schedulerInterrupt.Set();
52        }
53
54        public override void Run()
55        {
56            thread.CurrentUICulture = Thread.CurrentThread.CurrentUICulture;
57            thread.Start();
58        }
59
60        public override void AddTask(ref Task task)
61        {
62            lock (unusedIdsLock)
63            {
64                if (unusedIds.Count != 0)
65                {
66                    task.ID = unusedIds[0];
67                    unusedIds.RemoveAt(0);
68                }
69                else
70                    task.ID = ++nextId;
71            }
72
73            //Set the executor of the task
74            task.Executor = this;
75
76            //Add the task to the set of tasks
77            lock (tasksLock)
78                tasks.Add(task.ID, task);
79
80            //Call all the event handlers who registered to be notified of tasks
81            //being added.
82            OnTaskAdded(task);
83
84            //If the task is scheduled to run now, break the waiting thread and
85            //run it immediately
86            if (task.Schedule == Schedule.RunNow)
87            {
88                QueueTask(task);
89            }
90            //If the task is scheduled, add the next execution time to the list
91            //of schduled tasks.
92            else if (task.Schedule != Schedule.RunOnRestart)
93            {
94                ScheduleTask(task);
95            }
96        }
97
98        public override bool DeleteTask(uint taskId)
99        {
100            lock (tasksLock)
101            {
102                if (!tasks.ContainsKey(taskId))
103                    return false;
104
105                Task task = tasks[taskId];
106                lock (unusedIdsLock)
107                    unusedIds.Add(taskId);
108                tasks.Remove(taskId);
109
110                for (int i = 0; i != scheduledTasks.Count; )
111                    if (scheduledTasks.Values[i].ID == taskId)
112                        scheduledTasks.RemoveAt(i);
113                    else
114                        ++i;
115
116                //Call all event handlers registered to be notified of task deletions.
117                OnTaskDeleted(task);
118            }
119
120            return true;
121        }
122
123        public override void ReplaceTask(Task task)
124        {
125            lock (tasksLock)
126            {
127                //Replace the task in the global set
128                if (!tasks.ContainsKey(task.ID))
129                    return;
130
131                tasks[task.ID] = task;
132
133                //Then replace the task if it is in the queue
134                for (int i = 0; i != scheduledTasks.Count; ++i)
135                    if (scheduledTasks.Values[i].ID == task.ID)
136                    {
137                        scheduledTasks.RemoveAt(i);
138                        if (task.Schedule is RecurringSchedule)
139                            ScheduleTask(task);
140                        else if (task.Schedule == Schedule.RunNow)
141                            QueueTask(task);
142                    }
143            }
144        }
145
146        public override void QueueTask(Task task)
147        {
148            lock (tasksLock)
149            {
150                //Set the task variable to indicate that the task is already
151                //waiting to be executed.
152                task.Queued = true;
153
154                //Queue the task to be run immediately.
155                scheduledTasks.Add(DateTime.Now, task);
156                schedulerInterrupt.Set();
157            }
158        }
159
160        public override void ScheduleTask(Task task)
161        {
162            RecurringSchedule schedule = (RecurringSchedule)task.Schedule;
163            if (schedule.MissedPreviousSchedule &&
164                ManagerLibrary.Instance.Settings.ExecuteMissedTasksImmediately)
165                //OK, we've missed the schedule and the user wants the thing
166                //to follow up immediately.
167                scheduledTasks.Add(DateTime.Now, task);
168            else
169                scheduledTasks.Add(schedule.NextRun, task);
170        }
171
172        public override void QueueRestartTasks()
173        {
174            lock (tasksLock)
175            {
176                foreach (Task task in tasks.Values)
177                    if (task.Schedule == Schedule.RunOnRestart)
178                        QueueTask(task);
179            }
180        }
181
182        public override void CancelTask(Task task)
183        {
184            lock (currentTask)
185            {
186                if (currentTask == task)
187                {
188                    currentTask.Cancelled = true;
189                    return;
190                }
191            }
192
193            lock (tasksLock)
194                for (int i = 0; i != scheduledTasks.Count; ++i)
195                    if (scheduledTasks.Values[i] == task)
196                    {
197                        scheduledTasks.RemoveAt(i);
198                        return;
199                    }
200
201            throw new ArgumentOutOfRangeException(S._("The task to be cancelled must " +
202                "either be currently executing or queued."));
203        }
204
205        public override Task GetTask(uint taskId)
206        {
207            lock (tasksLock)
208            {
209                if (!tasks.ContainsKey(taskId))
210                    return null;
211                return tasks[taskId];
212            }
213        }
214
215        public override List<Task> GetTasks()
216        {
217            lock (tasksLock)
218            {
219                Task[] result = new Task[tasks.Count];
220                tasks.Values.CopyTo(result, 0);
221                return new List<Task>(result);
222            }
223        }
224
225        public override void SaveTaskList(Stream stream)
226        {
227            lock (tasksLock)
228                new BinaryFormatter().Serialize(stream, tasks);
229        }
230
231        public override void LoadTaskList(Stream stream)
232        {
233            lock (tasksLock)
234            {
235                //Load the list into the dictionary
236                tasks = (Dictionary<uint, Task>)new BinaryFormatter().Deserialize(stream);
237
238                //Ignore the next portion if there are no tasks
239                if (tasks.Count == 0)
240                    return;
241
242                lock (unusedIdsLock)
243                {
244                    //Find gaps in the numbering
245                    nextId = 1;
246                    foreach (uint id in tasks.Keys)
247                    {
248                        Task currentTask = tasks[id];
249                        currentTask.Executor = this;
250                        while (id > nextId)
251                            unusedIds.Add(nextId++);
252                        ++nextId;
253
254                        //Check if the task is recurring. If it is, check if we missed it.
255                        if (currentTask.Schedule is RecurringSchedule)
256                            ScheduleTask(currentTask);
257                    }
258
259                    //Decrement the ID, since the next ID will be preincremented
260                    //before use.
261                    --nextId;
262                }
263            }
264        }
265
266        /// <summary>
267        /// The thread entry point for this object. This object operates on a queue
268        /// and hence the thread will sequentially execute tasks.
269        /// </summary>
270        private void Main()
271        {
272            //The waiting thread will utilize a polling loop to check for new
273            //scheduled tasks. This will be checked every 30 seconds. However,
274            //when the thread is waiting for a new task, it can be interrupted.
275            while (thread.ThreadState != ThreadState.AbortRequested)
276            {
277                //Check for a new task
278                Task task = null;
279                lock (tasksLock)
280                {
281                    if (scheduledTasks.Count != 0 &&
282                        (scheduledTasks.Values[0].Schedule == Schedule.RunNow ||
283                         scheduledTasks.Keys[0] <= DateTime.Now))
284                    {
285                        task = scheduledTasks.Values[0];
286                        scheduledTasks.RemoveAt(0);
287                    }
288                }
289
290                if (task != null)
291                {
292                    //Set the currently executing task.
293                    currentTask = task;
294
295                    try
296                    {
297                        //Prevent the system from sleeping.
298                        KernelAPI.SetThreadExecutionState(KernelAPI.EXECUTION_STATE.ES_CONTINUOUS |
299                            KernelAPI.EXECUTION_STATE.ES_SYSTEM_REQUIRED);
300
301                        //Broadcast the task started event.
302                        task.Queued = false;
303                        task.Cancelled = false;
304                        task.OnTaskStarted(new TaskEventArgs(task));
305                        OnTaskProcessing(task);
306
307                        //Start a new log session to separate this session's events
308                        //from previous ones.
309                        task.Log.NewSession();
310
311                        //Run the task
312                        TaskProgressManager progress = new TaskProgressManager(currentTask);
313                        foreach (Task.ErasureTarget target in task.Targets)
314                            try
315                            {
316                                progress.Event.CurrentTarget = target;
317                                ++progress.Event.CurrentTargetIndex;
318                                if (target is Task.UnusedSpace)
319                                    EraseUnusedSpace(task, (Task.UnusedSpace)target, progress);
320                                else if (target is Task.FilesystemObject)
321                                    EraseFilesystemObject(task, (Task.FilesystemObject)target, progress);
322                                else
323                                    throw new ArgumentException(S._("Unknown erasure target."));
324                            }
325                            catch (FatalException)
326                            {
327                                throw;
328                            }
329                            catch (Exception e)
330                            {
331                                task.Log.Add(new LogEntry(e.Message, LogLevel.ERROR));
332                            }
333                    }
334                    catch (FatalException e)
335                    {
336                        task.Log.Add(new LogEntry(e.Message, LogLevel.FATAL));
337                    }
338                    catch (Exception e)
339                    {
340                        task.Log.Add(new LogEntry(e.Message, LogLevel.ERROR));
341                    }
342                    finally
343                    {
344                        //Allow the system to sleep again.
345                        KernelAPI.SetThreadExecutionState(KernelAPI.EXECUTION_STATE.ES_CONTINUOUS);
346
347                        //If the task is a recurring task, reschedule it since we are done.
348                        if (task.Schedule is RecurringSchedule)
349                            ((RecurringSchedule)task.Schedule).Reschedule(DateTime.Now);
350
351                        //If the task is an execute on restart task, it is only run
352                        //once and can now be restored to an immediately executed task
353                        if (task.Schedule == Schedule.RunOnRestart)
354                            task.Schedule = Schedule.RunNow;
355
356                        //And the task finished event.
357                        task.OnTaskFinished(new TaskEventArgs(task));
358                        OnTaskProcessed(currentTask);
359                    }
360
361                    currentTask = null;
362                }
363
364                //Wait for half a minute to check for the next scheduled task.
365                schedulerInterrupt.WaitOne(30000, false);
366            }
367        }
368
369        /// <summary>
370        /// Manages the progress for any operation.
371        /// </summary>
372        private class ProgressManager
373        {
374            /// <summary>
375            /// Starts measuring the speed of the task.
376            /// </summary>
377            public void Start()
378            {
379                startTime = DateTime.Now;
380            }
381
382            /// <summary>
383            /// Tracks the amount of the operation completed.
384            /// </summary>
385            public long Completed
386            {
387                get
388                {
389                    return completed;
390                }
391                set
392                {
393                    lastCompleted += value - completed;
394                    completed = value;
395                }
396            }
397
398            /// <summary>
399            /// The amount to reach before the operation completes.
400            /// </summary>
401            public long Total
402            {
403                get
404                {
405                    return total;
406                }
407                set
408                {
409                    total = value;
410                }
411            }
412
413            /// <summary>
414            /// Gets the percentage of the operation completed.
415            /// </summary>
416            public float Progress
417            {
418                get
419                {
420                    return (float)((double)Completed / Total);
421                }
422            }
423
424            /// <summary>
425            /// Computes the speed of the erase, in units of completion per second,
426            /// based on the information collected in the previous 15 seconds.
427            /// </summary>
428            public int Speed
429            {
430                get
431                {
432                    if (DateTime.Now == startTime)
433                        return 0;
434
435                    if ((DateTime.Now - lastSpeedCalc).Seconds < 15 && lastSpeed != 0)
436                        return lastSpeed;
437
438                    lastSpeed = (int)(lastCompleted / (DateTime.Now - lastSpeedCalc).TotalSeconds);
439                    lastSpeedCalc = DateTime.Now;
440                    lastCompleted = 0;
441                    return lastSpeed;
442                }
443            }
444
445            /// <summary>
446            /// Calculates the estimated amount of time left based on the total
447            /// amount of information to erase and the current speed of the erase
448            /// </summary>
449            public TimeSpan TimeLeft
450            {
451                get
452                {
453                    if (Speed == 0)
454                        return new TimeSpan(0, 0, -1);
455                    return new TimeSpan(0, 0, (int)((Total - Completed) / Speed));
456                }
457            }
458
459            /// <summary>
460            /// The starting time of the operation, used to determine average speed.
461            /// </summary>
462            private DateTime startTime;
463
464            /// <summary>
465            /// The last time a speed calculation was computed so that speed is not
466            /// computed too often.
467            /// </summary>
468            private DateTime lastSpeedCalc;
469
470            /// <summary>
471            /// The last calculated speed of the operation.
472            /// </summary>
473            private int lastSpeed;
474
475            /// <summary>
476            /// The amount of the operation completed since the last speed computation.
477            /// </summary>
478            private long lastCompleted;
479
480            /// <summary>
481            /// The amount of the operation completed.
482            /// </summary>
483            private long completed;
484
485            /// <summary>
486            /// The amount to reach before the operation is completed.
487            /// </summary>
488            private long total;
489        }
490
491        /// <summary>
492        /// Provides a common interface to track the progress made by the Erase functions.
493        /// </summary>
494        private class TaskProgressManager : ProgressManager
495        {
496            /// <summary>
497            /// Constructor.
498            /// </summary>
499            public TaskProgressManager(Task task)
500            {
501                foreach (Task.ErasureTarget target in task.Targets)
502                    Total += target.TotalData;
503
504                Event = new TaskProgressEventArgs(task);
505                Start();
506            }
507
508            /// <summary>
509            /// The TaskProgressEventArgs object representing the progress of the current
510            /// task.
511            /// </summary>
512            public TaskProgressEventArgs Event
513            {
514                get
515                {
516                    return evt;
517                }
518                set
519                {
520                    evt = value;
521                }
522            }
523
524            private TaskProgressEventArgs evt;
525        }
526
527        #region Unused Space erasure functions
528        /// <summary>
529        /// Executes a unused space erase.
530        /// </summary>
531        /// <param name="task">The task currently being executed</param>
532        /// <param name="target">The target of the unused space erase.</param>
533        /// <param name="progress">The progress manager object managing the progress of the task</param>
534        private void EraseUnusedSpace(Task task, Task.UnusedSpace target, TaskProgressManager progress)
535        {
536            //Check for sufficient privileges to run the unused space erasure.
537            if (!Permissions.IsAdministrator())
538            {
539                if (Environment.OSVersion.Platform == PlatformID.Win32NT &&
540                    Environment.OSVersion.Version >= new Version(6, 0))
541                {
542                    throw new Exception(S._("The program does not have the required permissions " +
543                        "to erase the unused space on disk. Run the program as an administrator " +
544                        "and retry the operation."));
545                }
546                else
547                    throw new Exception(S._("The program does not have the required permissions " +
548                        "to erase the unused space on disk"));
549            }
550
551            //If the user is under disk quotas, log a warning message
552            if (VolumeInfo.FromMountpoint(target.Drive).HasQuota)
553                task.Log.Add(new LogEntry(S._("The drive which is having its unused space erased " +
554                    "has disk quotas active. This will prevent the complete erasure of unused " +
555                    "space and will pose a security concern"), LogLevel.WARNING));
556
557            //Get the erasure method if the user specified he wants the default.
558            ErasureMethod method = target.Method;
559           
560            //Erase the cluster tips of every file on the drive.
561            if (target.EraseClusterTips)
562            {
563                progress.Event.CurrentItemName = S._("Cluster tips");
564                progress.Event.CurrentTargetTotalPasses = method.Passes;
565                progress.Event.TimeLeft = progress.TimeLeft;
566                task.OnProgressChanged(progress.Event);
567
568                ProgressManager tipProgress = new ProgressManager();
569                tipProgress.Start();
570                EraseClusterTips(task, target, method,
571                    delegate(int currentFile, string currentFilePath, int totalFiles)
572                    {
573                        tipProgress.Total = totalFiles;
574                        tipProgress.Completed = currentFile;
575
576                        progress.Event.CurrentItemName = S._("(Tips) {0}", currentFilePath);
577                        progress.Event.CurrentItemProgress = tipProgress.Progress;
578                        progress.Event.CurrentTargetProgress = progress.Event.CurrentItemProgress / 10;
579                        progress.Event.TimeLeft = tipProgress.TimeLeft;
580                        task.OnProgressChanged(progress.Event);
581
582                        lock (currentTask)
583                            if (currentTask.Cancelled)
584                                throw new FatalException(S._("The task was cancelled."));
585                    }
586                );
587            }
588
589            //Make a folder to dump our temporary files in
590            DirectoryInfo info = new DirectoryInfo(target.Drive);
591            {
592                string directoryName;
593                do
594                    directoryName = GenerateRandomFileName(18);
595                while (Directory.Exists(info.FullName + Path.DirectorySeparatorChar + directoryName));
596                info = info.CreateSubdirectory(directoryName);
597            }
598
599            try
600            {
601                //Set the folder's compression flag off since we want to use as much
602                //space as possible
603                if (Eraser.Util.File.IsCompressed(info.FullName))
604                    Eraser.Util.File.SetCompression(info.FullName, false);
605
606                //Determine the total amount of data that needs to be written.
607                VolumeInfo volInfo = VolumeInfo.FromMountpoint(target.Drive);
608                long totalSize = method.CalculateEraseDataSize(null, volInfo.TotalFreeSpace);
609
610                //Continue creating files while there is free space.
611                progress.Event.CurrentItemName = S._("Unused space");
612                task.OnProgressChanged(progress.Event);
613                while (volInfo.AvailableFreeSpace > 0)
614                {
615                    //Generate a non-existant file name
616                    string currFile;
617                    do
618                        currFile = info.FullName + Path.DirectorySeparatorChar +
619                            GenerateRandomFileName(18);
620                    while (System.IO.File.Exists(currFile));
621
622                    //Create the stream
623                    using (FileStream stream = new FileStream(currFile, FileMode.CreateNew,
624                        FileAccess.Write, FileShare.None, 8, FileOptions.WriteThrough))
625                    {
626                        //Set the length of the file to be the amount of free space left
627                        //or the maximum size of one of these dumps.
628                        long streamLength = Math.Min(ErasureMethod.FreeSpaceFileUnit,
629                            volInfo.AvailableFreeSpace);
630
631                        //Handle IO exceptions gracefully, because the filesystem
632                        //may require more space than demanded by us for file allocation.
633                        while (true)
634                            try
635                            {
636                                stream.SetLength(streamLength);
637                                break;
638                            }
639                            catch (IOException)
640                            {
641                                if (streamLength > volInfo.ClusterSize)
642                                    streamLength -= volInfo.ClusterSize;
643                                else
644                                    throw;
645                            }
646
647                        //Then run the erase task
648                        method.Erase(stream, long.MaxValue,
649                            PRNGManager.GetInstance(ManagerLibrary.Instance.Settings.ActivePRNG),
650                            delegate(long lastWritten, int currentPass)
651                            {
652                                progress.Completed += lastWritten;
653                                progress.Event.CurrentItemPass = currentPass;
654                                progress.Event.CurrentItemProgress = progress.Progress;
655                                if (target.EraseClusterTips)
656                                    progress.Event.CurrentTargetProgress = (float)
657                                        (0.1f + progress.Event.CurrentItemProgress * 0.8f);
658                                else
659                                    progress.Event.CurrentTargetProgress = (float)
660                                        (progress.Event.CurrentItemProgress * 0.9f);
661                                progress.Event.TimeLeft = progress.TimeLeft;
662                                task.OnProgressChanged(progress.Event);
663
664                                lock (currentTask)
665                                    if (currentTask.Cancelled)
666                                        throw new FatalException(S._("The task was cancelled."));
667                            }
668                        );
669                    }
670                }
671
672                //Erase old resident file system table files
673                progress.Event.CurrentItemName = S._("Old resident file system table files");
674                task.OnProgressChanged(progress.Event);
675                EraseOldFilesystemResidentFiles(info, method, null);
676            }
677            finally
678            {
679                //Remove the folder holding all our temporary files.
680                progress.Event.CurrentItemName = S._("Removing temporary files");
681                task.OnProgressChanged(progress.Event);
682                RemoveFolder(info);
683            }
684
685            //Then clean the old file system entries
686            progress.Event.CurrentItemName = S._("Old file system entries");
687            ProgressManager fsEntriesProgress = new ProgressManager();
688            fsEntriesProgress.Start();
689            EraseOldFilesystemEntries(info.Parent,
690                delegate(int currentFile, int totalFiles)
691                {
692                    //Compute the progress
693                    fsEntriesProgress.Total = totalFiles;
694                    fsEntriesProgress.Completed = currentFile;
695
696                    //Set the event parameters, then broadcast the progress event.
697                    progress.Event.TimeLeft = fsEntriesProgress.TimeLeft;
698                    progress.Event.CurrentItemProgress = fsEntriesProgress.Progress;
699                    progress.Event.CurrentTargetProgress = (float)(
700                        0.9 + progress.Event.CurrentItemProgress / 10);
701                    task.OnProgressChanged(progress.Event);
702                }
703            );
704        }
705
706        private delegate void SubFoldersHandler(DirectoryInfo info);
707        private delegate void ClusterTipsEraseProgress(int currentFile,
708            string currentFilePath, int totalFiles);
709
710        private static void EraseClusterTips(Task task, Task.UnusedSpace target,
711            ErasureMethod method, ClusterTipsEraseProgress callback)
712        {
713            //List all the files which can be erased.
714            List<string> files = new List<string>();
715            SubFoldersHandler subFolders = null;
716
717            subFolders = delegate(DirectoryInfo info)
718            {
719                //Check if we've been cancelled
720                if (task.Cancelled)
721                    throw new FatalException(S._("The task was cancelled."));
722
723                try
724                {
725                    //Skip this directory if it is a reparse point
726                    if ((info.Attributes & FileAttributes.ReparsePoint) != 0)
727                    {
728                        task.Log.Add(new LogEntry(S._("Files in {0} did not have their cluster " +
729                            "tips erased because it is a hard link or a symbolic link.",
730                            info.FullName), LogLevel.INFORMATION));
731                        return;
732                    }
733
734                    foreach (FileInfo file in info.GetFiles())
735                        if (Util.File.IsProtectedSystemFile(file.FullName))
736                            task.Log.Add(new LogEntry(S._("{0} did not have its cluster tips " +
737                                "erased, because it is a system file", file.FullName),
738                                LogLevel.INFORMATION));
739                        else if ((file.Attributes & FileAttributes.ReparsePoint) != 0)
740                            task.Log.Add(new LogEntry(S._("{0} did not have its cluster tips " +
741                                "erased because it is a hard link or a symbolic link.",
742                                file.FullName), LogLevel.INFORMATION));
743                        else if ((file.Attributes & FileAttributes.Compressed) != 0 ||
744                            (file.Attributes & FileAttributes.Encrypted) != 0 ||
745                            (file.Attributes & FileAttributes.SparseFile) != 0)
746                        {
747                            task.Log.Add(new LogEntry(S._("{0} did not have its cluster tips " +
748                                "erased because it is compressed, encrypted or a sparse file.",
749                                file.FullName), LogLevel.INFORMATION));
750                        }
751                        else
752                        {
753                            try
754                            {
755                                foreach (string i in Util.File.GetADSes(file))
756                                    files.Add(file.FullName + ':' + i);
757
758                                files.Add(file.FullName);
759                            }
760                            catch (IOException e)
761                            {
762                                task.Log.Add(new LogEntry(S._("{0} did not have its cluster tips erased " +
763                                    "because of the following error: {1}", info.FullName, e.Message),
764                                    LogLevel.ERROR));
765                            }
766                        }
767
768                    foreach (DirectoryInfo subDirInfo in info.GetDirectories())
769                        subFolders(subDirInfo);
770                }
771                catch (UnauthorizedAccessException e)
772                {
773                    task.Log.Add(new LogEntry(S._("{0} did not have its cluster tips erased " +
774                        "because of the following error: {1}", info.FullName, e.Message),
775                        LogLevel.ERROR));
776                }
777                catch (IOException e)
778                {
779                    task.Log.Add(new LogEntry(S._("{0} did not have its cluster tips erased " +
780                        "because of the following error: {1}", info.FullName, e.Message),
781                        LogLevel.ERROR));
782                }
783            };
784
785            subFolders(new DirectoryInfo(target.Drive));
786
787            //For every file, erase the cluster tips.
788            for (int i = 0, j = files.Count; i != j; ++i)
789            {
790                //Get the file attributes for restoring later
791                StreamInfo info = new StreamInfo(files[i]);
792                FileAttributes fileAttr = info.Attributes;
793
794                try
795                {
796                    //Reset the file attributes.
797                    info.Attributes = FileAttributes.Normal;
798                    EraseFileClusterTips(files[i], method);
799                }
800                catch (Exception e)
801                {
802                    task.Log.Add(new LogEntry(S._("{0} did not have its cluster tips erased. " +
803                        "The error returned was: {1}", files[i], e.Message), LogLevel.ERROR));
804                }
805                finally
806                {
807                    info.Attributes = fileAttr;
808                }
809                callback(i, files[i], files.Count);
810            }
811        }
812
813        /// <summary>
814        /// Erases the cluster tips of the given file.
815        /// </summary>
816        /// <param name="file">The file to erase.</param>
817        /// <param name="method">The erasure method to use.</param>
818        private static void EraseFileClusterTips(string file, ErasureMethod method)
819        {
820            //Get the file access times
821            StreamInfo streamInfo = new StreamInfo(file);
822            DateTime lastAccess = DateTime.MinValue,
823                     lastWrite = DateTime.MinValue,
824                     created = DateTime.MinValue;
825            //Create the stream, lengthen the file, then tell the erasure method
826            //to erase the tips.
827            using (FileStream stream = streamInfo.Open(FileMode.Open, FileAccess.Write,
828                FileShare.None, FileOptions.WriteThrough))
829            {
830                long fileLength = stream.Length;
831                long fileArea = GetFileArea(file);
832
833                try
834                {
835                    //Get the file access times
836                    FileInfo info = streamInfo.File;
837                    if (info != null)
838                    {
839                        lastAccess = info.LastAccessTime;
840                        lastWrite = info.LastWriteTime;
841                        created = info.CreationTime;
842                    }
843
844                    stream.SetLength(fileArea);
845                    stream.Seek(fileLength, SeekOrigin.Begin);
846
847                    //Erase the file
848                    method.Erase(stream, long.MaxValue, PRNGManager.GetInstance(
849                        ManagerLibrary.Instance.Settings.ActivePRNG), null);
850                }
851                finally
852                {
853                    //Make sure the file is restored!
854                    stream.SetLength(fileLength);
855                }
856            }
857
858            //Set the file times
859            FileInfo fileInfo = streamInfo.File;
860            if (fileInfo != null)
861            {
862                fileInfo.LastAccessTime = lastAccess;
863                fileInfo.LastWriteTime = lastWrite;
864                fileInfo.CreationTime = created;
865            }
866        }
867        #endregion
868
869        #region Filesystem Object erasure functions
870        /// <summary>
871        /// The prototype of callbacks handling the file system table erase progress
872        /// </summary>
873        /// <param name="currentFile">The current file being erased.</param>
874        /// <param name="totalFiles">The estimated number of files that must be erased.</param>
875        private delegate void FilesystemEntriesEraseProgress(int currentFile, int totalFiles);
876       
877        /// <summary>
878        /// Erases old file system table-resident files. This creates small one-byte
879        /// files until disk is full. This will erase unused space which was used for
880        /// files resident in the file system table.
881        /// </summary>
882        /// <param name="info">The directory information structure containing
883        /// the path to store the temporary one-byte files. The file system table
884        /// of that drive will be erased.</param>
885        /// <param name="method">The method used to erase the files.</param>
886        private void EraseOldFilesystemResidentFiles(DirectoryInfo info, ErasureMethod method,
887            FilesystemEntriesEraseProgress callback)
888        {
889            VolumeInfo volInfo = VolumeInfo.FromMountpoint(info.FullName);
890            if (volInfo.VolumeFormat == "NTFS")
891            {
892                try
893                {
894                    //Squeeze one-byte files until the volume or the MFT is full.
895                    long oldMFTSize = NtfsAPI.GetMftValidSize(volInfo);
896                    for ( ; ; )
897                    {
898                        //Open this stream
899                        using (FileStream strm = new FileStream(Path.Combine(
900                            info.FullName, GenerateRandomFileName(18)),
901                            FileMode.CreateNew, FileAccess.Write, FileShare.None, 8,
902                            FileOptions.WriteThrough))
903                        {
904                            //Stretch the file size to use up some of the resident space.
905                            strm.SetLength(1);
906
907                            //Then run the erase task
908                            method.Erase(strm, long.MaxValue,
909                                PRNGManager.GetInstance(ManagerLibrary.Instance.Settings.ActivePRNG),
910                                null);
911                        }
912
913                        //We can stop when the MFT has grown.
914                        if (NtfsAPI.GetMftValidSize(volInfo) > oldMFTSize)
915                            break;
916                    }
917                }
918                catch (IOException)
919                {
920                    //OK, enough squeezing.
921                }
922            }
923        }
924
925        /// <summary>
926        /// Erases the unused space in the file system table by creating files, until
927        /// the table grows.
928        ///
929        /// This will overwrite unused portions of the table which were previously
930        /// used to store file entries.
931        /// </summary>
932        /// <param name="info">The directory information structure containing the path
933        /// to store the temporary files.</param>
934        /// <param name="callback">The callback function to handle the progress of the
935        /// file system entry erasure.</param>
936        private void EraseOldFilesystemEntries(DirectoryInfo info, FilesystemEntriesEraseProgress callback)
937        {
938            VolumeInfo volInfo = VolumeInfo.FromMountpoint(info.FullName);
939            if (volInfo.VolumeFormat == "NTFS")
940            {
941                //Create a directory to hold all the temporary files
942                DirectoryInfo tempDir = info.CreateSubdirectory(GenerateRandomFileName(32));
943
944                try
945                {
946                    //Get the size of the MFT
947                    long mftSize = NtfsAPI.GetMftValidSize(volInfo);
948                    long mftRecordSegmentSize = NtfsAPI.GetMftRecordSegmentSize(volInfo);
949                    int pollingInterval = (int)Math.Max(1, (mftSize / volInfo.ClusterSize / 20));
950                    int totalFiles = (int)Math.Max(1L, mftSize / mftRecordSegmentSize) *
951                        (FilenameErasePasses + 1);
952                    int filesCreated = 0;
953
954                    while (true)
955                    {
956                        ++filesCreated;
957                        using (FileStream strm = new FileStream(Path.Combine(
958                            tempDir.FullName, GenerateRandomFileName(220)),
959                            FileMode.CreateNew, FileAccess.Write))
960                        {
961                        }
962
963                        if (filesCreated % pollingInterval == 0)
964                        {
965                            if (callback != null)
966                                callback(filesCreated, totalFiles);
967
968                            lock (currentTask)
969                                if (currentTask.Cancelled)
970                                    throw new FatalException(S._("The task was cancelled."));
971
972                            //Check if the MFT has grown.
973                            if (mftSize < NtfsAPI.GetMftValidSize(volInfo))
974                                break;
975                        }
976                    }
977                }
978                catch (IOException)
979                {
980                }
981                finally
982                {
983                    //Clear up all the temporary files
984                    FileInfo[] files = tempDir.GetFiles("*", SearchOption.AllDirectories);
985                    int totalFiles = files.Length * (FilenameErasePasses + 1);
986                    for (int i = 0; i < files.Length; ++i)
987                    {
988                        if (callback != null && i % 50 == 0)
989                            callback(files.Length + i * FilenameErasePasses, totalFiles);
990                        RemoveFile(files[i]);
991                    }
992                   
993                    RemoveFolder(tempDir);
994                }
995            }
996            else
997                throw new NotImplementedException(S._("Could not erase old file system " +
998                    "entries: Unsupported File system")); //Eraser.cpp@2348
999        }
1000
1001        /// <summary>
1002        /// Erases a file or folder on the volume.
1003        /// </summary>
1004        /// <param name="task">The task currently being processed.</param>
1005        /// <param name="target">The target of the erasure.</param>
1006        /// <param name="progress">The progress manager for the current task.</param>
1007        private void EraseFilesystemObject(Task task, Task.FilesystemObject target,
1008            TaskProgressManager progress)
1009        {
1010            //Retrieve the list of files to erase.
1011            long dataTotal = 0;
1012            List<string> paths = target.GetPaths(out dataTotal);
1013
1014            //Get the erasure method if the user specified he wants the default.
1015            ErasureMethod method = target.Method;
1016
1017            //Calculate the total amount of data required to finish the wipe.
1018            dataTotal = method.CalculateEraseDataSize(paths, dataTotal);
1019
1020            //Iterate over every path, and erase the path.
1021            for (int i = 0; i < paths.Count; ++i)
1022            {
1023                //Update the task progress
1024                progress.Event.CurrentTargetProgress = i / (float)paths.Count;
1025                progress.Event.CurrentTarget = target;
1026                progress.Event.CurrentItemName = paths[i];
1027                progress.Event.CurrentItemProgress = 0;
1028                progress.Event.CurrentTargetTotalPasses = method.Passes;
1029                task.OnProgressChanged(progress.Event);
1030
1031               
1032                //Remove the read-only flag, if it is set.
1033                StreamInfo info = new StreamInfo(paths[i]);
1034                bool isReadOnly = false;
1035                if (isReadOnly = info.IsReadOnly)
1036                    info.IsReadOnly = false;
1037
1038                try
1039                {
1040                    //Make sure the file does not have any attributes which may affect
1041                    //the erasure process
1042                    if ((info.Attributes & FileAttributes.Compressed) != 0 || 
1043                        (info.Attributes & FileAttributes.Encrypted) != 0 ||
1044                        (info.Attributes & FileAttributes.SparseFile) != 0)
1045                    {
1046                        //Log the error
1047                        task.Log.Add(new LogEntry(S._("The file {0} could not be erased " +
1048                            "because the file was either compressed, encrypted or a sparse file.",
1049                            info.FullName), LogLevel.ERROR));
1050                    }
1051
1052                    //Create the file stream, and call the erasure method to write to
1053                    //the stream.
1054                    using (FileStream strm = info.Open(FileMode.Open, FileAccess.Write,
1055                        FileShare.None, FileOptions.WriteThrough))
1056                    {
1057                        //Set the end of the stream after the wrap-round the cluster size
1058                        strm.SetLength(GetFileArea(paths[i]));
1059
1060                        //If the stream is empty, there's nothing to overwrite. Continue
1061                        //to the next entry
1062                        if (strm.Length != 0)
1063                        {
1064                            //Then erase the file.
1065                            long itemWritten = 0,
1066                                 itemTotal = method.CalculateEraseDataSize(null, strm.Length);
1067                            method.Erase(strm, long.MaxValue,
1068                                PRNGManager.GetInstance(ManagerLibrary.Instance.Settings.ActivePRNG),
1069                                delegate(long lastWritten, int currentPass)
1070                                {
1071                                    dataTotal -= lastWritten;
1072                                    progress.Completed += lastWritten;
1073                                    progress.Event.CurrentItemPass = currentPass;
1074                                    progress.Event.CurrentItemProgress = (float)
1075                                        ((itemWritten += lastWritten) / (float)itemTotal);
1076                                    progress.Event.CurrentTargetProgress =
1077                                        (i + progress.Event.CurrentItemProgress) /
1078                                        (float)paths.Count;
1079                                    progress.Event.TimeLeft = progress.TimeLeft;
1080                                    task.OnProgressChanged(progress.Event);
1081
1082                                    lock (currentTask)
1083                                        if (currentTask.Cancelled)
1084                                            throw new FatalException(S._("The task was cancelled."));
1085                                }
1086                            );
1087                        }
1088
1089                        //Set the length of the file to 0.
1090                        strm.Seek(0, SeekOrigin.Begin);
1091                        strm.SetLength(0);
1092                    }
1093
1094                    //Remove the file.
1095                    FileInfo fileInfo = info.File;
1096                    if (fileInfo != null)
1097                        RemoveFile(fileInfo);
1098                }
1099                catch (UnauthorizedAccessException)
1100                {
1101                    task.Log.Add(new LogEntry(S._("The file {0} could not be erased because the " +
1102                        "file's permissions prevent access to the file.", info.FullName),
1103                        LogLevel.ERROR));
1104                }
1105                catch (FileLoadException)
1106                {
1107                    task.Log.Add(new LogEntry(S._("The file {0} could not be erased because the " +
1108                        "file is currently in use.", info.FullName), LogLevel.ERROR));
1109                }
1110                finally
1111                {
1112                    //Re-set the read-only flag
1113                    info.IsReadOnly = isReadOnly;
1114                }
1115            }
1116
1117            //If the user requested a folder removal, do it.
1118            if (target is Task.Folder)
1119            {
1120                Task.Folder fldr = (Task.Folder)target;
1121                if (fldr.DeleteIfEmpty)
1122                    RemoveFolder(new DirectoryInfo(fldr.Path));
1123            }
1124
1125            //If the user was erasing the recycle bin, clear the bin.
1126            if (target is Task.RecycleBin)
1127            {
1128                ShellAPI.SHEmptyRecycleBin(IntPtr.Zero, null,
1129                    ShellAPI.SHEmptyRecycleBinFlags.SHERB_NOCONFIRMATION |
1130                    ShellAPI.SHEmptyRecycleBinFlags.SHERB_NOPROGRESSUI |
1131                    ShellAPI.SHEmptyRecycleBinFlags.SHERB_NOSOUND);
1132            }
1133        }
1134
1135        /// <summary>
1136        /// Retrieves the size of the file on disk, calculated by the amount of
1137        /// clusters allocated by it.
1138        /// </summary>
1139        /// <param name="filePath">The path to the file.</param>
1140        /// <returns>The area of the file.</returns>
1141        private static long GetFileArea(string filePath)
1142        {
1143            StreamInfo info = new StreamInfo(filePath);
1144            VolumeInfo volume = VolumeInfo.FromMountpoint(info.Directory.FullName);
1145            long clusterSize = volume.ClusterSize;
1146            return (info.Length + (clusterSize - 1)) & ~(clusterSize - 1);
1147        }
1148        #endregion
1149
1150        /// <summary>
1151        /// Securely removes files.
1152        /// </summary>
1153        /// <param name="info">The FileInfo object representing the file.</param>
1154        private static void RemoveFile(FileInfo info)
1155        {
1156            //Set the date of the file to be invalid to prevent forensic
1157            //detection
1158            info.CreationTime = info.LastWriteTime = info.LastAccessTime =
1159                new DateTime(1980, 1, 1, 0, 0, 0);
1160            info.Attributes = FileAttributes.Normal;
1161            info.Attributes = FileAttributes.NotContentIndexed;
1162
1163            //Rename the file a few times to erase the entry from the file system
1164            //table.
1165            string newPath = info.DirectoryName + Path.DirectorySeparatorChar +
1166                GenerateRandomFileName(info.Name.Length);
1167            for (int i = 0, tries = 0; i < FilenameErasePasses; ++tries)
1168            {
1169                //Try to rename the file. If it fails, it is probably due to another
1170                //process locking the file. Defer, then rename again.
1171                try
1172                {
1173                    info.MoveTo(newPath);
1174                    ++i;
1175                }
1176                catch (IOException)
1177                {
1178                    Thread.Sleep(100);
1179
1180                    //If after FilenameEraseTries the file is still locked, some program is
1181                    //definitely using the file; throw an exception.
1182                    if (tries > FilenameEraseTries)
1183                        throw new IOException(S._("The file {0} is currently in use and " +
1184                            "cannot be removed.", info.FullName));
1185                }
1186            }
1187
1188            //If the user wants plausible deniability, find a random file on the same
1189            //volume and write it over.
1190            if (Manager.ManagerLibrary.Instance.Settings.PlausibleDeniability)
1191            {
1192                //Get the template file to copy
1193                FileInfo shadowFileInfo;
1194                {
1195                    string shadowFile = null;
1196                    List<string> entries = ManagerLibrary.Instance.Settings.PlausibleDeniabilityFiles.GetRange(
1197                        0, ManagerLibrary.Instance.Settings.PlausibleDeniabilityFiles.Count);
1198                    PRNG prng = PRNGManager.GetInstance(ManagerLibrary.Instance.Settings.ActivePRNG);
1199                    do
1200                    {
1201                        if (entries.Count == 0)
1202                            throw new FatalException(S._("Plausible deniability was selected, " +
1203                                "but no decoy files were found. The current file has been only " +
1204                                "replaced with random data."));
1205
1206                        int index = prng.Next(entries.Count - 1);
1207                        if ((System.IO.File.GetAttributes(entries[index]) & FileAttributes.Directory) != 0)
1208                        {
1209                            DirectoryInfo dir = new DirectoryInfo(entries[index]);
1210                            FileInfo[] files = dir.GetFiles("*", SearchOption.AllDirectories);
1211                            foreach (FileInfo f in files)
1212                                entries.Add(f.FullName);
1213                        }
1214                        else
1215                            shadowFile = entries[index];
1216
1217                        entries.RemoveAt(index);
1218                    }
1219                    while (shadowFile == null || shadowFile.Length == 0);
1220                    shadowFileInfo = new FileInfo(shadowFile);
1221                }
1222
1223                //Dump the copy (the first 4MB, or less, depending on the file size and available
1224                //user space)
1225                long amountToCopy = Math.Min(4 * 1024 * 1024, shadowFileInfo.Length);
1226                using (FileStream shadowFileStream = shadowFileInfo.OpenRead())
1227                using (FileStream destFileStream = info.OpenWrite())
1228                {
1229                    while (destFileStream.Position < amountToCopy)
1230                    {
1231                        byte[] buf = new byte[524288];
1232                        int bytesRead = shadowFileStream.Read(buf, 0, buf.Length);
1233
1234                        //Stop bothering if the input stream is at the end
1235                        if (bytesRead == 0)
1236                            break;
1237
1238                        //Dump the read contents onto the file to be deleted
1239                        destFileStream.Write(buf, 0,
1240                            (int)Math.Min(bytesRead, amountToCopy - destFileStream.Position));
1241                    }
1242                }
1243            }
1244
1245            //Then delete the file.
1246            for (int i = 0; i < FilenameEraseTries; ++i)
1247                try
1248                {
1249                    info.Delete();
1250                    break;
1251                }
1252                catch (IOException)
1253                {
1254                    if (i > FilenameEraseTries)
1255                        throw new IOException(S._("The file {0} is currently in use and " +
1256                            "cannot be removed.", info.FullName));
1257                    Thread.Sleep(100);
1258                }
1259        }
1260
1261        /// <summary>
1262        /// Removes the folder and all its contents.
1263        /// </summary>
1264        /// <param name="info">The folder to remove.</param>
1265        private static void RemoveFolder(DirectoryInfo info)
1266        {
1267            foreach (DirectoryInfo dir in info.GetDirectories())
1268                RemoveFolder(dir);
1269            foreach (FileInfo file in info.GetFiles())
1270                RemoveFile(file);
1271
1272            //Then clean up this folder.
1273            for (int i = 0; i < FilenameErasePasses; ++i)
1274            {
1275                //Rename the folder.
1276                string newPath = info.Parent.FullName + Path.DirectorySeparatorChar +
1277                    GenerateRandomFileName(info.Name.Length);
1278
1279                //Try to rename the file. If it fails, it is probably due to another
1280                //process locking the file. Defer, then rename again.
1281                try
1282                {
1283                    info.MoveTo(newPath);
1284                }
1285                catch (IOException)
1286                {
1287                    Thread.Sleep(100);
1288                    --i;
1289                }
1290            }
1291
1292            //Remove the folder
1293            info.Delete(true);
1294        }
1295
1296        /// <summary>
1297        /// Generates a random file name with the given length.
1298        /// </summary>
1299        /// <param name="length">The length of the file name to generate.</param>
1300        /// <returns>A random file name.</returns>
1301        private static string GenerateRandomFileName(int length)
1302        {
1303            //Get a random file name
1304            PRNG prng = PRNGManager.GetInstance(ManagerLibrary.Instance.Settings.ActivePRNG);
1305            byte[] newFileNameAry = new byte[length];
1306            prng.NextBytes(newFileNameAry);
1307
1308            //Validate the name
1309            string validFileNameChars = "0123456789abcdefghijklmnopqrstuvwxyz" +
1310                "ABCDEFGHIJKLMNOPQRSTUVWXYZ _+=-()[]{}',`~!";
1311            for (int j = 0, k = newFileNameAry.Length; j < k; ++j)
1312                newFileNameAry[j] = (byte)validFileNameChars[
1313                    (int)newFileNameAry[j] % validFileNameChars.Length];
1314
1315            return new System.Text.UTF8Encoding().GetString(newFileNameAry);
1316        }
1317
1318        /// <summary>
1319        /// Gets a random file from within the provided directory.
1320        /// </summary>
1321        /// <param name="info">The directory to get a random file name from.</param>
1322        /// <returns>A string containing the full path to the file.</returns>
1323        private static string GetRandomFileName(DirectoryInfo info)
1324        {
1325            //First retrieve the list of files and folders in the provided directory.
1326            FileSystemInfo[] entries = null;
1327            try
1328            {
1329                entries = info.GetFileSystemInfos();
1330            }
1331            catch (Exception)
1332            {
1333                return string.Empty;
1334            }
1335            if (entries.Length == 0)
1336                return string.Empty;
1337
1338            //Find a random entry.
1339            PRNG prng = PRNGManager.GetInstance(ManagerLibrary.Instance.Settings.ActivePRNG);
1340            string result = string.Empty;
1341            while (result.Length == 0)
1342            {
1343                int index = prng.Next(entries.Length - 1);
1344                if (entries[index] is DirectoryInfo)
1345                    result = GetRandomFileName((DirectoryInfo)entries[index]);
1346                else
1347                    result = ((FileInfo)entries[index]).FullName;
1348            }
1349
1350            return result;
1351        }
1352
1353        /// <summary>
1354        /// The thread object.
1355        /// </summary>
1356        private Thread thread;
1357
1358        /// <summary>
1359        /// The lock preventing concurrent access for the tasks list and the
1360        /// tasks queue.
1361        /// </summary>
1362        private object tasksLock = new object();
1363
1364        /// <summary>
1365        /// The list of tasks. Includes all immediate, reboot, and recurring tasks
1366        /// </summary>
1367        private Dictionary<uint, Task> tasks = new Dictionary<uint, Task>();
1368
1369        /// <summary>
1370        /// The queue of tasks. This queue is executed when the first element's
1371        /// timestamp (the key) has been past. This list assumes that all tasks
1372        /// are sorted by timestamp, smallest one first.
1373        /// </summary>
1374        private SortedList<DateTime, Task> scheduledTasks =
1375            new SortedList<DateTime, Task>();
1376
1377        /// <summary>
1378        /// The currently executing task.
1379        /// </summary>
1380        Task currentTask;
1381
1382        /// <summary>
1383        /// The list of task IDs for recycling.
1384        /// </summary>
1385        private List<uint> unusedIds = new List<uint>();
1386
1387        /// <summary>
1388        /// Lock preventing concurrent access for the IDs.
1389        /// </summary>
1390        private object unusedIdsLock = new object();
1391
1392        /// <summary>
1393        /// Incrementing ID. This value is incremented by one every time an ID
1394        /// is required by no unused IDs remain.
1395        /// </summary>
1396        private uint nextId = 0;
1397
1398        /// <summary>
1399        /// An automatically reset event allowing the addition of new tasks to
1400        /// interrupt the thread's sleeping state waiting for the next recurring
1401        /// task to be due.
1402        /// </summary>
1403        AutoResetEvent schedulerInterrupt = new AutoResetEvent(true);
1404    }
1405}
Note: See TracBrowser for help on using the repository browser.