source: branches/eraser6/6.0/Eraser/Program.cs @ 1851

Revision 1851, 39.6 KB checked in by lowjoel, 5 years ago (diff)

Fixed command line parsing when commas and equality signs are in the filename.

  • 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.IO.Pipes;
28using System.Text;
29using System.Threading;
30using System.Runtime.Serialization;
31using System.Globalization;
32using System.Reflection;
33using System.Diagnostics;
34using System.ComponentModel;
35using System.Security.Principal;
36using System.Security.AccessControl;
37
38using Eraser.Manager;
39using Eraser.Util;
40
41namespace Eraser
42{
43    static class Program
44    {
45        /// <summary>
46        /// The main entry point for the application.
47        /// </summary>
48        [STAThread]
49        static int Main(string[] commandLine)
50        {
51            //Trivial case: no command parameters
52            if (commandLine.Length == 0)
53                GUIMain(commandLine);
54
55            //Determine if the sole parameter is --restart; if it is, start the GUI
56            //passing isRestart as true. Otherwise, we're a console application.
57            else if (commandLine.Length == 1)
58            {
59                if (commandLine[0] == "--atRestart" || commandLine[0] == "--quiet")
60                {
61                    GUIMain(commandLine);
62                }
63                else
64                {
65                    return CommandMain(commandLine);
66                }
67            }
68
69            //The other trivial case: definitely a console application.
70            else
71                return CommandMain(commandLine);
72
73            //No error.
74            return 0;
75        }
76
77        /// <summary>
78        /// Runs Eraser as a command-line application.
79        /// </summary>
80        /// <param name="commandLine">The command line parameters passed to Eraser.</param>
81        private static int CommandMain(string[] commandLine)
82        {
83            //True if the user specified a quiet command.
84            bool isQuiet = false;
85
86            try
87            {
88                CommandLineProgram program = new CommandLineProgram(commandLine);
89                isQuiet = program.Arguments.Quiet;
90
91                using (ManagerLibrary library = new ManagerLibrary(new Settings()))
92                    program.Run();
93
94                return 0;
95            }
96            catch (UnauthorizedAccessException)
97            {
98                return 5; //ERROR_ACCESS_DENIED
99            }
100            catch (Win32Exception e)
101            {
102                Console.WriteLine(e.Message);
103                return e.ErrorCode;
104            }
105            catch (Exception e)
106            {
107                Console.WriteLine(e.Message);
108                return 1;
109            }
110            finally
111            {
112                //Flush the buffered output to the console
113                Console.Out.Flush();
114
115                //Don't ask for a key to press if the user specified Quiet
116                if (!isQuiet)
117                {
118                    Console.Write("\nPress enter to continue . . . ");
119                    Console.Out.Flush();
120                    Console.ReadLine();
121                }
122
123                KernelApi.FreeConsole();
124            }
125        }
126
127        /// <summary>
128        /// Runs Eraser as a GUI application.
129        /// </summary>
130        /// <param name="commandLine">The command line parameters passed to Eraser.</param>
131        private static void GUIMain(string[] commandLine)
132        {
133            //Create the program instance
134            using (GUIProgram program = new GUIProgram(commandLine, "Eraser-BAD0DAC6-C9EE-4acc-" +
135                "8701-C9B3C64BC65E-GUI-" +
136                System.Security.Principal.WindowsIdentity.GetCurrent().User.ToString()))
137
138            //Then run the program instance.
139            using (ManagerLibrary library = new ManagerLibrary(new Settings()))
140            {
141                program.InitInstance += OnGUIInitInstance;
142                program.NextInstance += OnGUINextInstance;
143                program.ExitInstance += OnGUIExitInstance;
144                program.Run();
145            }
146        }
147
148        /// <summary>
149        /// Triggered when the Program is started for the first time.
150        /// </summary>
151        /// <param name="sender">The sender of the object.</param>
152        /// <returns>True if the user did not specify --quiet, false otherwise.</returns>
153        private static bool OnGUIInitInstance(object sender)
154        {
155            GUIProgram program = (GUIProgram)sender;
156            eraserClient = new RemoteExecutorServer();
157
158            //Set our UI language
159            EraserSettings settings = EraserSettings.Get();
160            System.Threading.Thread.CurrentThread.CurrentUICulture =
161                new CultureInfo(settings.Language);
162            Application.SafeTopLevelCaptionFormat = S._("Eraser");
163
164            //Load the task list
165            SettingsCompatibility.Execute();
166            try
167            {
168                if (System.IO.File.Exists(TaskListPath))
169                {
170                    using (FileStream stream = new FileStream(TaskListPath, FileMode.Open,
171                        FileAccess.Read, FileShare.Read))
172                    {
173                        eraserClient.Tasks.LoadFromStream(stream);
174                    }
175                }
176            }
177            catch (SerializationException e)
178            {
179                System.IO.File.Delete(TaskListPath);
180                MessageBox.Show(S._("Could not load task list. All task entries have " +
181                    "been lost. The error returned was: {0}", e.Message), S._("Eraser"),
182                    MessageBoxButtons.OK, MessageBoxIcon.Error,
183                    MessageBoxDefaultButton.Button1,
184                    S.IsRightToLeft(null) ? MessageBoxOptions.RtlReading : 0);
185            }
186
187            //Create the main form
188            program.MainForm = new MainForm();
189            bool showMainForm = true;
190            foreach (string param in program.CommandLine)
191            {
192                //Run tasks which are meant to be run on restart
193                switch (param)
194                {
195                    case "--atRestart":
196                        eraserClient.QueueRestartTasks();
197                        goto case "--quiet";
198
199                    //Hide the main form if the user specified the quiet command
200                    //line
201                    case "--quiet":
202                        showMainForm = false;
203                        break;
204                }
205            }
206
207            //Run the eraser client.
208            eraserClient.Run();
209            return showMainForm;
210        }
211
212        /// <summary>
213        /// Triggered when a second instance of Eraser is started.
214        /// </summary>
215        /// <param name="sender">The sender of the event.</param>
216        /// <param name="message">The message from the source application.</param>
217        private static void OnGUINextInstance(object sender, string message)
218        {
219            //Another instance of the GUI Program has been started: show the main window
220            //now as we still do not have a facility to handle the command line arguments.
221            GUIProgram program = (GUIProgram)sender;
222
223            //Invoke the function if we aren't on the main thread
224            if (program.MainForm.InvokeRequired)
225            {
226                program.MainForm.Invoke(new GUIProgram.NextInstanceFunction(
227                    OnGUINextInstance), new object[] { sender, message });
228                return;
229            }
230
231            program.MainForm.Visible = true;
232        }
233
234        /// <summary>
235        /// Triggered when the first instance of Eraser is exited.
236        /// </summary>
237        /// <param name="sender">The sender of the event.</param>
238        private static void OnGUIExitInstance(object sender)
239        {
240            //Save the task list
241            if (!Directory.Exists(Program.AppDataPath))
242                Directory.CreateDirectory(Program.AppDataPath);
243            using (FileStream stream = new FileStream(TaskListPath, FileMode.Create,
244                FileAccess.Write, FileShare.None))
245            {
246                eraserClient.Tasks.SaveToStream(stream);
247            }
248
249            //Dispose the eraser executor instance
250            eraserClient.Dispose();
251        }
252
253        /// <summary>
254        /// The global Executor instance.
255        /// </summary>
256        public static Executor eraserClient;
257
258        /// <summary>
259        /// Path to the Eraser application data path.
260        /// </summary>
261        public static readonly string AppDataPath = Path.Combine(Environment.GetFolderPath(
262            Environment.SpecialFolder.LocalApplicationData), @"Eraser 6");
263
264        /// <summary>
265        /// File name of the Eraser task list.
266        /// </summary>
267        private const string TaskListFileName = @"Task List.ersx";
268
269        /// <summary>
270        /// Path to the Eraser task list.
271        /// </summary>
272        public static readonly string TaskListPath = Path.Combine(AppDataPath, TaskListFileName);
273
274        /// <summary>
275        /// Path to the Eraser settings key (relative to HKCU)
276        /// </summary>
277        public const string SettingsPath = @"SOFTWARE\Eraser\Eraser 6";
278    }
279
280    class GUIProgram : IDisposable
281    {
282        /// <summary>
283        /// Constructor.
284        /// </summary>
285        /// <param name="commandLine">The command line arguments associated with
286        /// this program launch</param>
287        /// <param name="instanceID">The instance ID of the program, used to group
288        /// instances of the program together.</param>
289        public GUIProgram(string[] commandLine, string instanceID)
290        {
291            Application.EnableVisualStyles();
292            Application.SetCompatibleTextRenderingDefault(false);
293            this.instanceID = instanceID;
294            this.CommandLine = commandLine;
295
296            //Check if there already is another instance of the program.
297            globalMutex = new Mutex(true, instanceID, out isFirstInstance);
298        }
299
300        ~GUIProgram()
301        {
302            Dispose(false);
303        }
304
305        protected virtual void Dispose(bool disposing)
306        {
307            if (disposing)
308                globalMutex.Close();
309        }
310
311        public void Dispose()
312        {
313            Dispose(true);
314            GC.SuppressFinalize(this);
315        }
316
317        /// <summary>
318        /// Runs the event loop of the GUI program, returning true if the program
319        /// was started as there were no other instances of the program, or false
320        /// if other instances were found.
321        /// </summary>
322        /// <remarks>
323        /// This function must always be called in your program, regardless
324        /// of the value of <see cref="IsAlreadyRunning"/>. If this function is not
325        /// called, the first instance will never be notified that another was started.
326        /// </remarks>
327        /// <returns>True if the application was started, or false if another instance
328        /// was detected.</returns>
329        public bool Run()
330        {
331            //If no other instances are running, set up our pipe server so clients
332            //can connect and give us subsequent command lines.
333            if (IsFirstInstance)
334            {
335                try
336                {
337                    //Initialise and run the program.
338                    bool ShowMainForm = OnInitInstance(this);
339                    if (MainForm == null)
340                        return false;
341
342                    //Create the pipe server which will handle connections to us
343                    pipeServer = new Thread(ServerMain);
344                    pipeServer.Start();
345
346                    //Handle the exit instance event. This will occur when the main form
347                    //has been closed.
348                    Application.ApplicationExit += OnExitInstance;
349                    MainForm.FormClosed += OnExitInstance;
350
351                    if (ShowMainForm)
352                        Application.Run(MainForm);
353                    else
354                        Application.Run();
355
356                    return true;
357                }
358                finally
359                {
360                    pipeServer.Abort();
361                }
362            }
363
364            //Another instance of the program is running. Connect to it and transfer
365            //the command line arguments
366            else
367            {
368                try
369                {
370                    NamedPipeClientStream client = new NamedPipeClientStream(".", instanceID,
371                        PipeDirection.Out);
372                    client.Connect(500);
373
374                    StringBuilder commandLineStr = new StringBuilder(CommandLine.Length * 64);
375                    foreach (string param in CommandLine)
376                        commandLineStr.Append(string.Format(
377                            CultureInfo.InvariantCulture, "{0}\0", param));
378
379                    byte[] buffer = new byte[commandLineStr.Length];
380                    int count = Encoding.UTF8.GetBytes(commandLineStr.ToString(), 0,
381                        commandLineStr.Length, buffer, 0);
382                    client.Write(buffer, 0, count);
383                }
384                catch (UnauthorizedAccessException)
385                {
386                    //We can't connect to the pipe because the other instance of Eraser
387                    //is running with higher privileges than this instance. Tell the
388                    //user this is the case and show him how to resolve the issue.
389                    MessageBox.Show(S._("Another instance of Eraser is already running but it is " +
390                        "running with higher privileges than this instance of Eraser.\n\n" +
391                        "Eraser will now exit."), S._("Eraser"), MessageBoxButtons.OK,
392                        MessageBoxIcon.Information, MessageBoxDefaultButton.Button1,
393                        S.IsRightToLeft(null) ? MessageBoxOptions.RtlReading : 0);
394                }
395                catch (IOException ex)
396                {
397                    MessageBox.Show(S._("Another instance of Eraser is already running but " +
398                        "cannot be connected to.\n\nThe error returned was: {0}", ex.Message),
399                        S._("Eraser"), MessageBoxButtons.OK, MessageBoxIcon.Error,
400                        MessageBoxDefaultButton.Button1,
401                        S.IsRightToLeft(null) ? MessageBoxOptions.RtlReading : 0);
402                }
403                catch (TimeoutException)
404                {
405                    //Can't do much: half a second is a reasonably long time to wait.
406                }
407                return false;
408            }
409        }
410
411        /// <summary>
412        /// Holds information required for an asynchronous call to
413        /// NamedPipeServerStream.BeginWaitForConnection.
414        /// </summary>
415        private struct ServerAsyncInfo
416        {
417            public NamedPipeServerStream Server;
418            public AutoResetEvent WaitHandle;
419        }
420
421        /// <summary>
422        /// Runs a background thread, monitoring for new connections to the server.
423        /// </summary>
424        private void ServerMain()
425        {
426            while (pipeServer.ThreadState != System.Threading.ThreadState.AbortRequested)
427            {
428                PipeSecurity security = new PipeSecurity();
429                security.AddAccessRule(new PipeAccessRule(
430                    WindowsIdentity.GetCurrent().User,
431                    PipeAccessRights.FullControl, AccessControlType.Allow));
432                using (NamedPipeServerStream server = new NamedPipeServerStream(instanceID,
433                    PipeDirection.In, 1, PipeTransmissionMode.Message, PipeOptions.Asynchronous,
434                    128, 128, security))
435                {
436                    ServerAsyncInfo async = new ServerAsyncInfo();
437                    async.Server = server;
438                    async.WaitHandle = new AutoResetEvent(false);
439                    IAsyncResult result = server.BeginWaitForConnection(WaitForConnection, async);
440
441                    //Wait for the operation to complete.
442                    if (result.AsyncWaitHandle.WaitOne())
443                        //It completed. Wait for the processing to complete.
444                        async.WaitHandle.WaitOne();
445                }
446            }
447        }
448
449        /// <summary>
450        /// Waits for new connections to be made to the server.
451        /// </summary>
452        /// <param name="result"></param>
453        private void WaitForConnection(IAsyncResult result)
454        {
455            ServerAsyncInfo async = (ServerAsyncInfo)result.AsyncState;
456
457            try
458            {
459                //We're done waiting for the connection
460                async.Server.EndWaitForConnection(result);
461
462                //Process the connection if the server was successfully connected.
463                if (async.Server.IsConnected)
464                {
465                    //Read the message from the secondary instance
466                    byte[] buffer = new byte[8192];
467                    string message = string.Empty;
468
469                    do
470                    {
471                        int lastRead = async.Server.Read(buffer, 0, buffer.Length);
472                        message += Encoding.UTF8.GetString(buffer, 0, lastRead);
473                    }
474                    while (!async.Server.IsMessageComplete);
475
476                    //Let the event handler process the message.
477                    OnNextInstance(this, message);
478                }
479            }
480            catch (ObjectDisposedException)
481            {
482            }
483            finally
484            {
485                //Reset the wait event
486                async.WaitHandle.Set();
487            }
488        }
489
490        /// <summary>
491        /// Gets the command line arguments this instance was started with.
492        /// </summary>
493        public string[] CommandLine { get; private set; }
494
495        /// <summary>
496        /// Gets whether another instance of the program is already running.
497        /// </summary>
498        public bool IsFirstInstance
499        {
500            get
501            {
502                return isFirstInstance;
503            }
504        }
505
506        /// <summary>
507        /// The main form for this program instance. This form will be shown when
508        /// run is called if it is non-null and if its Visible property is true.
509        /// </summary>
510        public Form MainForm { get; set; }
511
512        #region Events
513        /// <summary>
514        /// The prototype of event handlers procesing the InitInstance event.
515        /// </summary>
516        /// <param name="sender">The sender of the event.</param>
517        /// <returns>True if the MainForm property holds a valid reference to
518        /// a form, and the form should be displayed to the user.</returns>
519        public delegate bool InitInstanceFunction(object sender);
520
521        /// <summary>
522        /// The event object managing listeners to the instance initialisation event.
523        /// This event is raised when the first instance of the program is started
524        /// and this is where the program initialisation code should be.
525        /// </summary>
526        public event InitInstanceFunction InitInstance;
527
528        /// <summary>
529        /// Broadcasts the InitInstance event.
530        /// </summary>
531        /// <param name="sender">The sender of the event.</param>
532        /// <returns>True if the MainForm object should be shown.</returns>
533        private bool OnInitInstance(object sender)
534        {
535            if (InitInstance != null)
536                return InitInstance(sender);
537            return true;
538        }
539
540        /// <summary>
541        /// The prototype of event handlers procesing the NextInstance event.
542        /// </summary>
543        /// <param name="sender">The sender of the event</param>
544        public delegate void NextInstanceFunction(object sender, string message);
545
546        /// <summary>
547        /// The event object managing listeners to the next instance event. This
548        /// event is raised when a second instance of the program is started.
549        /// </summary>
550        public event NextInstanceFunction NextInstance;
551
552        /// <summary>
553        /// Broadcasts the NextInstance event.
554        /// </summary>
555        /// <param name="sender">The sender of the event.</param>
556        /// <param name="message">The message sent by the secondary instance.</param>
557        private void OnNextInstance(object sender, string message)
558        {
559            if (NextInstance != null)
560                NextInstance(sender, message);
561        }
562
563        /// <summary>
564        /// The prototype of event handlers procesing the ExitInstance event.
565        /// </summary>
566        /// <param name="sender">The sender of the event.</param>
567        public delegate void ExitInstanceFunction(object sender);
568
569        /// <summary>
570        /// The event object managing listeners to the exit instance event. This
571        /// event is raised when the first instance of the program is exited.
572        /// </summary>
573        public event ExitInstanceFunction ExitInstance;
574
575        /// <summary>
576        /// Broadcasts the ExitInstance event after getting the notification that the
577        /// application is exiting.
578        /// </summary>
579        /// <param name="sender">The sender of the event.</param>
580        private void OnExitInstance(object sender, EventArgs e)
581        {
582            //If the exit event has been broadcast don't repeat.
583            if (Exited)
584                return;
585
586            Exited = true;
587            if (ExitInstance != null)
588                ExitInstance(sender);
589        }
590
591        /// Tracks whether the Exit event has been broadcast. It should only be broadcast
592        /// once in the lifetime of the application.
593        private bool Exited;
594        #endregion
595
596        #region Instance variables
597        /// <summary>
598        /// The Instance ID of this program, used to group program instances together.
599        /// </summary>
600        private string instanceID;
601
602        /// <summary>
603        /// The named mutex ensuring that only one instance of the application runs
604        /// at a time.
605        /// </summary>
606        private Mutex globalMutex;
607
608        /// <summary>
609        /// The thread maintaining the pipe server for secondary instances to connect to.
610        /// </summary>
611        private Thread pipeServer;
612
613        /// <summary>
614        /// Holds whether this instance of the program is the first instance.
615        /// </summary>
616        private bool isFirstInstance;
617        #endregion
618    }
619
620    class CommandLineProgram
621    {
622        #region Command Line parsing classes
623        /// <summary>
624        /// Manages a command line.
625        /// </summary>
626        public abstract class CommandLine
627        {
628            /// <summary>
629            /// Gets the CommandLine-derived object for the given command line.
630            /// </summary>
631            /// <param name="cmdParams">The raw arguments passed to the program.</param>
632            /// <returns>A processed CommandLine Object.</returns>
633            public static CommandLine Get(string[] cmdParams)
634            {
635                //Get the action.
636                if (cmdParams.Length < 1)
637                    throw new ArgumentException("An action must be specified.");
638                string action = cmdParams[0];
639
640                CommandLine result = null;
641                switch (action)
642                {
643                    case "help":
644                        result = new HelpCommandLine();
645                        break;
646                    case "querymethods":
647                        result = new QueryMethodsCommandLine();
648                        break;
649                    case "importtasklist":
650                        result = new ImportTaskListCommandLine();
651                        break;
652                    case "addtask":
653                        result = new AddTaskCommandLine();
654                        break;
655                }
656
657                if (result != null)
658                {
659                    result.Parse(cmdParams);
660                    return result;
661                }
662                else
663                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
664                        "Unknown action: {0}", action));
665            }
666
667            /// <summary>
668            /// Constructor.
669            /// </summary>
670            protected CommandLine()
671            {
672            }
673
674            /// <summary>
675            /// Parses the given command line arguments to the respective properties of
676            /// the class.
677            /// </summary>
678            /// <param name="cmdParams">The raw arguments passed to the program.</param>
679            /// <returns></returns>
680            public bool Parse(string[] cmdParams)
681            {
682                //Iterate over each argument, resolving them ourselves and letting
683                //subclasses resolve them if we don't know how to.
684                for (int i = 1; i != cmdParams.Length; ++i)
685                {
686                    if (IsParam(cmdParams[i], "quiet", "q"))
687                        Quiet = true;
688                    else if (!ResolveParameter(cmdParams[i]))
689                        throw new ArgumentException("Unknown argument: " + cmdParams[i]);
690                }
691
692                return true;
693            }
694
695            /// <summary>
696            /// Called when a parameter is not used by the current CommandLine object
697            /// for subclasses to handle their parameters.
698            /// </summary>
699            /// <param name="param">The parameter to resolve.</param>
700            /// <returns>Return true if the parameter was resolved and accepted.</returns>
701            protected virtual bool ResolveParameter(string param)
702            {
703                return false;
704            }
705
706            /// <summary>
707            /// Checks if the given <paramref name="parameter"/> refers to the
708            /// <paramref name="expectedParameter"/>, regardless of whether it is specified
709            /// with -, --, or /
710            /// </summary>
711            /// <param name="parameter">The parameter on the command line.</param>
712            /// <param name="expectedParameter">The parameter the program is looking for, without
713            /// the - or / prefix.</param>
714            /// <param name="shortParameter">The short parameter when used with a single hyphen,
715            /// without the - or / prefix.</param>
716            /// <returns>True if the parameter references the given expected parameter.</returns>
717            protected static bool IsParam(string parameter, string expectedParameter,
718                string shortParameter)
719            {
720                //Trivial case
721                if (parameter.Length < 1)
722                    return false;
723
724                //Extract the bits before the equal sign.
725                {
726                    int equalPos = parameter.IndexOf('=');
727                    if (equalPos != -1)
728                        parameter = parameter.Substring(0, equalPos);
729                }
730
731                //Get the first letter.
732                switch (parameter[0])
733                {
734                    case '-':
735                        //Can be a - or a --. Check for the second parameter
736                        if (parameter.Length < 2)
737                            //Nothing specified at the end... it's invalid.
738                            return false;
739
740                        if (parameter[1] == '-')
741                            return parameter.Substring(2) == expectedParameter;
742                        else if (string.IsNullOrEmpty(shortParameter))
743                            return parameter.Substring(1) == expectedParameter;
744                        else
745                            return parameter.Substring(1) == shortParameter;
746
747                    case '/':
748                        //The / can be used with both long and short parameters.
749                        parameter = parameter.Substring(1);
750                        return parameter == expectedParameter || (
751                            !string.IsNullOrEmpty(shortParameter) && parameter == shortParameter
752                        );
753
754                    default:
755                        return false;
756                }
757            }
758
759            /// <summary>
760            /// Gets the list of subparameters of the parameter. Subparameters are text
761            /// after the first =, separated by commas.
762            /// </summary>
763            /// <param name="param">The subparameter text to parse.</param>
764            /// <returns>The list of subparameters in the parameter.</returns>
765            protected static List<KeyValuePair<string, string>> GetSubParameters(string param)
766            {
767                List<KeyValuePair<string, string>> result =
768                    new List<KeyValuePair<string, string>>();
769                int lastPos = 0;
770                int commaPos = (param += ',').IndexOf(',');
771
772                while (commaPos != -1)
773                {
774                    //Check that the first parameter is not a \ otherwise this comma
775                    //is escaped
776                    if (commaPos == 0 || (commaPos > 0 && CountEscapes(param, commaPos) % 2 == 0))
777                    {
778                        //Extract the current subparameter, and dissect the subparameter
779                        //at the first =.
780                        string subParam = param.Substring(lastPos, commaPos - lastPos);
781                        int equalPos = -1;
782
783                        do
784                        {
785                            equalPos = subParam.IndexOf('=', equalPos + 1);
786                            if (equalPos == -1)
787                            {
788                                result.Add(new KeyValuePair<string, string>(
789                                    UnescapeCommandLine(subParam), null));
790                            }
791                            else if (equalPos == 0 ||
792                                (equalPos > 0 && CountEscapes(param, equalPos) % 2 == 0))
793                            {
794                                result.Add(new KeyValuePair<string, string>(
795                                    UnescapeCommandLine(subParam.Substring(0, equalPos)),
796                                    UnescapeCommandLine(subParam.Substring(equalPos + 1))));
797                                break;
798                            }
799                        }
800                        while (equalPos != -1);
801                        lastPos = ++commaPos;
802                    }
803                    else
804                        ++commaPos;
805
806                    //Find the next ,
807                    commaPos = param.IndexOf(',', commaPos);
808                }
809
810                return result;
811            }
812
813            /// <summary>
814            /// Gets the number of escapes the current token has.
815            /// </summary>
816            /// <param name="param">The parameter.</param>
817            /// <param name="index">The index of the character escaped.</param>
818            /// <returns>The number of escapes. Multiples of two indicate backslashes, odd
819            /// numbers indicate the current character is escaped.</returns>
820            private static int CountEscapes(string param, int index)
821            {
822                if (index == 0)
823                    return 0;
824
825                for (int i = 0; index != 0; ++i)
826                    if (param[--index] != '\\')
827                        return i;
828
829                return 0;
830            }
831
832            /// <summary>
833            /// Unescapes a subparameter command line, removing the extra
834            /// </summary>
835            /// <param name="param"></param>
836            /// <returns></returns>
837            private static string UnescapeCommandLine(string param)
838            {
839                StringBuilder result = new StringBuilder(param.Length);
840                for (int i = 0; i < param.Length; ++i)
841                    if (param[i] == '\\' && i < param.Length - 1)
842                        result.Append(param[++i]);
843                    else
844                        result.Append(param[i]);
845                return result.ToString();
846            }
847
848            /// <summary>
849            /// True if no console window should be created.
850            /// </summary>
851            public bool Quiet { get; private set; }
852        }
853
854        /// <summary>
855        /// Manages a help query command line.
856        /// </summary>
857        class HelpCommandLine : CommandLine
858        {
859            public HelpCommandLine()
860            {
861            }
862        }
863
864        class QueryMethodsCommandLine : CommandLine
865        {
866            public QueryMethodsCommandLine()
867            {
868            }
869        }
870
871        /// <summary>
872        /// Manages a command line for adding tasks to the global DirectExecutor
873        /// </summary>
874        class AddTaskCommandLine : CommandLine
875        {
876            /// <summary>
877            /// Constructor.
878            /// </summary>
879            public AddTaskCommandLine()
880            {
881                Schedule = Schedule.RunNow;
882                Targets = new List<ErasureTarget>();
883            }
884
885            protected override bool ResolveParameter(string param)
886            {
887                int equalPos = param.IndexOf('=');
888                if (IsParam(param, "method", "m"))
889                {
890                    if (equalPos == -1)
891                        throw new ArgumentException("--method must be specified with an Erasure " +
892                            "method GUID.");
893
894                    List<KeyValuePair<string, string>> subParams =
895                        GetSubParameters(param.Substring(equalPos + 1));
896                    ErasureMethod = new Guid(subParams[0].Key);
897                }
898                else if (IsParam(param, "schedule", "s"))
899                {
900                    if (equalPos == -1)
901                        throw new ArgumentException("--schedule must be specified with a Schedule " +
902                            "type.");
903
904                    List<KeyValuePair<string, string>> subParams =
905                        GetSubParameters(param.Substring(equalPos + 1));
906                    switch (subParams[0].Key)
907                    {
908                        case "now":
909                            Schedule = Schedule.RunNow;
910                            break;
911                        case "manually":
912                            Schedule = Schedule.RunManually;
913                            break;
914                        case "restart":
915                            Schedule = Schedule.RunOnRestart;
916                            break;
917                        default:
918                            throw new ArgumentException("Unknown schedule type: " + subParams[0].Key);
919                    }
920                }
921                else if (IsParam(param, "recycled", "r"))
922                {
923                    Targets.Add(new RecycleBinTarget());
924                }
925                else if (IsParam(param, "unused", "u"))
926                {
927                    if (equalPos == -1)
928                        throw new ArgumentException("--unused must be specified with the Volume " +
929                            "to erase.");
930
931                    //Create the UnusedSpace target for inclusion into the task.
932                    UnusedSpaceTarget target = new UnusedSpaceTarget();
933
934                    //Determine if cluster tips should be erased.
935                    target.EraseClusterTips = false;
936                    List<KeyValuePair<string, string>> subParams =
937                        GetSubParameters(param.Substring(equalPos + 1));
938                    foreach (KeyValuePair<string, string> kvp in subParams)
939                        if (kvp.Value == null && target.Drive == null)
940                            target.Drive = Path.GetFullPath(kvp.Key);
941                        else if (kvp.Key == "clusterTips")
942                            target.EraseClusterTips = true;
943                        else
944                            throw new ArgumentException("Unknown subparameter: " + kvp.Key);
945                    Targets.Add(target);
946                }
947                else if (IsParam(param, "dir", "d") || IsParam(param, "directory", null))
948                {
949                    if (equalPos == -1)
950                        throw new ArgumentException("--directory must be specified with the " +
951                            "directory to erase.");
952
953                    //Create the base target
954                    FolderTarget target = new FolderTarget();
955
956                    //Parse the subparameters.
957                    List<KeyValuePair<string, string>> subParams =
958                        GetSubParameters(param.Substring(equalPos + 1));
959                    foreach (KeyValuePair<string, string> kvp in subParams)
960                        if (kvp.Value == null && target.Path == null)
961                            target.Path = Path.GetFullPath(kvp.Key);
962                        else if (kvp.Key == "excludeMask")
963                        {
964                            if (kvp.Value == null)
965                                throw new ArgumentException("The exclude mask must be specified " +
966                                    "if the excludeMask subparameter is specified");
967                            target.ExcludeMask = kvp.Value;
968                        }
969                        else if (kvp.Key == "includeMask")
970                        {
971                            if (kvp.Value == null)
972                                throw new ArgumentException("The include mask must be specified " +
973                                    "if the includeMask subparameter is specified");
974                            target.IncludeMask = kvp.Value;
975                        }
976                        else if (kvp.Key == "delete")
977                            target.DeleteIfEmpty = true;
978                        else
979                            throw new ArgumentException("Unknown subparameter: " + kvp.Key);
980
981                    //Add the target to the list of targets
982                    Targets.Add(target);
983                }
984                else if (IsParam(param, "file", "f"))
985                {
986                    if (equalPos == -1)
987                        throw new ArgumentException("--file must be specified with the " +
988                            "file to erase.");
989
990                    //It's just a file!
991                    FileTarget target = new FileTarget();
992
993                    //Parse the subparameters.
994                    List<KeyValuePair<string, string>> subParams =
995                        GetSubParameters(param.Substring(equalPos + 1));
996                    foreach (KeyValuePair<string, string> kvp in subParams)
997                        if (kvp.Value == null && target.Path == null)
998                            target.Path = Path.GetFullPath(kvp.Key);
999                        else
1000                            throw new ArgumentException("Unknown subparameter: " + kvp.Key);
1001
1002                    Targets.Add(target);
1003                }
1004                else
1005                    return false;
1006
1007                return true;
1008            }
1009
1010            /// <summary>
1011            /// The erasure method which the user specified on the command line.
1012            /// </summary>
1013            public Guid ErasureMethod { get; private set; }
1014
1015            /// <summary>
1016            /// The schedule for the current set of targets.
1017            /// </summary>
1018            public Schedule Schedule { get; private set; }
1019
1020            /// <summary>
1021            /// The list of targets which was specified on the command line.
1022            /// </summary>
1023            public List<ErasureTarget> Targets { get; private set; }
1024        }
1025
1026        /// <summary>
1027        /// Manages a command line for importing a task list into the global
1028        /// DirectExecutor.
1029        /// </summary>
1030        class ImportTaskListCommandLine : CommandLine
1031        {
1032            /// <summary>
1033            /// Constructor.
1034            /// </summary>
1035            public ImportTaskListCommandLine()
1036            {
1037            }
1038
1039            protected override bool ResolveParameter(string param)
1040            {
1041                if (!System.IO.File.Exists(param))
1042                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
1043                        "The file {0} does not exist.", param));
1044               
1045                files.Add(param);
1046                return true;
1047            }
1048
1049            public ICollection<string> Files
1050            {
1051                get
1052                {
1053                    return files.AsReadOnly();
1054                }
1055            }
1056
1057            private List<string> files = new List<string>();
1058        }
1059        #endregion
1060
1061        /// <summary>
1062        /// Constructor.
1063        /// </summary>
1064        /// <param name="cmdParams">The raw command line arguments passed to the program.</param>
1065        public CommandLineProgram(string[] cmdParams)
1066        {
1067            try
1068            {
1069                Arguments = CommandLine.Get(cmdParams);
1070
1071                //If the user did not specify the quiet command line, then create the console.
1072                if (!Arguments.Quiet)
1073                    CreateConsole();
1074
1075                //Map actions to their handlers
1076                actionHandlers.Add(typeof(AddTaskCommandLine), AddTask);
1077                actionHandlers.Add(typeof(ImportTaskListCommandLine), ImportTaskList);
1078                actionHandlers.Add(typeof(QueryMethodsCommandLine), QueryMethods);
1079                actionHandlers.Add(typeof(HelpCommandLine), Help);
1080            }
1081            finally
1082            {
1083                if (Arguments == null || !Arguments.Quiet)
1084                    CreateConsole();
1085            }
1086        }
1087
1088        /// <summary>
1089        /// Runs the program, analogous to System.Windows.Forms.Application.Run.
1090        /// </summary>
1091        public void Run()
1092        {
1093            //Call the function handling the current command line.
1094            actionHandlers[Arguments.GetType()]();
1095        }
1096
1097        /// <summary>
1098        /// Creates a console for our application, setting the input/output streams to the
1099        /// defaults.
1100        /// </summary>
1101        private static void CreateConsole()
1102        {
1103            if (KernelApi.AllocConsole())
1104            {
1105                Console.SetOut(new StreamWriter(Console.OpenStandardOutput()));
1106                Console.SetIn(new StreamReader(Console.OpenStandardInput()));
1107            }
1108        }
1109
1110        /// <summary>
1111        /// Prints the command line help for Eraser.
1112        /// </summary>
1113        private static void CommandUsage()
1114        {
1115            Console.WriteLine(@"usage: Eraser <action> <arguments>
1116where action is
1117    help                    Show this help message.
1118    addtask                 Adds tasks to the current task list.
1119    querymethods            Lists all registered Erasure methods.
1120
1121global parameters:
1122    --quiet, -q             Do not create a Console window to display progress.
1123
1124parameters for help:
1125    eraser help
1126
1127    no parameters to set.
1128
1129parameters for addtask:
1130    eraser addtask [--method=<methodGUID>] [--schedule=(now|manually|restart)] (--recycled " +
1131@"| --unused=<volume> | --dir=<directory> | --file=<file>)[...]
1132
1133    --method, -m            The Erasure method to use.
1134    --schedule, -s          The schedule the task will follow. The value must
1135                            be one of:
1136            now             The task will be queued for immediate execution.
1137            manually        The task will be created but not queued for execution.
1138            restart         The task will be queued for execution when the
1139                            computer is next restarted.
1140                            This parameter defaults to now.
1141    --recycled, -r          Erases files and folders in the recycle bin
1142    --unused, -u            Erases unused space in the volume.
1143        optional arguments: --unused=<drive>[,clusterTips]
1144            clusterTips     If specified, the drive's files will have their
1145                            cluster tips erased.
1146    --dir, --directory, -d  Erases files and folders in the directory
1147        optional arguments: --dir=<directory>[,e=excludeMask][,i=includeMask][,delete]
1148            excludeMask     A wildcard expression for files and folders to
1149                            exclude.
1150            includeMask     A wildcard expression for files and folders to
1151                            include.
1152                            The include mask is applied before the exclude
1153                            mask.
1154            delete          Deletes the folder at the end of the erasure if
1155                            specified.
1156    --file, -f              Erases the specified file
1157
1158parameters for querymethods:
1159    eraser querymethods
1160
1161    no parameters to set.
1162
1163All arguments are case sensitive.");
1164            Console.Out.Flush();
1165        }
1166
1167        #region Action Handlers
1168        /// <summary>
1169        /// The command line arguments passed to the program.
1170        /// </summary>
1171        public CommandLine Arguments { get; private set; }
1172
1173        /// <summary>
1174        /// Prints the help text for Eraser (with copyright)
1175        /// </summary>
1176        private void Help()
1177        {
1178            Console.WriteLine(@"Eraser {0}
1179(c) 2008 The Eraser Project
1180Eraser is Open-Source Software: see http://eraser.heidi.ie/ for details.
1181", Assembly.GetExecutingAssembly().GetName().Version);
1182
1183            Console.Out.Flush();
1184            CommandUsage();
1185        }
1186
1187        /// <summary>
1188        /// Lists all registered erasure methods.
1189        /// </summary>
1190        /// <param name="commandLine">The command line parameters passed to the program.</param>
1191        private void QueryMethods()
1192        {
1193            //Output the header
1194            const string methodFormat = "{0,-2} {1,-39} {2}";
1195            Console.WriteLine(methodFormat, "", "Method", "GUID");
1196            Console.WriteLine(new string('-', 79));
1197
1198            //Refresh the list of erasure methods
1199            Dictionary<Guid, ErasureMethod> methods = ErasureMethodManager.Items;
1200            foreach (ErasureMethod method in methods.Values)
1201            {
1202                Console.WriteLine(methodFormat, (method is UnusedSpaceErasureMethod) ?
1203                    "U" : "", method.Name, method.Guid.ToString());
1204            }
1205        }
1206
1207        /// <summary>
1208        /// Parses the command line for tasks and adds them using the
1209        /// <see cref="RemoteExecutor"/> class.
1210        /// </summary>
1211        /// <param name="commandLine">The command line parameters passed to the program.</param>
1212        private void AddTask()
1213        {
1214            AddTaskCommandLine taskArgs = (AddTaskCommandLine)Arguments;
1215           
1216            //Create the task, and set the method to use.
1217            Task task = new Task();
1218            ErasureMethod method = taskArgs.ErasureMethod == Guid.Empty ? 
1219                ErasureMethodManager.Default :
1220                ErasureMethodManager.GetInstance(taskArgs.ErasureMethod);
1221
1222            foreach (ErasureTarget target in taskArgs.Targets)
1223            {
1224                target.Method = method;
1225                task.Targets.Add(target);
1226            }
1227
1228            //Check the number of tasks in the task.
1229            if (task.Targets.Count == 0)
1230                throw new ArgumentException("Tasks must contain at least one erasure target.");
1231
1232            //Set the schedule for the task.
1233            task.Schedule = taskArgs.Schedule;
1234
1235            //Send the task out.
1236            try
1237            {
1238                using (RemoteExecutorClient client = new RemoteExecutorClient())
1239                {
1240                    client.Run();
1241                    if (!client.IsConnected)
1242                    {
1243                        //The client cannot connect to the server. This probably means
1244                        //that the server process isn't running. Start an instance.
1245                        Process eraserInstance = Process.Start(
1246                            Assembly.GetExecutingAssembly().Location, "--quiet");
1247                        Thread.Sleep(0);
1248                        eraserInstance.WaitForInputIdle();
1249
1250                        client.Run();
1251                        if (!client.IsConnected)
1252                            throw new IOException("Eraser cannot connect to the running " +
1253                                "instance for erasures.");
1254                    }
1255
1256                    client.Tasks.Add(task);
1257                }
1258            }
1259            catch (UnauthorizedAccessException e)
1260            {
1261                //We can't connect to the pipe because the other instance of Eraser
1262                //is running with higher privileges than this instance.
1263                throw new UnauthorizedAccessException("Another instance of Eraser " +
1264                    "is already running but it is running with higher privileges than " +
1265                    "this instance of Eraser. Tasks cannot be added in this manner.\n\n" +
1266                    "Close the running instance of Eraser and start it again without " +
1267                    "administrator privileges, or run the command again as an " +
1268                    "administrator.", e);
1269            }
1270        }
1271
1272        /// <summary>
1273        /// Imports the given tasklists and adds them to the global Eraser instance.
1274        /// </summary>
1275        private void ImportTaskList()
1276        {
1277            ImportTaskListCommandLine cmdLine = (ImportTaskListCommandLine)Arguments;
1278
1279            //Import the task list
1280            try
1281            {
1282                using (RemoteExecutorClient client = new RemoteExecutorClient())
1283                {
1284                    client.Run();
1285                    if (!client.IsConnected)
1286                    {
1287                        //The client cannot connect to the server. This probably means
1288                        //that the server process isn't running. Start an instance.
1289                        Process eraserInstance = Process.Start(
1290                            Assembly.GetExecutingAssembly().Location, "--quiet");
1291                        eraserInstance.WaitForInputIdle();
1292
1293                        client.Run();
1294                        if (!client.IsConnected)
1295                            throw new IOException("Eraser cannot connect to the running " +
1296                                "instance for erasures.");
1297                    }
1298
1299                    foreach (string path in cmdLine.Files)
1300                        using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read))
1301                            client.Tasks.LoadFromStream(stream);
1302                }
1303            }
1304            catch (UnauthorizedAccessException e)
1305            {
1306                //We can't connect to the pipe because the other instance of Eraser
1307                //is running with higher privileges than this instance.
1308                throw new UnauthorizedAccessException("Another instance of Eraser " +
1309                    "is already running but it is running with higher privileges than " +
1310                    "this instance of Eraser. Tasks cannot be added in this manner.\n\n" +
1311                    "Close the running instance of Eraser and start it again without " +
1312                    "administrator privileges, or run the command again as an " +
1313                    "administrator.", e);
1314            }
1315        }
1316        #endregion
1317
1318        /// <summary>
1319        /// The prototype of an action handler in the class which executes an
1320        /// action as specified in the command line.
1321        /// </summary>
1322        private delegate void ActionHandler();
1323
1324        /// <summary>
1325        /// Matches an action handler to a function in the class.
1326        /// </summary>
1327        private Dictionary<Type, ActionHandler> actionHandlers =
1328            new Dictionary<Type, ActionHandler>();
1329    }
1330}
Note: See TracBrowser for help on using the repository browser.