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

Revision 2002, 21.4 KB checked in by lowjoel, 5 years ago (diff)

Implemented response file support to allow for very long command lines.

  • 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            Console.Out.Flush();
251        }
252
253        /// <summary>
254        /// Prints the help text for Eraser (with copyright)
255        /// </summary>
256        /// <param name="arguments">Not used.</param>
257        private static void CommandHelp(ConsoleArguments arguments)
258        {
259            Console.WriteLine(@"Eraser {0}
260(c) 2008-2010 The Eraser Project
261Eraser is Open-Source Software: see http://eraser.heidi.ie/ for details.
262", Assembly.GetExecutingAssembly().GetName().Version);
263
264            PrintCommandHelp();
265        }
266
267        /// <summary>
268        /// Lists all registered erasure methods.
269        /// </summary>
270        /// <param name="arguments">Not used.</param>
271        private static void CommandQueryMethods(ConsoleArguments arguments)
272        {
273            //Output the header
274            const string methodFormat = "{0,-2} {1,-39} {2}";
275            Console.WriteLine(methodFormat, "", "Method", "GUID");
276            Console.WriteLine(new string('-', 79));
277
278            //Refresh the list of erasure methods
279            foreach (ErasureMethod method in ManagerLibrary.Instance.ErasureMethodRegistrar)
280            {
281                Console.WriteLine(methodFormat, (method is UnusedSpaceErasureMethod) ?
282                    "U" : "", method.Name, method.Guid.ToString());
283            }
284        }
285
286        /// <summary>
287        /// Parses the command line for tasks and adds them using the
288        /// <see cref="RemoteExecutor"/> class.
289        /// </summary>
290        /// <param name="arg">The command line parameters passed to the program.</param>
291        private static void CommandAddTask(ConsoleArguments arg)
292        {
293            AddTaskArguments arguments = (AddTaskArguments)arg;
294
295            //Create the task then set the method as well as schedule
296            Task task = new Task();
297            ErasureMethod method = arguments.ErasureMethod == Guid.Empty ?
298                ErasureMethodRegistrar.Default :
299                ManagerLibrary.Instance.ErasureMethodRegistrar[arguments.ErasureMethod];
300            switch (arguments.Schedule.ToUpperInvariant())
301            {
302                case "":
303                case "NOW":
304                    task.Schedule = Schedule.RunNow;
305                    break;
306                case "MANUALLY":
307                    task.Schedule = Schedule.RunManually;
308                    break;
309                case "RESTART":
310                    task.Schedule = Schedule.RunOnRestart;
311                    break;
312                default:
313                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
314                        "Unknown schedule type: {0}", arguments.Schedule), "/schedule");
315            }
316
317            //Parse the rest of the command line parameters as target expressions.
318            List<string> trueValues = new List<string>(new string[] { "yes", "true" });
319            string[] strings = new string[] {
320                //The recycle bin target
321                "(?<recycleBin>recyclebin)",
322
323                //The unused space erasure target, taking the optional clusterTips
324                //argument which defaults to true; if none is specified it's assumed
325                //false
326                "unused=(?<unusedVolume>.*)(?<unusedTips>,clusterTips(=(?<unusedTipsValue>true|false))?)?",
327
328                //The directory target, taking a list of + and - wildcard expressions.
329                "dir=(?<directoryName>.*)(?<directoryParams>(?<directoryExcludeMask>,-[^,]+)|(?<directoryIncludeMask>,\\+[^,]+)|(?<directoryDeleteIfEmpty>,deleteIfEmpty(=(?<directoryDeleteIfEmptyValue>true|false))?))*",
330
331                //The file target.
332                "file=(?<fileName>.*)"
333            };
334
335            Regex regex = new Regex(string.Join("|", strings),
336                RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.RightToLeft);
337            foreach (string argument in arguments.PositionalArguments)
338            {
339                Match match = regex.Match(argument);
340                if (match.Captures.Count == 0)
341                {
342                    Console.WriteLine("Unknown argument: {0}, skipped.", argument);
343                    continue;
344                }
345
346                ErasureTarget target = null;
347                if (match.Groups["recycleBin"].Success)
348                {
349                    target = new RecycleBinTarget();
350                }
351                else if (match.Groups["unusedVolume"].Success)
352                {
353                    UnusedSpaceTarget unusedSpaceTarget = new UnusedSpaceTarget();
354                    target = unusedSpaceTarget;
355                    unusedSpaceTarget.Drive = match.Groups["unusedVolume"].Value;
356
357                    if (!match.Groups["unusedTips"].Success)
358                        unusedSpaceTarget.EraseClusterTips = false;
359                    else if (!match.Groups["unusedTipsValue"].Success)
360                        unusedSpaceTarget.EraseClusterTips = true;
361                    else
362                        unusedSpaceTarget.EraseClusterTips =
363                            trueValues.IndexOf(match.Groups["unusedTipsValue"].Value) != -1;
364                }
365                else if (match.Groups["directoryName"].Success)
366                {
367                    FolderTarget folderTarget = new FolderTarget();
368                    target = folderTarget;
369
370                    folderTarget.Path = match.Groups["directoryName"].Value;
371                    if (!match.Groups["directoryDeleteIfEmpty"].Success)
372                        folderTarget.DeleteIfEmpty = false;
373                    else if (!match.Groups["directoryDeleteIfEmptyValue"].Success)
374                        folderTarget.DeleteIfEmpty = true;
375                    else
376                        folderTarget.DeleteIfEmpty =
377                            trueValues.IndexOf(match.Groups["directoryDeleteIfEmptyValue"].Value) != -1;
378                    if (match.Groups["directoryExcludeMask"].Success)
379                        folderTarget.ExcludeMask += match.Groups["directoryExcludeMask"].Value.Remove(0, 2) + ' ';
380                    if (match.Groups["directoryIncludeMask"].Success)
381                        folderTarget.IncludeMask += match.Groups["directoryIncludeMask"].Value.Remove(0, 2) + ' ';
382                }
383                else if (match.Groups["fileName"].Success)
384                {
385                    FileTarget fileTarget = new FileTarget();
386                    target = fileTarget;
387                    fileTarget.Path = match.Groups["fileName"].Value;
388                }
389
390                if (target == null)
391                    continue;
392
393                target.Method = method;
394                task.Targets.Add(target);
395            }
396
397            //Check the number of tasks in the task.
398            if (task.Targets.Count == 0)
399                throw new ArgumentException("Tasks must contain at least one erasure target.");
400
401            //Send the task out.
402            try
403            {
404                using (RemoteExecutorClient client = new RemoteExecutorClient())
405                {
406                    client.Run();
407                    if (!client.IsConnected)
408                    {
409                        //The client cannot connect to the server. This probably means
410                        //that the server process isn't running. Start an instance.
411                        Process eraserInstance = Process.Start(
412                            Assembly.GetExecutingAssembly().Location, "/quiet");
413                        Thread.Sleep(0);
414                        eraserInstance.WaitForInputIdle();
415
416                        client.Run();
417                        if (!client.IsConnected)
418                            throw new IOException("Eraser cannot connect to the running " +
419                                "instance for erasures.");
420                    }
421
422                    client.Tasks.Add(task);
423                }
424            }
425            catch (UnauthorizedAccessException e)
426            {
427                //We can't connect to the pipe because the other instance of Eraser
428                //is running with higher privileges than this instance.
429                throw new UnauthorizedAccessException("Another instance of Eraser " +
430                    "is already running but it is running with higher privileges than " +
431                    "this instance of Eraser. Tasks cannot be added in this manner.\n\n" +
432                    "Close the running instance of Eraser and start it again without " +
433                    "administrator privileges, or run the command again as an " +
434                    "administrator.", e);
435            }
436        }
437
438        /// <summary>
439        /// Imports the given tasklists and adds them to the global Eraser instance.
440        /// </summary>
441        /// <param name="args">The list of files specified on the command line.</param>
442        private static void CommandImportTaskList(ConsoleArguments args)
443        {
444            //Import the task list
445            try
446            {
447                using (RemoteExecutorClient client = new RemoteExecutorClient())
448                {
449                    client.Run();
450                    if (!client.IsConnected)
451                    {
452                        //The client cannot connect to the server. This probably means
453                        //that the server process isn't running. Start an instance.
454                        Process eraserInstance = Process.Start(
455                            Assembly.GetExecutingAssembly().Location, "/quiet");
456                        eraserInstance.WaitForInputIdle();
457
458                        client.Run();
459                        if (!client.IsConnected)
460                            throw new IOException("Eraser cannot connect to the running " +
461                                "instance for erasures.");
462                    }
463
464                    foreach (string path in args.PositionalArguments)
465                        using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read))
466                            client.Tasks.LoadFromStream(stream);
467                }
468            }
469            catch (UnauthorizedAccessException e)
470            {
471                //We can't connect to the pipe because the other instance of Eraser
472                //is running with higher privileges than this instance.
473                throw new UnauthorizedAccessException("Another instance of Eraser " +
474                    "is already running but it is running with higher privileges than " +
475                    "this instance of Eraser. Tasks cannot be added in this manner.\n\n" +
476                    "Close the running instance of Eraser and start it again without " +
477                    "administrator privileges, or run the command again as an " +
478                    "administrator.", e);
479            }
480        }
481        #endregion
482
483        #region GUI Program code
484        /// <summary>
485        /// Runs Eraser as a GUI application.
486        /// </summary>
487        /// <param name="commandLine">The command line parameters passed to Eraser.</param>
488        private static void GUIMain(string[] commandLine)
489        {
490            //Create a unique program instance ID for this user.
491            string instanceId = "Eraser-BAD0DAC6-C9EE-4acc-8701-C9B3C64BC65E-GUI-" +
492                WindowsIdentity.GetCurrent().User.ToString();
493
494            //Then initialise the instance and initialise the Manager library.
495            using (GuiProgram program = new GuiProgram(commandLine, instanceId))
496            using (ManagerLibrary library = new ManagerLibrary(new Settings()))
497            {
498                program.InitInstance += OnGUIInitInstance;
499                program.NextInstance += OnGUINextInstance;
500                program.ExitInstance += OnGUIExitInstance;
501                program.Run();
502            }
503        }
504
505        /// <summary>
506        /// Triggered when the Program is started for the first time.
507        /// </summary>
508        /// <param name="sender">The sender of the object.</param>
509        /// <param name="e">Event arguments.</param>
510        private static void OnGUIInitInstance(object sender, InitInstanceEventArgs e)
511        {
512            GuiProgram program = (GuiProgram)sender;
513            eraserClient = new RemoteExecutorServer();
514
515            //Set our UI language
516            EraserSettings settings = EraserSettings.Get();
517            Thread.CurrentThread.CurrentUICulture = new CultureInfo(settings.Language);
518            Application.SafeTopLevelCaptionFormat = S._("Eraser");
519
520            //Load the task list
521            try
522            {
523                if (File.Exists(TaskListPath))
524                {
525                    using (FileStream stream = new FileStream(TaskListPath, FileMode.Open,
526                        FileAccess.Read, FileShare.Read))
527                    {
528                        eraserClient.Tasks.LoadFromStream(stream);
529                    }
530                }
531            }
532            catch (InvalidDataException ex)
533            {
534                File.Delete(TaskListPath);
535                MessageBox.Show(S._("Could not load task list. All task entries have " +
536                    "been lost. The error returned was: {0}", ex.Message), S._("Eraser"),
537                    MessageBoxButtons.OK, MessageBoxIcon.Error,
538                    MessageBoxDefaultButton.Button1,
539                    Localisation.IsRightToLeft(null) ?
540                        MessageBoxOptions.RtlReading | MessageBoxOptions.RightAlign : 0);
541            }
542
543            //Create the main form
544            program.MainForm = new MainForm();
545
546            //Decide whether to display any UI
547            GuiArguments arguments = new GuiArguments();
548            Args.Parse(program.CommandLine, CommandLinePrefixes, CommandLineSeparators, arguments);
549            e.ShowMainForm = !arguments.AtRestart && !arguments.Quiet;
550
551            //Queue tasks meant for running at restart if we are given that command line.
552            if (arguments.AtRestart)
553                eraserClient.QueueRestartTasks();
554
555            //Run the eraser client.
556            eraserClient.Run();
557        }
558
559        /// <summary>
560        /// Triggered when a second instance of Eraser is started.
561        /// </summary>
562        /// <param name="sender">The sender of the event.</param>
563        /// <param name="e">Event argument.</param>
564        private static void OnGUINextInstance(object sender, NextInstanceEventArgs e)
565        {
566            //Another instance of the GUI Program has been started: show the main window
567            //now as we still do not have a facility to handle the command line arguments.
568            GuiProgram program = (GuiProgram)sender;
569
570            //Invoke the function if we aren't on the main thread
571            if (program.MainForm.InvokeRequired)
572            {
573                program.MainForm.Invoke(
574                    (GuiProgram.NextInstanceEventHandler)OnGUINextInstance,
575                    sender, e);
576                return;
577            }
578
579            program.MainForm.Show();
580        }
581
582        /// <summary>
583        /// Triggered when the first instance of Eraser is exited.
584        /// </summary>
585        /// <param name="sender">The sender of the event.</param>
586        /// <param name="e">Event argument.</param>
587        private static void OnGUIExitInstance(object sender, EventArgs e)
588        {
589            //Save the task list
590            if (!Directory.Exists(Program.AppDataPath))
591                Directory.CreateDirectory(Program.AppDataPath);
592            using (FileStream stream = new FileStream(TaskListPath, FileMode.Create,
593                FileAccess.Write, FileShare.None))
594            {
595                eraserClient.Tasks.SaveToStream(stream);
596            }
597
598            //Dispose the eraser executor instance
599            eraserClient.Dispose();
600        }
601        #endregion
602
603        /// <summary>
604        /// The acceptable list of command line prefixes we will accept.
605        /// </summary>
606        public const string CommandLinePrefixes = "(/|-|--)";
607
608        /// <summary>
609        /// The acceptable list of command line separators we will accept.
610        /// </summary>
611        public const string CommandLineSeparators = "(:|=)";
612
613        /// <summary>
614        /// The global Executor instance.
615        /// </summary>
616        public static Executor eraserClient;
617
618        /// <summary>
619        /// Path to the Eraser application data path.
620        /// </summary>
621        public static readonly string AppDataPath = Path.Combine(Environment.GetFolderPath(
622            Environment.SpecialFolder.LocalApplicationData), @"Eraser 6");
623
624        /// <summary>
625        /// File name of the Eraser task list.
626        /// </summary>
627        private const string TaskListFileName = @"Task List.ersx";
628
629        /// <summary>
630        /// Path to the Eraser task list.
631        /// </summary>
632        public static readonly string TaskListPath = Path.Combine(AppDataPath, TaskListFileName);
633
634        /// <summary>
635        /// Path to the Eraser settings key (relative to HKCU)
636        /// </summary>
637        public const string SettingsPath = @"SOFTWARE\Eraser\Eraser 6";
638    }
639}
Note: See TracBrowser for help on using the repository browser.