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

Revision 2666, 40.0 KB checked in by lowjoel, 3 years ago (diff)

Merged revision(s) 2661 from trunk/eraser: Handle the situation where creating a new mutex when starting the application would cause an UnauthorizedAccessException?. I can't figure out when this can happen since our mutex name contains the windows SID of the user launching it, but we'll have to live with it for now.

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