using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.Threading;
using System.IO;
using Eraser.Util;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
namespace Eraser.Manager
{
///
/// The DirectExecutor class is used by the Eraser GUI directly when the program
/// is run without the help of a Service.
///
public class DirectExecutor : Executor, IDisposable
{
public DirectExecutor()
{
thread = new Thread(delegate()
{
Main();
});
thread.Start();
Thread.Sleep(0);
}
void IDisposable.Dispose()
{
thread.Abort();
schedulerInterrupt.Set();
}
public override void AddTask(ref Task task)
{
lock (unusedIdsLock)
{
if (unusedIds.Count != 0)
{
task.id = unusedIds[0];
unusedIds.RemoveAt(0);
}
else
task.id = ++nextId;
}
//Set the executor of the task
task.executor = this;
//Add the task to the set of tasks
lock (tasksLock)
{
tasks.Add(task.ID, task);
//If the task is scheduled to run now, break the waiting thread and
//run it immediately
if (task.Schedule == Schedule.RunNow)
{
QueueTask(task);
}
//If the task is scheduled, add the next execution time to the list
//of schduled tasks.
else if (task.Schedule != Schedule.RunOnRestart)
{
scheduledTasks.Add((task.Schedule as RecurringSchedule).NextRun, task);
}
}
}
public override bool DeleteTask(uint taskId)
{
lock (tasksLock)
{
if (!tasks.ContainsKey(taskId))
return false;
lock (unusedIdsLock)
unusedIds.Add(taskId);
tasks.Remove(taskId);
for (int i = 0; i != scheduledTasks.Count; ++i)
if (scheduledTasks.Values[i].id == taskId)
scheduledTasks.RemoveAt(i);
}
return true;
}
public override void ReplaceTask(Task task)
{
lock (tasksLock)
{
//Replace the task in the global set
if (!tasks.ContainsKey(task.ID))
return;
tasks[task.ID] = task;
//Then replace the task if it is in the queue
for (int i = 0; i != scheduledTasks.Count; ++i)
if (scheduledTasks.Values[i].id == task.ID)
scheduledTasks.Values[i] = task;
}
}
public override void QueueTask(Task task)
{
lock (tasksLock)
{
//Set the task variable to indicate that the task is already
//waiting to be executed.
task.queued = true;
//Queue the task to be run immediately.
scheduledTasks.Add(DateTime.Now, task);
schedulerInterrupt.Set();
}
}
public override void QueueRestartTasks()
{
lock (tasksLock)
{
foreach (Task task in tasks.Values)
if (task.Schedule == Schedule.RunOnRestart)
QueueTask(task);
}
}
public override void CancelTask(Task task)
{
lock (currentTask)
{
if (currentTask == task)
{
currentTask.cancelled = true;
return;
}
}
lock (tasksLock)
for (int i = 0; i != scheduledTasks.Count; ++i)
if (scheduledTasks.Values[i] == task)
{
scheduledTasks.RemoveAt(i);
return;
}
throw new ArgumentOutOfRangeException("The task to be cancelled must " +
"either be currently executing or queued.");
}
public override Task GetTask(uint taskId)
{
lock (tasksLock)
{
if (!tasks.ContainsKey(taskId))
return null;
return tasks[taskId];
}
}
public override List GetTasks()
{
lock (tasksLock)
{
Task[] result = new Task[tasks.Count];
tasks.Values.CopyTo(result, 0);
return new List(result);
}
}
public override void SaveTaskList(Stream stream)
{
lock (tasksLock)
new BinaryFormatter().Serialize(stream, tasks);
}
public override void LoadTaskList(Stream stream)
{
lock (tasksLock)
{
//Load the list into the dictionary
tasks = (Dictionary)new BinaryFormatter().Deserialize(stream);
//Ignore the next portion if there are no tasks
if (tasks.Count == 0)
return;
lock (unusedIdsLock)
{
//Find gaps in the numbering
nextId = 1;
foreach (uint id in tasks.Keys)
{
while (id > nextId)
unusedIds.Add(++nextId);
++nextId;
}
//Decrement the ID, since the next ID will be preincremented
//before use.
--nextId;
}
}
}
///
/// The thread entry point for this object. This object operates on a queue
/// and hence the thread will sequentially execute tasks.
///
private void Main()
{
//The waiting thread will utilize a polling loop to check for new
//scheduled tasks. This will be checked every 30 seconds. However,
//when the thread is waiting for a new task, it can be interrupted.
while (thread.ThreadState != ThreadState.AbortRequested)
{
//Check for a new task
Task task = null;
lock (tasksLock)
{
if (scheduledTasks.Count != 0 &&
(scheduledTasks.Values[0].Schedule == Schedule.RunNow ||
scheduledTasks.Keys[0] <= DateTime.Now))
{
task = scheduledTasks.Values[0];
scheduledTasks.RemoveAt(0);
}
}
if (task != null)
{
//Set the currently executing task.
currentTask = task;
try
{
//Broadcast the task started event.
task.queued = false;
task.cancelled = false;
task.OnTaskStarted(new TaskEventArgs(task));
//Run the task
foreach (Task.ErasureTarget target in task.Targets)
try
{
if (target is Task.UnusedSpace)
EraseUnusedSpace(task, (Task.UnusedSpace)target);
else if (target is Task.FilesystemObject)
EraseFilesystemObject(task, (Task.FilesystemObject)target);
else
throw new ArgumentException("Unknown erasure target.");
}
catch (FatalException)
{
throw;
}
catch (Exception e)
{
task.LogEntry(new LogEntry(e.Message, LogLevel.ERROR));
}
}
catch (FatalException e)
{
task.LogEntry(new LogEntry(e.Message, LogLevel.FATAL));
}
finally
{
//If the task is a recurring task, reschedule it since we are done.
if (task.Schedule is RecurringSchedule)
((RecurringSchedule)task.Schedule).Reschedule(DateTime.Now);
//If the task is an execute on restart task, it is only run
//once and can now be restored to an immediately executed task
if (task.Schedule == Schedule.RunOnRestart)
task.Schedule = Schedule.RunNow;
//And the task finished event.
task.OnTaskFinished(new TaskEventArgs(task));
}
}
//Wait for half a minute to check for the next scheduled task.
schedulerInterrupt.WaitOne(30000, false);
}
}
///
/// Executes a unused space erase.
///
/// The target of the unused space erase.
private void EraseUnusedSpace(Task task, Task.UnusedSpace target)
{
throw new NotImplementedException("Unused space erasures are not "+
"currently implemented");
}
///
/// Erases a file or folder on the volume.
///
/// The target of the erasure.
private void EraseFilesystemObject(Task task, Task.FilesystemObject target)
{
//Retrieve the list of files to erase.
long totalSize = 0;
List paths = target.GetPaths(out totalSize);
TaskProgressEventArgs eventArgs = new TaskProgressEventArgs(task, 0, 0);
//Get the erasure method if the user specified he wants the default.
ErasureMethod method = target.Method;
if (method == ErasureMethodManager.Default)
method = ErasureMethodManager.GetInstance(Globals.Settings.DefaultFileErasureMethod);
//Calculate the total amount of data required to finish the wipe. This
//value is just the total about of data to be erased multiplied by
//number of passes
totalSize = method.CalculateEraseDataSize(paths, totalSize);
//Record the start of the erasure pass so we can calculate speed of erasures
long totalLeft = totalSize;
long overallWriteSpeed = 0;
DateTime startTime = DateTime.Now;
//Iterate over every path, and erase the path.
for (int i = 0; i < paths.Count; ++i)
{
//Update the task progress
eventArgs.overallProgress = (i * 100) / paths.Count;
eventArgs.currentTarget = target;
eventArgs.currentItemName = paths[i];
eventArgs.currentItemProgress = 0;
eventArgs.totalPasses = method.Passes;
task.OnProgressChanged(eventArgs);
//Make sure the file does not have any attributes which may
//affect the erasure process
FileInfo info = new FileInfo(paths[i]);
if ((info.Attributes & FileAttributes.Compressed) != 0 ||
(info.Attributes & FileAttributes.Encrypted) != 0 ||
(info.Attributes & FileAttributes.SparseFile) != 0 ||
(info.Attributes & FileAttributes.ReparsePoint) != 0)
{
//Log the error
throw new ArgumentException("Compressed, encrypted, or sparse" +
"files cannot be erased with Eraser.");
}
//Remove the read-only flag, if it is set.
if ((info.Attributes & FileAttributes.ReadOnly) != 0)
info.Attributes &= ~FileAttributes.ReadOnly;
//Create the file stream, and call the erasure method
//to write to the stream.
using (FileStream strm = new FileStream(info.FullName,
FileMode.Open, FileAccess.Write, FileShare.None,
8, FileOptions.WriteThrough))
{
//Set the end of the stream after the wrap-round the cluster size
uint clusterSize = Drive.GetClusterSize(info.Directory.Root.FullName);
long roundUpFileLength = strm.Length % clusterSize;
if (roundUpFileLength != 0)
strm.SetLength(strm.Length + (clusterSize - roundUpFileLength));
//Then erase the file.
method.Erase(strm, long.MaxValue,
PRNGManager.GetInstance(Globals.Settings.ActivePRNG),
delegate(float currentProgress, int currentPass)
{
long amountWritten = (long)(currentProgress * (info.Length * method.Passes));
if (overallWriteSpeed != 0)
eventArgs.timeLeft = (int)((totalLeft - amountWritten) /
overallWriteSpeed);
else if (amountWritten != 0 && (DateTime.Now - startTime).TotalSeconds != 0)
overallWriteSpeed = (long)(amountWritten /
(DateTime.Now - startTime).TotalSeconds);
eventArgs.currentPass = currentPass;
eventArgs.currentItemProgress = (int)
((float)currentProgress * 100.0);
eventArgs.overallProgress = (int)
(((i + currentProgress) / (float)paths.Count) * 100);
task.OnProgressChanged(eventArgs);
lock (currentTask)
if (currentTask.cancelled)
throw new FatalException("The task was cancelled.");
}
);
//Update the speed-influencing statistics.
totalLeft -= info.Length * method.Passes;
overallWriteSpeed = (long)((totalSize - totalLeft) /
(DateTime.Now - startTime).TotalSeconds);
//Set the length of the file to 0.
strm.Seek(0, SeekOrigin.Begin);
strm.SetLength(0);
}
//Remove the file.
RemoveFile(info);
}
//If the user requested a folder removal, do it.
if (target is Task.Folder)
{
Task.Folder fldr = (Task.Folder)target;
if (fldr.DeleteIfEmpty)
RemoveFolder(new DirectoryInfo(fldr.Path));
}
}
///
/// Securely removes files.
///
/// The FileInfo object representing the file.
private static void RemoveFile(FileInfo info)
{
//Set the date of the file to be invalid to prevent forensic
//detection
info.CreationTime = info.LastWriteTime = info.LastAccessTime =
new DateTime(1800, 1, 1, 0, 0, 0);
info.Attributes = FileAttributes.Normal;
info.Attributes = FileAttributes.NotContentIndexed;
//Rename the file a few times to erase the record from the MFT.
for (int i = 0; i < FilenameErasePasses; ++i)
{
//Rename the file.
string newPath = info.DirectoryName + Path.DirectorySeparatorChar +
GetRandomFileName(info.Name.Length);
//Try to rename the file. If it fails, it is probably due to another
//process locking the file. Defer, then rename again.
try
{
info.MoveTo(newPath);
}
catch (IOException)
{
Thread.Sleep(100);
--i;
}
}
//Then delete the file.
info.Delete();
}
private static void RemoveFolder(DirectoryInfo info)
{
foreach (FileInfo file in info.GetFiles())
RemoveFile(file);
foreach (DirectoryInfo dir in info.GetDirectories())
RemoveFolder(dir);
//Then clean up this folder.
for (int i = 0; i < FilenameErasePasses; ++i)
{
//Rename the folder.
string newPath = info.Parent.FullName + Path.DirectorySeparatorChar +
GetRandomFileName(info.Name.Length);
//Try to rename the file. If it fails, it is probably due to another
//process locking the file. Defer, then rename again.
try
{
info.MoveTo(newPath);
}
catch (IOException)
{
Thread.Sleep(100);
--i;
}
}
//Remove the folder
info.Delete();
}
///
/// Generates a random file name with the given length.
///
/// The length of the file name to generate.
/// A random file name.
private static string GetRandomFileName(int length)
{
//Get a random file name
PRNG prng = PRNGManager.GetInstance(Globals.Settings.ActivePRNG);
byte[] newFileNameAry = new byte[length];
prng.NextBytes(newFileNameAry);
//Validate the name
string validFileNameChars = "0123456789abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ _+=-()[]{}',`~!";
for (int j = 0, k = newFileNameAry.Length; j < k; ++j)
newFileNameAry[j] = (byte)validFileNameChars[
(int)newFileNameAry[j] % validFileNameChars.Length];
return new System.Text.UTF8Encoding().GetString(newFileNameAry);
}
///
/// The thread object.
///
private Thread thread;
///
/// The lock preventing concurrent access for the tasks list and the
/// tasks queue.
///
private object tasksLock = new object();
///
/// The list of tasks. Includes all immediate, reboot, and recurring tasks
///
private Dictionary tasks = new Dictionary();
///
/// The queue of tasks. This queue is executed when the first element's
/// timestamp (the key) has been past. This list assumes that all tasks
/// are sorted by timestamp, smallest one first.
///
private SortedList scheduledTasks =
new SortedList();
///
/// The currently executing task.
///
Task currentTask;
///
/// The list of task IDs for recycling.
///
private List unusedIds = new List();
///
/// Lock preventing concurrent access for the IDs.
///
private object unusedIdsLock = new object();
///
/// Incrementing ID. This value is incremented by one every time an ID
/// is required by no unused IDs remain.
///
private uint nextId = 0;
///
/// An automatically reset event allowing the addition of new tasks to
/// interrupt the thread's sleeping state waiting for the next recurring
/// task to be due.
///
AutoResetEvent schedulerInterrupt = new AutoResetEvent(true);
}
}