source: trunk/eraser6/Eraser/Program.cs @ 1192

Revision 1192, 46.4 KB checked in by lowjoel, 5 years ago (diff)

The Eraser client didn't understand the new parameter format where everything needs to be escaped - this revision fixes that (only file erasures were affected)

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