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

Revision 1784, 39.4 KB checked in by lowjoel, 5 years ago (diff)
  • When connecting to a RemoteExecutor? instance, wait for 3 seconds instead of 0.5 seconds - the old timeout seemed too short for new instances to start and wait for connections (and WaitForInputIdle? doesn't seem to wait completely)
  • When starting the new Eraser process, yield the current thread's timeslice to allow the new instance to start
  • 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 ||                                    //No possibility of escaping
777                        (commaPos >= 1 && param[commaPos - 1] != '\\') ||   //Second character
778                        (commaPos >= 2 && param[commaPos - 2] == '\\'))     //Cannot be a \\ which is an escape
779                    {
780                        //Extract the current subparameter, and dissect the subparameter
781                        //at the first =.
782                        string subParam = param.Substring(lastPos, commaPos - lastPos);
783                        int equalPos = -1;
784
785                        do
786                        {
787                            equalPos = subParam.IndexOf('=', equalPos + 1);
788                            if (equalPos == -1)
789                            {
790                                result.Add(new KeyValuePair<string, string>(
791                                    UnescapeCommandLine(subParam), null));
792                            }
793                            else if (equalPos == 0 ||                               //No possibility of escaping
794                                (equalPos >= 1 && subParam[equalPos - 1] != '\\') ||//Second character
795                                (equalPos >= 2 && subParam[equalPos - 2] == '\\'))  //Double \\ which is an escape
796                            {
797                                result.Add(new KeyValuePair<string, string>(
798                                    UnescapeCommandLine(subParam.Substring(0, equalPos)),
799                                    UnescapeCommandLine(subParam.Substring(equalPos + 1))));
800                                break;
801                            }
802                        }
803                        while (equalPos != -1);
804                        lastPos = ++commaPos;
805                    }
806                    else
807                        ++commaPos;
808
809                    //Find the next ,
810                    commaPos = param.IndexOf(',', commaPos);
811                }
812
813                return result;
814            }
815
816            /// <summary>
817            /// Unescapes a subparameter command line, removing the extra
818            /// </summary>
819            /// <param name="param"></param>
820            /// <returns></returns>
821            private static string UnescapeCommandLine(string param)
822            {
823                StringBuilder result = new StringBuilder(param.Length);
824                for (int i = 0; i < param.Length; ++i)
825                    if (param[i] == '\\' && i < param.Length - 1)
826                        result.Append(param[++i]);
827                    else
828                        result.Append(param[i]);
829                return result.ToString();
830            }
831
832            /// <summary>
833            /// True if no console window should be created.
834            /// </summary>
835            public bool Quiet { get; private set; }
836        }
837
838        /// <summary>
839        /// Manages a help query command line.
840        /// </summary>
841        class HelpCommandLine : CommandLine
842        {
843            public HelpCommandLine()
844            {
845            }
846        }
847
848        class QueryMethodsCommandLine : CommandLine
849        {
850            public QueryMethodsCommandLine()
851            {
852            }
853        }
854
855        /// <summary>
856        /// Manages a command line for adding tasks to the global DirectExecutor
857        /// </summary>
858        class AddTaskCommandLine : CommandLine
859        {
860            /// <summary>
861            /// Constructor.
862            /// </summary>
863            public AddTaskCommandLine()
864            {
865                Schedule = Schedule.RunNow;
866                Targets = new List<ErasureTarget>();
867            }
868
869            protected override bool ResolveParameter(string param)
870            {
871                int equalPos = param.IndexOf('=');
872                if (IsParam(param, "method", "m"))
873                {
874                    if (equalPos == -1)
875                        throw new ArgumentException("--method must be specified with an Erasure " +
876                            "method GUID.");
877
878                    List<KeyValuePair<string, string>> subParams =
879                        GetSubParameters(param.Substring(equalPos + 1));
880                    ErasureMethod = new Guid(subParams[0].Key);
881                }
882                else if (IsParam(param, "schedule", "s"))
883                {
884                    if (equalPos == -1)
885                        throw new ArgumentException("--schedule must be specified with a Schedule " +
886                            "type.");
887
888                    List<KeyValuePair<string, string>> subParams =
889                        GetSubParameters(param.Substring(equalPos + 1));
890                    switch (subParams[0].Key)
891                    {
892                        case "now":
893                            Schedule = Schedule.RunNow;
894                            break;
895                        case "manually":
896                            Schedule = Schedule.RunManually;
897                            break;
898                        case "restart":
899                            Schedule = Schedule.RunOnRestart;
900                            break;
901                        default:
902                            throw new ArgumentException("Unknown schedule type: " + subParams[0].Key);
903                    }
904                }
905                else if (IsParam(param, "recycled", "r"))
906                {
907                    Targets.Add(new RecycleBinTarget());
908                }
909                else if (IsParam(param, "unused", "u"))
910                {
911                    if (equalPos == -1)
912                        throw new ArgumentException("--unused must be specified with the Volume " +
913                            "to erase.");
914
915                    //Create the UnusedSpace target for inclusion into the task.
916                    UnusedSpaceTarget target = new UnusedSpaceTarget();
917
918                    //Determine if cluster tips should be erased.
919                    target.EraseClusterTips = false;
920                    List<KeyValuePair<string, string>> subParams =
921                        GetSubParameters(param.Substring(equalPos + 1));
922                    foreach (KeyValuePair<string, string> kvp in subParams)
923                        if (kvp.Value == null && target.Drive == null)
924                            target.Drive = Path.GetFullPath(kvp.Key);
925                        else if (kvp.Key == "clusterTips")
926                            target.EraseClusterTips = true;
927                        else
928                            throw new ArgumentException("Unknown subparameter: " + kvp.Key);
929                    Targets.Add(target);
930                }
931                else if (IsParam(param, "dir", "d") || IsParam(param, "directory", null))
932                {
933                    if (equalPos == -1)
934                        throw new ArgumentException("--directory must be specified with the " +
935                            "directory to erase.");
936
937                    //Create the base target
938                    FolderTarget target = new FolderTarget();
939
940                    //Parse the subparameters.
941                    List<KeyValuePair<string, string>> subParams =
942                        GetSubParameters(param.Substring(equalPos + 1));
943                    foreach (KeyValuePair<string, string> kvp in subParams)
944                        if (kvp.Value == null && target.Path == null)
945                            target.Path = Path.GetFullPath(kvp.Key);
946                        else if (kvp.Key == "excludeMask")
947                        {
948                            if (kvp.Value == null)
949                                throw new ArgumentException("The exclude mask must be specified " +
950                                    "if the excludeMask subparameter is specified");
951                            target.ExcludeMask = kvp.Value;
952                        }
953                        else if (kvp.Key == "includeMask")
954                        {
955                            if (kvp.Value == null)
956                                throw new ArgumentException("The include mask must be specified " +
957                                    "if the includeMask subparameter is specified");
958                            target.IncludeMask = kvp.Value;
959                        }
960                        else if (kvp.Key == "delete")
961                            target.DeleteIfEmpty = true;
962                        else
963                            throw new ArgumentException("Unknown subparameter: " + kvp.Key);
964
965                    //Add the target to the list of targets
966                    Targets.Add(target);
967                }
968                else if (IsParam(param, "file", "f"))
969                {
970                    if (equalPos == -1)
971                        throw new ArgumentException("--file must be specified with the " +
972                            "file to erase.");
973
974                    //It's just a file!
975                    FileTarget target = new FileTarget();
976
977                    //Parse the subparameters.
978                    List<KeyValuePair<string, string>> subParams =
979                        GetSubParameters(param.Substring(equalPos + 1));
980                    foreach (KeyValuePair<string, string> kvp in subParams)
981                        if (kvp.Value == null && target.Path == null)
982                            target.Path = Path.GetFullPath(kvp.Key);
983                        else
984                            throw new ArgumentException("Unknown subparameter: " + kvp.Key);
985
986                    Targets.Add(target);
987                }
988                else
989                    return false;
990
991                return true;
992            }
993
994            /// <summary>
995            /// The erasure method which the user specified on the command line.
996            /// </summary>
997            public Guid ErasureMethod { get; private set; }
998
999            /// <summary>
1000            /// The schedule for the current set of targets.
1001            /// </summary>
1002            public Schedule Schedule { get; private set; }
1003
1004            /// <summary>
1005            /// The list of targets which was specified on the command line.
1006            /// </summary>
1007            public List<ErasureTarget> Targets { get; private set; }
1008        }
1009
1010        /// <summary>
1011        /// Manages a command line for importing a task list into the global
1012        /// DirectExecutor.
1013        /// </summary>
1014        class ImportTaskListCommandLine : CommandLine
1015        {
1016            /// <summary>
1017            /// Constructor.
1018            /// </summary>
1019            public ImportTaskListCommandLine()
1020            {
1021            }
1022
1023            protected override bool ResolveParameter(string param)
1024            {
1025                if (!System.IO.File.Exists(param))
1026                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
1027                        "The file {0} does not exist.", param));
1028               
1029                files.Add(param);
1030                return true;
1031            }
1032
1033            public ICollection<string> Files
1034            {
1035                get
1036                {
1037                    return files.AsReadOnly();
1038                }
1039            }
1040
1041            private List<string> files = new List<string>();
1042        }
1043        #endregion
1044
1045        /// <summary>
1046        /// Constructor.
1047        /// </summary>
1048        /// <param name="cmdParams">The raw command line arguments passed to the program.</param>
1049        public CommandLineProgram(string[] cmdParams)
1050        {
1051            try
1052            {
1053                Arguments = CommandLine.Get(cmdParams);
1054
1055                //If the user did not specify the quiet command line, then create the console.
1056                if (!Arguments.Quiet)
1057                    CreateConsole();
1058
1059                //Map actions to their handlers
1060                actionHandlers.Add(typeof(AddTaskCommandLine), AddTask);
1061                actionHandlers.Add(typeof(ImportTaskListCommandLine), ImportTaskList);
1062                actionHandlers.Add(typeof(QueryMethodsCommandLine), QueryMethods);
1063                actionHandlers.Add(typeof(HelpCommandLine), Help);
1064            }
1065            finally
1066            {
1067                if (Arguments == null || !Arguments.Quiet)
1068                    CreateConsole();
1069            }
1070        }
1071
1072        /// <summary>
1073        /// Runs the program, analogous to System.Windows.Forms.Application.Run.
1074        /// </summary>
1075        public void Run()
1076        {
1077            //Call the function handling the current command line.
1078            actionHandlers[Arguments.GetType()]();
1079        }
1080
1081        /// <summary>
1082        /// Creates a console for our application, setting the input/output streams to the
1083        /// defaults.
1084        /// </summary>
1085        private static void CreateConsole()
1086        {
1087            if (KernelApi.AllocConsole())
1088            {
1089                Console.SetOut(new StreamWriter(Console.OpenStandardOutput()));
1090                Console.SetIn(new StreamReader(Console.OpenStandardInput()));
1091            }
1092        }
1093
1094        /// <summary>
1095        /// Prints the command line help for Eraser.
1096        /// </summary>
1097        private static void CommandUsage()
1098        {
1099            Console.WriteLine(@"usage: Eraser <action> <arguments>
1100where action is
1101    help                    Show this help message.
1102    addtask                 Adds tasks to the current task list.
1103    querymethods            Lists all registered Erasure methods.
1104
1105global parameters:
1106    --quiet, -q             Do not create a Console window to display progress.
1107
1108parameters for help:
1109    eraser help
1110
1111    no parameters to set.
1112
1113parameters for addtask:
1114    eraser addtask [--method=<methodGUID>] [--schedule=(now|manually|restart)] (--recycled " +
1115@"| --unused=<volume> | --dir=<directory> | --file=<file>)[...]
1116
1117    --method, -m            The Erasure method to use.
1118    --schedule, -s          The schedule the task will follow. The value must
1119                            be one of:
1120            now             The task will be queued for immediate execution.
1121            manually        The task will be created but not queued for execution.
1122            restart         The task will be queued for execution when the
1123                            computer is next restarted.
1124                            This parameter defaults to now.
1125    --recycled, -r          Erases files and folders in the recycle bin
1126    --unused, -u            Erases unused space in the volume.
1127        optional arguments: --unused=<drive>[,clusterTips]
1128            clusterTips     If specified, the drive's files will have their
1129                            cluster tips erased.
1130    --dir, --directory, -d  Erases files and folders in the directory
1131        optional arguments: --dir=<directory>[,e=excludeMask][,i=includeMask][,delete]
1132            excludeMask     A wildcard expression for files and folders to
1133                            exclude.
1134            includeMask     A wildcard expression for files and folders to
1135                            include.
1136                            The include mask is applied before the exclude
1137                            mask.
1138            delete          Deletes the folder at the end of the erasure if
1139                            specified.
1140    --file, -f              Erases the specified file
1141
1142parameters for querymethods:
1143    eraser querymethods
1144
1145    no parameters to set.
1146
1147All arguments are case sensitive.");
1148            Console.Out.Flush();
1149        }
1150
1151        #region Action Handlers
1152        /// <summary>
1153        /// The command line arguments passed to the program.
1154        /// </summary>
1155        public CommandLine Arguments { get; private set; }
1156
1157        /// <summary>
1158        /// Prints the help text for Eraser (with copyright)
1159        /// </summary>
1160        private void Help()
1161        {
1162            Console.WriteLine(@"Eraser {0}
1163(c) 2008 The Eraser Project
1164Eraser is Open-Source Software: see http://eraser.heidi.ie/ for details.
1165", Assembly.GetExecutingAssembly().GetName().Version);
1166
1167            Console.Out.Flush();
1168            CommandUsage();
1169        }
1170
1171        /// <summary>
1172        /// Lists all registered erasure methods.
1173        /// </summary>
1174        /// <param name="commandLine">The command line parameters passed to the program.</param>
1175        private void QueryMethods()
1176        {
1177            //Output the header
1178            const string methodFormat = "{0,-2} {1,-39} {2}";
1179            Console.WriteLine(methodFormat, "", "Method", "GUID");
1180            Console.WriteLine(new string('-', 79));
1181
1182            //Refresh the list of erasure methods
1183            Dictionary<Guid, ErasureMethod> methods = ErasureMethodManager.Items;
1184            foreach (ErasureMethod method in methods.Values)
1185            {
1186                Console.WriteLine(methodFormat, (method is UnusedSpaceErasureMethod) ?
1187                    "U" : "", method.Name, method.Guid.ToString());
1188            }
1189        }
1190
1191        /// <summary>
1192        /// Parses the command line for tasks and adds them using the
1193        /// <see cref="RemoteExecutor"/> class.
1194        /// </summary>
1195        /// <param name="commandLine">The command line parameters passed to the program.</param>
1196        private void AddTask()
1197        {
1198            AddTaskCommandLine taskArgs = (AddTaskCommandLine)Arguments;
1199           
1200            //Create the task, and set the method to use.
1201            Task task = new Task();
1202            ErasureMethod method = taskArgs.ErasureMethod == Guid.Empty ? 
1203                ErasureMethodManager.Default :
1204                ErasureMethodManager.GetInstance(taskArgs.ErasureMethod);
1205
1206            foreach (ErasureTarget target in taskArgs.Targets)
1207            {
1208                target.Method = method;
1209                task.Targets.Add(target);
1210            }
1211
1212            //Check the number of tasks in the task.
1213            if (task.Targets.Count == 0)
1214                throw new ArgumentException("Tasks must contain at least one erasure target.");
1215
1216            //Set the schedule for the task.
1217            task.Schedule = taskArgs.Schedule;
1218
1219            //Send the task out.
1220            try
1221            {
1222                using (RemoteExecutorClient client = new RemoteExecutorClient())
1223                {
1224                    client.Run();
1225                    if (!client.IsConnected)
1226                    {
1227                        //The client cannot connect to the server. This probably means
1228                        //that the server process isn't running. Start an instance.
1229                        Process eraserInstance = Process.Start(
1230                            Assembly.GetExecutingAssembly().Location, "--quiet");
1231                        Thread.Sleep(0);
1232                        eraserInstance.WaitForInputIdle();
1233
1234                        client.Run();
1235                        if (!client.IsConnected)
1236                            throw new IOException("Eraser cannot connect to the running " +
1237                                "instance for erasures.");
1238                    }
1239
1240                    client.Tasks.Add(task);
1241                }
1242            }
1243            catch (UnauthorizedAccessException e)
1244            {
1245                //We can't connect to the pipe because the other instance of Eraser
1246                //is running with higher privileges than this instance.
1247                throw new UnauthorizedAccessException("Another instance of Eraser " +
1248                    "is already running but it is running with higher privileges than " +
1249                    "this instance of Eraser. Tasks cannot be added in this manner.\n\n" +
1250                    "Close the running instance of Eraser and start it again without " +
1251                    "administrator privileges, or run the command again as an " +
1252                    "administrator.", e);
1253            }
1254        }
1255
1256        /// <summary>
1257        /// Imports the given tasklists and adds them to the global Eraser instance.
1258        /// </summary>
1259        private void ImportTaskList()
1260        {
1261            ImportTaskListCommandLine cmdLine = (ImportTaskListCommandLine)Arguments;
1262
1263            //Import the task list
1264            try
1265            {
1266                using (RemoteExecutorClient client = new RemoteExecutorClient())
1267                {
1268                    client.Run();
1269                    if (!client.IsConnected)
1270                    {
1271                        //The client cannot connect to the server. This probably means
1272                        //that the server process isn't running. Start an instance.
1273                        Process eraserInstance = Process.Start(
1274                            Assembly.GetExecutingAssembly().Location, "--quiet");
1275                        eraserInstance.WaitForInputIdle();
1276
1277                        client.Run();
1278                        if (!client.IsConnected)
1279                            throw new IOException("Eraser cannot connect to the running " +
1280                                "instance for erasures.");
1281                    }
1282
1283                    foreach (string path in cmdLine.Files)
1284                        using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read))
1285                            client.Tasks.LoadFromStream(stream);
1286                }
1287            }
1288            catch (UnauthorizedAccessException e)
1289            {
1290                //We can't connect to the pipe because the other instance of Eraser
1291                //is running with higher privileges than this instance.
1292                throw new UnauthorizedAccessException("Another instance of Eraser " +
1293                    "is already running but it is running with higher privileges than " +
1294                    "this instance of Eraser. Tasks cannot be added in this manner.\n\n" +
1295                    "Close the running instance of Eraser and start it again without " +
1296                    "administrator privileges, or run the command again as an " +
1297                    "administrator.", e);
1298            }
1299        }
1300        #endregion
1301
1302        /// <summary>
1303        /// The prototype of an action handler in the class which executes an
1304        /// action as specified in the command line.
1305        /// </summary>
1306        private delegate void ActionHandler();
1307
1308        /// <summary>
1309        /// Matches an action handler to a function in the class.
1310        /// </summary>
1311        private Dictionary<Type, ActionHandler> actionHandlers =
1312            new Dictionary<Type, ActionHandler>();
1313    }
1314}
Note: See TracBrowser for help on using the repository browser.