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

Revision 260, 15.7 KB checked in by lowjoel, 7 years ago (diff)

-Change the RemoveFile? and RemoveFolder? functions to be static since they don't need the this reference
-Factor the random filename generator code out to a separate function
-Guard all MoveTo? files with an exception handler since antivirus programs may lock the file temporally

Line 
1using System;
2using System.Collections.Generic;
3using System.Collections.Specialized;
4using System.Text;
5using System.Threading;
6using System.IO;
7
8using Eraser.Util;
9using System.Runtime.Serialization;
10using System.Runtime.Serialization.Formatters.Binary;
11
12namespace Eraser.Manager
13{
14    /// <summary>
15    /// The DirectExecutor class is used by the Eraser GUI directly when the program
16    /// is run without the help of a Service.
17    /// </summary>
18    public class DirectExecutor : Executor, IDisposable
19    {
20        public DirectExecutor()
21        {
22            thread = new Thread(delegate()
23            {
24                Main();
25            });
26
27            thread.Start();
28            Thread.Sleep(0);
29        }
30
31        void IDisposable.Dispose()
32        {
33            thread.Abort();
34            schedulerInterrupt.Set();
35        }
36
37        public override void AddTask(ref Task task)
38        {
39            lock (unusedIdsLock)
40            {
41                if (unusedIds.Count != 0)
42                {
43                    task.id = unusedIds[0];
44                    unusedIds.RemoveAt(0);
45                }
46                else
47                    task.id = ++nextId;
48            }
49
50            //Set the executor of the task
51            task.executor = this;
52
53            //Add the task to the set of tasks
54            lock (tasksLock)
55            {
56                tasks.Add(task.ID, task);
57
58                //If the task is scheduled to run now, break the waiting thread and
59                //run it immediately
60                if (task.Schedule == Schedule.RunNow)
61                {
62                    QueueTask(task);
63                }
64                //If the task is scheduled, add the next execution time to the list
65                //of schduled tasks.
66                else if (task.Schedule != Schedule.RunOnRestart)
67                {
68                    scheduledTasks.Add((task.Schedule as RecurringSchedule).NextRun, task);
69                }
70            }
71        }
72
73        public override bool DeleteTask(uint taskId)
74        {
75            lock (tasksLock)
76            {
77                if (!tasks.ContainsKey(taskId))
78                    return false;
79
80                lock (unusedIdsLock)
81                    unusedIds.Add(taskId);
82                tasks.Remove(taskId);
83
84                for (int i = 0; i != scheduledTasks.Count; ++i)
85                    if (scheduledTasks.Values[i].id == taskId)
86                        scheduledTasks.RemoveAt(i);
87            }
88
89            return true;
90        }
91
92        public override void ReplaceTask(Task task)
93        {
94            lock (tasksLock)
95            {
96                //Replace the task in the global set
97                if (!tasks.ContainsKey(task.ID))
98                    return;
99
100                tasks[task.ID] = task;
101
102                //Then replace the task if it is in the queue
103                for (int i = 0; i != scheduledTasks.Count; ++i)
104                    if (scheduledTasks.Values[i].id == task.ID)
105                        scheduledTasks.Values[i] = task;
106            }
107        }
108
109        public override void QueueTask(Task task)
110        {
111            lock (tasksLock)
112            {
113                //Set the task variable to indicate that the task is already
114                //waiting to be executed.
115                task.queued = true;
116
117                //Queue the task to be run immediately.
118                scheduledTasks.Add(DateTime.Now, task);
119                schedulerInterrupt.Set();
120            }
121        }
122
123        public override void QueueRestartTasks()
124        {
125            lock (tasksLock)
126            {
127                foreach (Task task in tasks.Values)
128                    if (task.Schedule == Schedule.RunOnRestart)
129                        QueueTask(task);
130            }
131        }
132
133        public override void CancelTask(Task task)
134        {
135            lock (currentTask)
136            {
137                if (currentTask == task)
138                {
139                    currentTask.cancelled = true;
140                    return;
141                }
142            }
143
144            lock (tasksLock)
145                for (int i = 0; i != scheduledTasks.Count; ++i)
146                    if (scheduledTasks.Values[i] == task)
147                    {
148                        scheduledTasks.RemoveAt(i);
149                        return;
150                    }
151
152            throw new ArgumentOutOfRangeException("The task to be cancelled must " +
153                "either be currently executing or queued.");
154        }
155
156        public override Task GetTask(uint taskId)
157        {
158            lock (tasksLock)
159            {
160                if (!tasks.ContainsKey(taskId))
161                    return null;
162                return tasks[taskId];
163            }
164        }
165
166        public override List<Task> GetTasks()
167        {
168            lock (tasksLock)
169            {
170                Task[] result = new Task[tasks.Count];
171                tasks.Values.CopyTo(result, 0);
172                return new List<Task>(result);
173            }
174        }
175
176        public override void SaveTaskList(Stream stream)
177        {
178            lock (tasksLock)
179                new BinaryFormatter().Serialize(stream, tasks);
180        }
181
182        public override void LoadTaskList(Stream stream)
183        {
184            lock (tasksLock)
185            {
186                //Load the list into the dictionary
187                tasks = (Dictionary<uint, Task>)new BinaryFormatter().Deserialize(stream);
188
189                //Ignore the next portion if there are no tasks
190                if (tasks.Count == 0)
191                    return;
192
193                lock (unusedIdsLock)
194                {
195                    //Find gaps in the numbering
196                    nextId = 1;
197                    foreach (uint id in tasks.Keys)
198                    {
199                        while (id > nextId)
200                            unusedIds.Add(++nextId);
201                        ++nextId;
202                    }
203
204                    //Decrement the ID, since the next ID will be preincremented
205                    //before use.
206                    --nextId;
207                }
208            }
209        }
210
211        /// <summary>
212        /// The thread entry point for this object. This object operates on a queue
213        /// and hence the thread will sequentially execute tasks.
214        /// </summary>
215        private void Main()
216        {
217            //The waiting thread will utilize a polling loop to check for new
218            //scheduled tasks. This will be checked every 30 seconds. However,
219            //when the thread is waiting for a new task, it can be interrupted.
220            while (thread.ThreadState != ThreadState.AbortRequested)
221            {
222                //Check for a new task
223                Task task = null;
224                lock (tasksLock)
225                {
226                    if (scheduledTasks.Count != 0 &&
227                        (scheduledTasks.Values[0].Schedule == Schedule.RunNow ||
228                         scheduledTasks.Keys[0] <= DateTime.Now))
229                    {
230                        task = scheduledTasks.Values[0];
231                        scheduledTasks.RemoveAt(0);
232                    }
233                }
234
235                if (task != null)
236                {
237                    //Set the currently executing task.
238                    currentTask = task;
239
240                    try
241                    {
242                        //Broadcast the task started event.
243                        task.queued = false;
244                        task.cancelled = false;
245                        task.OnTaskStarted(new TaskEventArgs(task));
246
247                        //Run the task
248                        foreach (Task.ErasureTarget target in task.Targets)
249                            try
250                            {
251                                if (target is Task.UnusedSpace)
252                                    EraseUnusedSpace(task, (Task.UnusedSpace)target);
253                                else if (target is Task.FilesystemObject)
254                                    EraseFilesystemObject(task, (Task.FilesystemObject)target);
255                                else
256                                    throw new ArgumentException("Unknown erasure target.");
257                            }
258                            catch (FatalException)
259                            {
260                                throw;
261                            }
262                            catch (Exception e)
263                            {
264                                task.LogEntry(new LogEntry(e.Message, LogLevel.ERROR));
265                            }
266                    }
267                    catch (FatalException e)
268                    {
269                        task.LogEntry(new LogEntry(e.Message, LogLevel.FATAL));
270                    }
271                    finally
272                    {
273                        //If the task is a recurring task, reschedule it since we are done.
274                        if (task.Schedule is RecurringSchedule)
275                            ((RecurringSchedule)task.Schedule).Reschedule(DateTime.Now);
276
277                        //If the task is an execute on restart task, it is only run
278                        //once and can now be restored to an immediately executed task
279                        if (task.Schedule == Schedule.RunOnRestart)
280                            task.Schedule = Schedule.RunNow;
281
282                        //And the task finished event.
283                        task.OnTaskFinished(new TaskEventArgs(task));
284                    }
285                }
286
287                //Wait for half a minute to check for the next scheduled task.
288                schedulerInterrupt.WaitOne(30000, false);
289            }
290        }
291
292        /// <summary>
293        /// Executes a unused space erase.
294        /// </summary>
295        /// <param name="target">The target of the unused space erase.</param>
296        private void EraseUnusedSpace(Task task, Task.UnusedSpace target)
297        {
298            throw new NotImplementedException("Unused space erasures are not "+
299                "currently implemented");
300        }
301
302        /// <summary>
303        /// Erases a file or folder on the volume.
304        /// </summary>
305        /// <param name="target">The target of the erasure.</param>
306        private void EraseFilesystemObject(Task task, Task.FilesystemObject target)
307        {
308            //Retrieve the list of files to erase.
309            long totalSize = 0;
310            List<string> paths = target.GetPaths(out totalSize);
311            TaskProgressEventArgs eventArgs = new TaskProgressEventArgs(task, 0, 0);
312
313            //Get the erasure method if the user specified he wants the default.
314            ErasureMethod method = target.Method;
315            if (method == ErasureMethodManager.Default)
316                method = ErasureMethodManager.GetInstance(Globals.Settings.DefaultFileErasureMethod);
317
318            //Calculate the total amount of data required to finish the wipe. This
319            //value is just the total about of data to be erased multiplied by
320            //number of passes
321            totalSize = method.CalculateEraseDataSize(paths, totalSize);
322
323            //Record the start of the erasure pass so we can calculate speed of erasures
324            long totalLeft = totalSize;
325            long overallWriteSpeed = 0;
326            DateTime startTime = DateTime.Now;
327
328            //Iterate over every path, and erase the path.
329            for (int i = 0; i < paths.Count; ++i)
330            {
331                //Update the task progress
332                eventArgs.overallProgress = (i * 100) / paths.Count;
333                eventArgs.currentTarget = target;
334                eventArgs.currentItemName = paths[i];
335                eventArgs.currentItemProgress = 0;
336                eventArgs.totalPasses = method.Passes;
337                task.OnProgressChanged(eventArgs);
338
339                //Make sure the file does not have any attributes which may
340                //affect the erasure process
341                FileInfo info = new FileInfo(paths[i]);
342                if ((info.Attributes & FileAttributes.Compressed) != 0 ||
343                    (info.Attributes & FileAttributes.Encrypted) != 0 ||
344                    (info.Attributes & FileAttributes.SparseFile) != 0 ||
345                    (info.Attributes & FileAttributes.ReparsePoint) != 0)
346                {
347                    //Log the error
348                    throw new ArgumentException("Compressed, encrypted, or sparse" +
349                        "files cannot be erased with Eraser.");
350                }
351
352                //Remove the read-only flag, if it is set.
353                if ((info.Attributes & FileAttributes.ReadOnly) != 0)
354                    info.Attributes &= ~FileAttributes.ReadOnly;
355
356                //Create the file stream, and call the erasure method
357                //to write to the stream.
358                using (FileStream strm = new FileStream(info.FullName,
359                    FileMode.Open, FileAccess.Write, FileShare.None,
360                    8, FileOptions.WriteThrough))
361                {
362                    //Set the end of the stream after the wrap-round the cluster size
363                    uint clusterSize = Drive.GetClusterSize(info.Directory.Root.FullName);
364                    long roundUpFileLength = strm.Length % clusterSize;
365                    if (roundUpFileLength != 0)
366                        strm.SetLength(strm.Length + (clusterSize - roundUpFileLength));
367
368                    //Then erase the file.
369                    method.Erase(strm, long.MaxValue,
370                        PRNGManager.GetInstance(Globals.Settings.ActivePRNG),
371                        delegate(float currentProgress, int currentPass)
372                        {
373                            long amountWritten = (long)(currentProgress * (info.Length * method.Passes));
374                            if (overallWriteSpeed != 0)
375                                eventArgs.timeLeft = (int)((totalLeft - amountWritten) /
376                                    overallWriteSpeed);
377                            else if (amountWritten != 0 && (DateTime.Now - startTime).TotalSeconds != 0)
378                                overallWriteSpeed = (long)(amountWritten /
379                                    (DateTime.Now - startTime).TotalSeconds);
380
381                            eventArgs.currentPass = currentPass;
382                            eventArgs.currentItemProgress = (int)
383                                ((float)currentProgress * 100.0);
384                            eventArgs.overallProgress = (int)
385                                (((i + currentProgress) / (float)paths.Count) * 100);
386                            task.OnProgressChanged(eventArgs);
387
388                            lock (currentTask)
389                                if (currentTask.cancelled)
390                                    throw new FatalException("The task was cancelled.");
391                        }
392                    );
393
394                    //Update the speed-influencing statistics.
395                    totalLeft -= info.Length * method.Passes;
396                    overallWriteSpeed = (long)((totalSize - totalLeft) /
397                        (DateTime.Now - startTime).TotalSeconds);
398
399                    //Set the length of the file to 0.
400                    strm.Seek(0, SeekOrigin.Begin);
401                    strm.SetLength(0);
402                }
403
404                //Remove the file.
405                RemoveFile(info);
406            }
407
408            //If the user requested a folder removal, do it.
409            if (target is Task.Folder)
410            {
411                Task.Folder fldr = (Task.Folder)target;
412                if (fldr.DeleteIfEmpty)
413                    RemoveFolder(new DirectoryInfo(fldr.Path));
414            }
415        }
416
417        /// <summary>
418        /// Securely removes files.
419        /// </summary>
420        /// <param name="info">The FileInfo object representing the file.</param>
421        private static void RemoveFile(FileInfo info)
422        {
423            //Set the date of the file to be invalid to prevent forensic
424            //detection
425            info.CreationTime = info.LastWriteTime = info.LastAccessTime =
426                new DateTime(1800, 1, 1, 0, 0, 0);
427            info.Attributes = FileAttributes.Normal;
428            info.Attributes = FileAttributes.NotContentIndexed;
429
430            //Rename the file a few times to erase the record from the MFT.
431            for (int i = 0; i < FilenameErasePasses; ++i)
432            {
433                //Rename the file.
434                string newPath = info.DirectoryName + Path.DirectorySeparatorChar +
435                    GetRandomFileName(info.Name.Length);
436
437                //Try to rename the file. If it fails, it is probably due to another
438                //process locking the file. Defer, then rename again.
439                try
440                {
441                    info.MoveTo(newPath);
442                }
443                catch (IOException)
444                {
445                    Thread.Sleep(100);
446                    --i;
447                }
448            }
449
450            //Then delete the file.
451            info.Delete();
452        }
453
454        private static void RemoveFolder(DirectoryInfo info)
455        {
456            foreach (FileInfo file in info.GetFiles())
457                RemoveFile(file);
458            foreach (DirectoryInfo dir in info.GetDirectories())
459                RemoveFolder(dir);
460
461            //Then clean up this folder.
462            for (int i = 0; i < FilenameErasePasses; ++i)
463            {
464                //Rename the folder.
465                string newPath = info.Parent.FullName + Path.DirectorySeparatorChar +
466                    GetRandomFileName(info.Name.Length);
467
468                //Try to rename the file. If it fails, it is probably due to another
469                //process locking the file. Defer, then rename again.
470                try
471                {
472                    info.MoveTo(newPath);
473                }
474                catch (IOException)
475                {
476                    Thread.Sleep(100);
477                    --i;
478                }
479            }
480
481            //Remove the folder
482            info.Delete();
483        }
484
485        /// <summary>
486        /// Generates a random file name with the given length.
487        /// </summary>
488        /// <param name="length">The length of the file name to generate.</param>
489        /// <returns>A random file name.</returns>
490        private static string GetRandomFileName(int length)
491        {
492            //Get a random file name
493            PRNG prng = PRNGManager.GetInstance(Globals.Settings.ActivePRNG);
494            byte[] newFileNameAry = new byte[length];
495            prng.NextBytes(newFileNameAry);
496
497            //Validate the name
498            string validFileNameChars = "0123456789abcdefghijklmnopqrstuvwxyz" +
499                "ABCDEFGHIJKLMNOPQRSTUVWXYZ _+=-()[]{}',`~!";
500            for (int j = 0, k = newFileNameAry.Length; j < k; ++j)
501                newFileNameAry[j] = (byte)validFileNameChars[
502                    (int)newFileNameAry[j] % validFileNameChars.Length];
503
504            return new System.Text.UTF8Encoding().GetString(newFileNameAry);
505        }
506
507        /// <summary>
508        /// The thread object.
509        /// </summary>
510        private Thread thread;
511
512        /// <summary>
513        /// The lock preventing concurrent access for the tasks list and the
514        /// tasks queue.
515        /// </summary>
516        private object tasksLock = new object();
517
518        /// <summary>
519        /// The list of tasks. Includes all immediate, reboot, and recurring tasks
520        /// </summary>
521        private Dictionary<uint, Task> tasks = new Dictionary<uint, Task>();
522
523        /// <summary>
524        /// The queue of tasks. This queue is executed when the first element's
525        /// timestamp (the key) has been past. This list assumes that all tasks
526        /// are sorted by timestamp, smallest one first.
527        /// </summary>
528        private SortedList<DateTime, Task> scheduledTasks =
529            new SortedList<DateTime, Task>();
530
531        /// <summary>
532        /// The currently executing task.
533        /// </summary>
534        Task currentTask;
535
536        /// <summary>
537        /// The list of task IDs for recycling.
538        /// </summary>
539        private List<uint> unusedIds = new List<uint>();
540
541        /// <summary>
542        /// Lock preventing concurrent access for the IDs.
543        /// </summary>
544        private object unusedIdsLock = new object();
545
546        /// <summary>
547        /// Incrementing ID. This value is incremented by one every time an ID
548        /// is required by no unused IDs remain.
549        /// </summary>
550        private uint nextId = 0;
551
552        /// <summary>
553        /// An automatically reset event allowing the addition of new tasks to
554        /// interrupt the thread's sleeping state waiting for the next recurring
555        /// task to be due.
556        /// </summary>
557        AutoResetEvent schedulerInterrupt = new AutoResetEvent(true);
558    }
559}
Note: See TracBrowser for help on using the repository browser.