source: trunk/eraser/Eraser/Program.cs @ 2003

Revision 2003, 21.7 KB checked in by lowjoel, 5 years ago (diff)

Document the use of response files.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1/*
2 * $Id$
3 * Copyright 2008-2010 The Eraser Project
4 * Original Author: Joel Low <lowjoel@users.sourceforge.net>
5 * Modified By:
6 *
7 * This file is part of Eraser.
8 *
9 * Eraser is free software: you can redistribute it and/or modify it under the
10 * terms of the GNU General Public License as published by the Free Software
11 * Foundation, either version 3 of the License, or (at your option) any later
12 * version.
13 *
14 * Eraser is distributed in the hope that it will be useful, but WITHOUT ANY
15 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 *
18 * A copy of the GNU General Public License can be found at
19 * <http://www.gnu.org/licenses/>.
20 */
21
22using System;
23using System.Collections.Generic;
24using System.Windows.Forms;
25
26using System.IO;
27using System.Threading;
28using System.Globalization;
29using System.ComponentModel;
30using System.Runtime.Serialization;
31using System.Security.Principal;
32using System.Text.RegularExpressions;
33
34using System.Reflection;
35using System.Diagnostics;
36
37using ComLib.Arguments;
38
39using Eraser.Manager;
40using Eraser.Util;
41
42namespace Eraser
43{
44    internal static partial class Program
45    {
46        /// <summary>
47        /// The common program arguments shared between the GUI and console programs.
48        /// </summary>
49        class Arguments
50        {
51            /// <summary>
52            /// True if the program should not be started with any user-visible interfaces.
53            /// </summary>
54            /// <remarks>Errors will also be silently ignored.</remarks>
55            [Arg("quiet", "The program should not be started with any user-visible interfaces. " +
56                "Errors will be silently ignored.", typeof(bool), false, false, null)]
57            public bool Quiet { get; set; }
58        }
59
60        /// <summary>
61        /// Program arguments which only apply to the GUI program.
62        /// </summary>
63        class GuiArguments : Arguments
64        {
65            /// <summary>
66            /// True if the command line specified atRestart, which should result in the
67            /// queueing of tasks meant for running at restart.
68            /// </summary>
69            [Arg("atRestart", "The program should queue all tasks scheduled for running at " +
70                "the system restart.", typeof(bool), false, false, null)]
71            public bool AtRestart { get; set; }
72        }
73
74        class ConsoleArguments : Arguments
75        {
76            /// <summary>
77            /// The Action which this handler is in charge of.
78            /// </summary>
79            [Arg(0, "The action this command line is stating.", typeof(string), true, null, null)]
80            public string Action { get; set; }
81
82            /// <summary>
83            /// The list of command line parameters not placed in a switch.
84            /// </summary>
85            public List<string> PositionalArguments { get; set; }
86        }
87
88        class AddTaskArguments : ConsoleArguments
89        {
90            /// <summary>
91            /// The erasure method which the user specified on the command line.
92            /// </summary>
93            [Arg("method", "The erasure method to use", typeof(Guid), false, null, null)]
94            public Guid ErasureMethod { get; set; }
95
96            /// <summary>
97            /// The schedule for the current set of targets.
98            /// </summary>
99            [Arg("schedule", "The schedule to use", typeof(Schedule), false, null, null)]
100            public string Schedule { get; set; }
101        }
102
103        /// <summary>
104        /// The main entry point for the application.
105        /// </summary>
106        [STAThread]
107        static int Main(string[] rawCommandLine)
108        {
109            //Immediately parse command line arguments. Start by substituting all
110            //response files ("@filename") arguments with the arguments found in the
111            //file
112            List<string> commandLine = new List<string>();
113            foreach (string argument in rawCommandLine)
114            {
115                if (argument[0] == '@' && File.Exists(argument.Substring(1)))
116                {
117                    //The current parameter is a response file, parse the file
118                    //for arguments and substitute it.
119                    using (TextReader reader = new StreamReader(argument.Substring(1)))
120                    {
121                        commandLine.AddRange(Shell.ParseCommandLine(reader.ReadToEnd()));
122                    }
123                }
124            }
125
126            string[] finalCommandLine = commandLine.ToArray();
127            ComLib.BoolMessageItem argumentParser = Args.Parse(finalCommandLine,
128                CommandLinePrefixes, CommandLineSeparators);
129            Args parsedArguments = (Args)argumentParser.Item;
130
131            //We default to a GUI if:
132            // - The parser did not succeed.
133            // - The parser resulted in an empty arguments list
134            // - The parser's argument at index 0 is not equal to the first argument (this
135            //   is when the user is passing GUI options -- command line options always
136            //   start with the action, e.g. Eraser help, or Eraser addtask
137            if (!argumentParser.Success || parsedArguments.IsEmpty ||
138                parsedArguments.Positional.Count == 0 ||
139                parsedArguments.Positional[0] != parsedArguments.Raw[0])
140            {
141                GUIMain(finalCommandLine);
142            }
143            else
144            {
145                return CommandMain(finalCommandLine);
146            }
147
148            //Return zero to signify success
149            return 0;
150        }
151
152        #region Console Program code
153        /// <summary>
154        /// Runs Eraser as a command-line application.
155        /// </summary>
156        /// <param name="commandLine">The command line parameters passed to Eraser.</param>
157        private static int CommandMain(string[] commandLine)
158        {
159            using (ConsoleProgram program = new ConsoleProgram(commandLine))
160            using (ManagerLibrary library = new ManagerLibrary(new Settings()))
161                try
162                {
163                    program.Handlers.Add("help",
164                        new ConsoleActionData(CommandHelp, new ConsoleArguments()));
165                    program.Handlers.Add("querymethods",
166                        new ConsoleActionData(CommandQueryMethods, new ConsoleArguments()));
167                    program.Handlers.Add("addtask",
168                        new ConsoleActionData(CommandAddTask, new AddTaskArguments()));
169                    program.Handlers.Add("importtasklist",
170                        new ConsoleActionData(CommandImportTaskList, new ConsoleArguments()));
171                    program.Run();
172                    return 0;
173                }
174                catch (UnauthorizedAccessException)
175                {
176                    return Win32ErrorCode.AccessDenied;
177                }
178                catch (Win32Exception e)
179                {
180                    Console.WriteLine(e.Message);
181                    return e.ErrorCode;
182                }
183                catch (Exception e)
184                {
185                    Console.WriteLine(e.Message);
186                    return 1;
187                }
188        }
189
190        /// <summary>
191        /// Prints the command line help for Eraser.
192        /// </summary>
193        private static void PrintCommandHelp()
194        {
195            Console.WriteLine(@"usage: Eraser <action> <arguments>
196where action is
197  help                Show this help message.
198  addtask             Adds tasks to the current task list.
199  querymethods        Lists all registered Erasure methods.
200  importtasklist      Imports an Eraser Task list to the current user's Task
201                      List.
202
203global parameters:
204  /quiet              Do not create a Console window to display progress.
205
206parameters for help:
207  eraser help
208
209  no parameters to set.
210
211parameters for addtask:
212  eraser addtask [/method=<methodGUID>] [/schedule=(now|manually|restart)] (recyclebin | unused=<volume> | dir=<directory> | file=<file>)[...]
213
214  /method             The Erasure method to use.
215  /schedule           The schedule the task will follow. The value must be one
216                      of:
217      now             The task will be queued for immediate execution.
218      manually        The task will be created but not queued for execution.
219      restart         The task will be queued for execution when the computer
220                      is next restarted.
221  recyclebin          Erases files and folders in the recycle bin
222  unused              Erases unused space in the volume.
223    optional arguments: unused=<drive>[,clusterTips[=(true|false)]]
224      clusterTips     If specified, the drive's files will have their
225                      cluster tips erased. This parameter accepts a Boolean
226                      value (true/false) as an argument; if none is specified
227                      true is assumed.
228  dir                 Erases files and folders in the directory
229    optional arguments: dir=<directory>[,-excludeMask][,+includeMask][,deleteIfEmpty]
230      excludeMask     A wildcard expression for files and folders to
231                      exclude.
232      includeMask     A wildcard expression for files and folders to
233                      include.
234                      The include mask is applied before the exclude mask.
235      deleteIfEmpty   Deletes the folder at the end of the erasure if it is
236                      empty.
237  file                Erases the specified file
238
239parameters for querymethods:
240  eraser querymethods
241
242  no parameters to set.
243
244parameters for importtasklist:
245  eraser importtasklist (file)[...]
246
247  [file]              A list of one or more files to import.
248
249All arguments are case sensitive.
250
251Response files can be used for very long command lines (generally, anything
252involving more than 32,000 characters.) Response files are used by prepending
253""@"" to the path to the file, and passing it into the command line. The
254contents of the response files' will be substituted at the same position into
255the command line.");
256            Console.Out.Flush();
257        }
258
259        /// <summary>
260        /// Prints the help text for Eraser (with copyright)
261        /// </summary>
262        /// <param name="arguments">Not used.</param>
263        private static void CommandHelp(ConsoleArguments arguments)
264        {
265            Console.WriteLine(@"Eraser {0}
266(c) 2008-2010 The Eraser Project
267Eraser is Open-Source Software: see http://eraser.heidi.ie/ for details.
268", Assembly.GetExecutingAssembly().GetName().Version);
269
270            PrintCommandHelp();
271        }
272
273        /// <summary>
274        /// Lists all registered erasure methods.
275        /// </summary>
276        /// <param name="arguments">Not used.</param>
277        private static void CommandQueryMethods(ConsoleArguments arguments)
278        {
279            //Output the header
280            const string methodFormat = "{0,-2} {1,-39} {2}";
281            Console.WriteLine(methodFormat, "", "Method", "GUID");
282            Console.WriteLine(new string('-', 79));
283
284            //Refresh the list of erasure methods
285            foreach (ErasureMethod method in ManagerLibrary.Instance.ErasureMethodRegistrar)
286            {
287                Console.WriteLine(methodFormat, (method is UnusedSpaceErasureMethod) ?
288                    "U" : "", method.Name, method.Guid.ToString());
289            }
290        }
291
292        /// <summary>
293        /// Parses the command line for tasks and adds them using the
294        /// <see cref="RemoteExecutor"/> class.
295        /// </summary>
296        /// <param name="arg">The command line parameters passed to the program.</param>
297        private static void CommandAddTask(ConsoleArguments arg)
298        {
299            AddTaskArguments arguments = (AddTaskArguments)arg;
300
301            //Create the task then set the method as well as schedule
302            Task task = new Task();
303            ErasureMethod method = arguments.ErasureMethod == Guid.Empty ?
304                ErasureMethodRegistrar.Default :
305                ManagerLibrary.Instance.ErasureMethodRegistrar[arguments.ErasureMethod];
306            switch (arguments.Schedule.ToUpperInvariant())
307            {
308                case "":
309                case "NOW":
310                    task.Schedule = Schedule.RunNow;
311                    break;
312                case "MANUALLY":
313                    task.Schedule = Schedule.RunManually;
314                    break;
315                case "RESTART":
316                    task.Schedule = Schedule.RunOnRestart;
317                    break;
318                default:
319                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
320                        "Unknown schedule type: {0}", arguments.Schedule), "/schedule");
321            }
322
323            //Parse the rest of the command line parameters as target expressions.
324            List<string> trueValues = new List<string>(new string[] { "yes", "true" });
325            string[] strings = new string[] {
326                //The recycle bin target
327                "(?<recycleBin>recyclebin)",
328
329                //The unused space erasure target, taking the optional clusterTips
330                //argument which defaults to true; if none is specified it's assumed
331                //false
332                "unused=(?<unusedVolume>.*)(?<unusedTips>,clusterTips(=(?<unusedTipsValue>true|false))?)?",
333
334                //The directory target, taking a list of + and - wildcard expressions.
335                "dir=(?<directoryName>.*)(?<directoryParams>(?<directoryExcludeMask>,-[^,]+)|(?<directoryIncludeMask>,\\+[^,]+)|(?<directoryDeleteIfEmpty>,deleteIfEmpty(=(?<directoryDeleteIfEmptyValue>true|false))?))*",
336
337                //The file target.
338                "file=(?<fileName>.*)"
339            };
340
341            Regex regex = new Regex(string.Join("|", strings),
342                RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.RightToLeft);
343            foreach (string argument in arguments.PositionalArguments)
344            {
345                Match match = regex.Match(argument);
346                if (match.Captures.Count == 0)
347                {
348                    Console.WriteLine("Unknown argument: {0}, skipped.", argument);
349                    continue;
350                }
351
352                ErasureTarget target = null;
353                if (match.Groups["recycleBin"].Success)
354                {
355                    target = new RecycleBinTarget();
356                }
357                else if (match.Groups["unusedVolume"].Success)
358                {
359                    UnusedSpaceTarget unusedSpaceTarget = new UnusedSpaceTarget();
360                    target = unusedSpaceTarget;
361                    unusedSpaceTarget.Drive = match.Groups["unusedVolume"].Value;
362
363                    if (!match.Groups["unusedTips"].Success)
364                        unusedSpaceTarget.EraseClusterTips = false;
365                    else if (!match.Groups["unusedTipsValue"].Success)
366                        unusedSpaceTarget.EraseClusterTips = true;
367                    else
368                        unusedSpaceTarget.EraseClusterTips =
369                            trueValues.IndexOf(match.Groups["unusedTipsValue"].Value) != -1;
370                }
371                else if (match.Groups["directoryName"].Success)
372                {
373                    FolderTarget folderTarget = new FolderTarget();
374                    target = folderTarget;
375
376                    folderTarget.Path = match.Groups["directoryName"].Value;
377                    if (!match.Groups["directoryDeleteIfEmpty"].Success)
378                        folderTarget.DeleteIfEmpty = false;
379                    else if (!match.Groups["directoryDeleteIfEmptyValue"].Success)
380                        folderTarget.DeleteIfEmpty = true;
381                    else
382                        folderTarget.DeleteIfEmpty =
383                            trueValues.IndexOf(match.Groups["directoryDeleteIfEmptyValue"].Value) != -1;
384                    if (match.Groups["directoryExcludeMask"].Success)
385                        folderTarget.ExcludeMask += match.Groups["directoryExcludeMask"].Value.Remove(0, 2) + ' ';
386                    if (match.Groups["directoryIncludeMask"].Success)
387                        folderTarget.IncludeMask += match.Groups["directoryIncludeMask"].Value.Remove(0, 2) + ' ';
388                }
389                else if (match.Groups["fileName"].Success)
390                {
391                    FileTarget fileTarget = new FileTarget();
392                    target = fileTarget;
393                    fileTarget.Path = match.Groups["fileName"].Value;
394                }
395
396                if (target == null)
397                    continue;
398
399                target.Method = method;
400                task.Targets.Add(target);
401            }
402
403            //Check the number of tasks in the task.
404            if (task.Targets.Count == 0)
405                throw new ArgumentException("Tasks must contain at least one erasure target.");
406
407            //Send the task out.
408            try
409            {
410                using (RemoteExecutorClient client = new RemoteExecutorClient())
411                {
412                    client.Run();
413                    if (!client.IsConnected)
414                    {
415                        //The client cannot connect to the server. This probably means
416                        //that the server process isn't running. Start an instance.
417                        Process eraserInstance = Process.Start(
418                            Assembly.GetExecutingAssembly().Location, "/quiet");
419                        Thread.Sleep(0);
420                        eraserInstance.WaitForInputIdle();
421
422                        client.Run();
423                        if (!client.IsConnected)
424                            throw new IOException("Eraser cannot connect to the running " +
425                                "instance for erasures.");
426                    }
427
428                    client.Tasks.Add(task);
429                }
430            }
431            catch (UnauthorizedAccessException e)
432            {
433                //We can't connect to the pipe because the other instance of Eraser
434                //is running with higher privileges than this instance.
435                throw new UnauthorizedAccessException("Another instance of Eraser " +
436                    "is already running but it is running with higher privileges than " +
437                    "this instance of Eraser. Tasks cannot be added in this manner.\n\n" +
438                    "Close the running instance of Eraser and start it again without " +
439                    "administrator privileges, or run the command again as an " +
440                    "administrator.", e);
441            }
442        }
443
444        /// <summary>
445        /// Imports the given tasklists and adds them to the global Eraser instance.
446        /// </summary>
447        /// <param name="args">The list of files specified on the command line.</param>
448        private static void CommandImportTaskList(ConsoleArguments args)
449        {
450            //Import the task list
451            try
452            {
453                using (RemoteExecutorClient client = new RemoteExecutorClient())
454                {
455                    client.Run();
456                    if (!client.IsConnected)
457                    {
458                        //The client cannot connect to the server. This probably means
459                        //that the server process isn't running. Start an instance.
460                        Process eraserInstance = Process.Start(
461                            Assembly.GetExecutingAssembly().Location, "/quiet");
462                        eraserInstance.WaitForInputIdle();
463
464                        client.Run();
465                        if (!client.IsConnected)
466                            throw new IOException("Eraser cannot connect to the running " +
467                                "instance for erasures.");
468                    }
469
470                    foreach (string path in args.PositionalArguments)
471                        using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read))
472                            client.Tasks.LoadFromStream(stream);
473                }
474            }
475            catch (UnauthorizedAccessException e)
476            {
477                //We can't connect to the pipe because the other instance of Eraser
478                //is running with higher privileges than this instance.
479                throw new UnauthorizedAccessException("Another instance of Eraser " +
480                    "is already running but it is running with higher privileges than " +
481                    "this instance of Eraser. Tasks cannot be added in this manner.\n\n" +
482                    "Close the running instance of Eraser and start it again without " +
483                    "administrator privileges, or run the command again as an " +
484                    "administrator.", e);
485            }
486        }
487        #endregion
488
489        #region GUI Program code
490        /// <summary>
491        /// Runs Eraser as a GUI application.
492        /// </summary>
493        /// <param name="commandLine">The command line parameters passed to Eraser.</param>
494        private static void GUIMain(string[] commandLine)
495        {
496            //Create a unique program instance ID for this user.
497            string instanceId = "Eraser-BAD0DAC6-C9EE-4acc-8701-C9B3C64BC65E-GUI-" +
498                WindowsIdentity.GetCurrent().User.ToString();
499
500            //Then initialise the instance and initialise the Manager library.
501            using (GuiProgram program = new GuiProgram(commandLine, instanceId))
502            using (ManagerLibrary library = new ManagerLibrary(new Settings()))
503            {
504                program.InitInstance += OnGUIInitInstance;
505                program.NextInstance += OnGUINextInstance;
506                program.ExitInstance += OnGUIExitInstance;
507                program.Run();
508            }
509        }
510
511        /// <summary>
512        /// Triggered when the Program is started for the first time.
513        /// </summary>
514        /// <param name="sender">The sender of the object.</param>
515        /// <param name="e">Event arguments.</param>
516        private static void OnGUIInitInstance(object sender, InitInstanceEventArgs e)
517        {
518            GuiProgram program = (GuiProgram)sender;
519            eraserClient = new RemoteExecutorServer();
520
521            //Set our UI language
522            EraserSettings settings = EraserSettings.Get();
523            Thread.CurrentThread.CurrentUICulture = new CultureInfo(settings.Language);
524            Application.SafeTopLevelCaptionFormat = S._("Eraser");
525
526            //Load the task list
527            try
528            {
529                if (File.Exists(TaskListPath))
530                {
531                    using (FileStream stream = new FileStream(TaskListPath, FileMode.Open,
532                        FileAccess.Read, FileShare.Read))
533                    {
534                        eraserClient.Tasks.LoadFromStream(stream);
535                    }
536                }
537            }
538            catch (InvalidDataException ex)
539            {
540                File.Delete(TaskListPath);
541                MessageBox.Show(S._("Could not load task list. All task entries have " +
542                    "been lost. The error returned was: {0}", ex.Message), S._("Eraser"),
543                    MessageBoxButtons.OK, MessageBoxIcon.Error,
544                    MessageBoxDefaultButton.Button1,
545                    Localisation.IsRightToLeft(null) ?
546                        MessageBoxOptions.RtlReading | MessageBoxOptions.RightAlign : 0);
547            }
548
549            //Create the main form
550            program.MainForm = new MainForm();
551
552            //Decide whether to display any UI
553            GuiArguments arguments = new GuiArguments();
554            Args.Parse(program.CommandLine, CommandLinePrefixes, CommandLineSeparators, arguments);
555            e.ShowMainForm = !arguments.AtRestart && !arguments.Quiet;
556
557            //Queue tasks meant for running at restart if we are given that command line.
558            if (arguments.AtRestart)
559                eraserClient.QueueRestartTasks();
560
561            //Run the eraser client.
562            eraserClient.Run();
563        }
564
565        /// <summary>
566        /// Triggered when a second instance of Eraser is started.
567        /// </summary>
568        /// <param name="sender">The sender of the event.</param>
569        /// <param name="e">Event argument.</param>
570        private static void OnGUINextInstance(object sender, NextInstanceEventArgs e)
571        {
572            //Another instance of the GUI Program has been started: show the main window
573            //now as we still do not have a facility to handle the command line arguments.
574            GuiProgram program = (GuiProgram)sender;
575
576            //Invoke the function if we aren't on the main thread
577            if (program.MainForm.InvokeRequired)
578            {
579                program.MainForm.Invoke(
580                    (GuiProgram.NextInstanceEventHandler)OnGUINextInstance,
581                    sender, e);
582                return;
583            }
584
585            program.MainForm.Show();
586        }
587
588        /// <summary>
589        /// Triggered when the first instance of Eraser is exited.
590        /// </summary>
591        /// <param name="sender">The sender of the event.</param>
592        /// <param name="e">Event argument.</param>
593        private static void OnGUIExitInstance(object sender, EventArgs e)
594        {
595            //Save the task list
596            if (!Directory.Exists(Program.AppDataPath))
597                Directory.CreateDirectory(Program.AppDataPath);
598            using (FileStream stream = new FileStream(TaskListPath, FileMode.Create,
599                FileAccess.Write, FileShare.None))
600            {
601                eraserClient.Tasks.SaveToStream(stream);
602            }
603
604            //Dispose the eraser executor instance
605            eraserClient.Dispose();
606        }
607        #endregion
608
609        /// <summary>
610        /// The acceptable list of command line prefixes we will accept.
611        /// </summary>
612        public const string CommandLinePrefixes = "(/|-|--)";
613
614        /// <summary>
615        /// The acceptable list of command line separators we will accept.
616        /// </summary>
617        public const string CommandLineSeparators = "(:|=)";
618
619        /// <summary>
620        /// The global Executor instance.
621        /// </summary>
622        public static Executor eraserClient;
623
624        /// <summary>
625        /// Path to the Eraser application data path.
626        /// </summary>
627        public static readonly string AppDataPath = Path.Combine(Environment.GetFolderPath(
628            Environment.SpecialFolder.LocalApplicationData), @"Eraser 6");
629
630        /// <summary>
631        /// File name of the Eraser task list.
632        /// </summary>
633        private const string TaskListFileName = @"Task List.ersx";
634
635        /// <summary>
636        /// Path to the Eraser task list.
637        /// </summary>
638        public static readonly string TaskListPath = Path.Combine(AppDataPath, TaskListFileName);
639
640        /// <summary>
641        /// Path to the Eraser settings key (relative to HKCU)
642        /// </summary>
643        public const string SettingsPath = @"SOFTWARE\Eraser\Eraser 6";
644    }
645}
Note: See TracBrowser for help on using the repository browser.