source: trunk/eraser/Eraser/Program.GuiProgram.cs @ 2538

Revision 2538, 14.1 KB checked in by lowjoel, 2 years ago (diff)

Move the call to Application.EnableVisualStyles? to the start of the program so that if a console window creates a WinForm?, it would have the correct behaviour.
Fixing this bug also fixes the drawing problems associated with the display of the shell erase confirmation dialog.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Rev URL
Line 
1/*
2 * $Id$
3 * Copyright 2008-2012 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.Globalization;
31using System.ComponentModel;
32using System.Security.Principal;
33using System.Security.AccessControl;
34
35using Eraser.Util;
36
37namespace Eraser
38{
39    internal static partial class Program
40    {
41        /// <summary>
42        /// Manages a global single instance of a Windows Form application.
43        /// </summary>
44        class GuiProgram : IDisposable
45        {
46            /// <summary>
47            /// Constructor.
48            /// </summary>
49            /// <param name="commandLine">The command line arguments associated with
50            /// this program launch</param>
51            /// <param name="instanceID">The instance ID of the program, used to group
52            /// instances of the program together.</param>
53            public GuiProgram(string[] commandLine, string instanceID)
54            {
55                InstanceID = instanceID;
56                CommandLine = commandLine;
57
58                //Check if there already is another instance of the program.
59                bool isFirstInstance = false;
60                GlobalMutex = new Mutex(true, instanceID, out isFirstInstance);
61                IsFirstInstance = isFirstInstance;
62            }
63
64            #region IDisposable Interface
65            ~GuiProgram()
66            {
67                Dispose(false);
68            }
69
70            protected virtual void Dispose(bool disposing)
71            {
72                if (GlobalMutex == null)
73                    return;
74
75                if (disposing)
76                    GlobalMutex.Close();
77                GlobalMutex = null;
78            }
79
80            public void Dispose()
81            {
82                Dispose(true);
83                GC.SuppressFinalize(this);
84            }
85            #endregion
86
87            /// <summary>
88            /// Runs the event loop of the GUI program, returning true if the program
89            /// was started as there were no other instances of the program, or false
90            /// if other instances were found.
91            /// </summary>
92            /// <remarks>
93            /// This function must always be called in your program, regardless
94            /// of the value of <see cref="IsAlreadyRunning"/>. If this function is not
95            /// called, the first instance will never be notified that another was started.
96            /// </remarks>
97            public void Run()
98            {
99                //If no other instances are running, set up our pipe server so clients
100                //can connect and give us subsequent command lines.
101                if (IsFirstInstance)
102                {
103                    //Initialise and run the program.
104                    InitInstanceEventArgs eventArgs = new InitInstanceEventArgs();
105                    OnInitInstance(this, eventArgs);
106                    if (MainForm == null)
107                        return;
108
109                    try
110                    {
111                        //Create the pipe server which will handle connections to us
112                        PipeServer = new Thread(ServerMain);
113                        PipeServer.Start();
114
115                        //Handle the exit instance event. This will occur when the main form
116                        //has been closed.
117                        Application.ApplicationExit += OnExitInstance;
118                        MainForm.FormClosed += OnExitInstance;
119
120                        if (eventArgs.ShowMainForm)
121                            Application.Run(MainForm);
122                        else
123                            Application.Run();
124                    }
125                    finally
126                    {
127                        if (PipeServer != null)
128                            PipeServer.Abort();
129                    }
130                }
131
132                //Another instance of the program is running. Connect to it and transfer
133                //the command line arguments
134                else
135                {
136                    try
137                    {
138                        NamedPipeClientStream client = new NamedPipeClientStream(".", InstanceID,
139                            PipeDirection.Out);
140                        client.Connect(500);
141
142                        StringBuilder commandLineStr = new StringBuilder(CommandLine.Length * 64);
143                        foreach (string param in CommandLine)
144                            commandLineStr.Append(string.Format(
145                                CultureInfo.InvariantCulture, "{0}\0", param));
146
147                        byte[] buffer = new byte[commandLineStr.Length];
148                        int count = Encoding.UTF8.GetBytes(commandLineStr.ToString(), 0,
149                            commandLineStr.Length, buffer, 0);
150                        client.Write(buffer, 0, count);
151                    }
152                    catch (UnauthorizedAccessException)
153                    {
154                        //We can't connect to the pipe because the other instance of Eraser
155                        //is running with higher privileges than this instance. Tell the
156                        //user this is the case and show him how to resolve the issue.
157                        MessageBox.Show(S._("Another instance of Eraser is already running but it " +
158                            "is running with higher privileges than this instance of Eraser.\n\n" +
159                            "Eraser will now exit."), S._("Eraser"), MessageBoxButtons.OK,
160                            MessageBoxIcon.Information, MessageBoxDefaultButton.Button1,
161                            Localisation.IsRightToLeft(null) ?
162                                MessageBoxOptions.RtlReading | MessageBoxOptions.RightAlign : 0);
163                    }
164                    catch (IOException ex)
165                    {
166                        MessageBox.Show(S._("Another instance of Eraser is already running but " +
167                            "cannot be connected to.\n\nThe error returned was: {0}", ex.Message),
168                            S._("Eraser"), MessageBoxButtons.OK, MessageBoxIcon.Error,
169                            MessageBoxDefaultButton.Button1,
170                            Localisation.IsRightToLeft(null) ?
171                                MessageBoxOptions.RtlReading | MessageBoxOptions.RightAlign : 0);
172                    }
173                    catch (TimeoutException)
174                    {
175                        //Can't do much: half a second is a reasonably long time to wait.
176                    }
177                }
178            }
179
180            #region Next instance processing
181            /// <summary>
182            /// Holds information required for an asynchronous call to
183            /// NamedPipeServerStream.BeginWaitForConnection.
184            /// </summary>
185            private struct ServerAsyncInfo
186            {
187                public NamedPipeServerStream Server;
188                public AutoResetEvent WaitHandle;
189            }
190
191            /// <summary>
192            /// Runs a background thread, monitoring for new connections to the server.
193            /// </summary>
194            private void ServerMain()
195            {
196                while (PipeServer.ThreadState != System.Threading.ThreadState.AbortRequested)
197                {
198                    PipeSecurity security = new PipeSecurity();
199                    security.AddAccessRule(new PipeAccessRule(
200                        WindowsIdentity.GetCurrent().User,
201                        PipeAccessRights.FullControl,
202                        AccessControlType.Allow));
203
204                    using (NamedPipeServerStream server = new NamedPipeServerStream(InstanceID,
205                        PipeDirection.In, 1, PipeTransmissionMode.Message, PipeOptions.Asynchronous,
206                        128, 128, security))
207                    {
208                        ServerAsyncInfo async = new ServerAsyncInfo();
209                        async.Server = server;
210                        async.WaitHandle = new AutoResetEvent(false);
211                        IAsyncResult result = server.BeginWaitForConnection(WaitForConnection, async);
212
213                        //Wait for the operation to complete.
214                        if (result.AsyncWaitHandle.WaitOne())
215                            //It completed. Wait for the processing to complete.
216                            async.WaitHandle.WaitOne();
217                    }
218                }
219            }
220
221            /// <summary>
222            /// Waits for new connections to be made to the server.
223            /// </summary>
224            /// <param name="result"></param>
225            private void WaitForConnection(IAsyncResult result)
226            {
227                ServerAsyncInfo async = (ServerAsyncInfo)result.AsyncState;
228
229                try
230                {
231                    //We're done waiting for the connection
232                    async.Server.EndWaitForConnection(result);
233
234                    //Process the connection if the server was successfully connected.
235                    if (async.Server.IsConnected)
236                    {
237                        //Read the message from the secondary instance
238                        byte[] buffer = new byte[8192];
239                        string[] commandLine = null;
240                        string message = string.Empty;
241
242                        do
243                        {
244                            int lastRead = async.Server.Read(buffer, 0, buffer.Length);
245                            message += Encoding.UTF8.GetString(buffer, 0, lastRead);
246                        }
247                        while (!async.Server.IsMessageComplete);
248
249                        //Let the event handler process the message.
250                        OnNextInstance(this, new NextInstanceEventArgs(commandLine, message));
251                    }
252                }
253                catch (ObjectDisposedException)
254                {
255                }
256                finally
257                {
258                    //Reset the wait event
259                    async.WaitHandle.Set();
260                }
261            }
262            #endregion
263
264            /// <summary>
265            /// Gets the command line arguments this instance was started with.
266            /// </summary>
267            public string[] CommandLine { get; private set; }
268
269            /// <summary>
270            /// Gets whether another instance of the program is already running.
271            /// </summary>
272            public bool IsFirstInstance { get; private set; }
273
274            /// <summary>
275            /// The main form for this program instance. This form will be shown when
276            /// run is called if it is non-null and if its Visible property is true.
277            /// </summary>
278            public Form MainForm { get; set; }
279
280            #region Events
281            /// <summary>
282            /// The prototype of event handlers procesing the InitInstance event.
283            /// </summary>
284            /// <param name="sender">The sender of the event.</param>
285            /// <param name="e">Event arguments.</param>
286            /// <returns>True if the MainForm property holds a valid reference to
287            /// a form, and the form should be displayed to the user.</returns>
288            public delegate void InitInstanceEventHandler(object sender, InitInstanceEventArgs e);
289
290            /// <summary>
291            /// The event object managing listeners to the instance initialisation event.
292            /// This event is raised when the first instance of the program is started
293            /// and this is where the program initialisation code should be. When this
294            /// event is raised, the program should set the <see cref="GUIProgram.MainForm"/>
295            /// property to the program's main form. If the property remains null at the
296            /// end of the event, the program instance will quit. If the
297            /// <see cref="GUIProgram.MainForm.Visible"/> property is set to false,
298            /// the main form will not be shown to the user by default.
299            /// </summary>
300            public event InitInstanceEventHandler InitInstance;
301
302            /// <summary>
303            /// Broadcasts the InitInstance event.
304            /// </summary>
305            /// <param name="sender">The sender of the event.</param>
306            /// <param name="e">Event arguments.</param>
307            /// <returns>True if the MainForm object should be shown.</returns>
308            private void OnInitInstance(object sender, InitInstanceEventArgs e)
309            {
310                if (InitInstance != null)
311                    InitInstance(sender, e);
312            }
313
314            /// <summary>
315            /// The prototype of event handlers procesing the NextInstance event.
316            /// </summary>
317            /// <param name="sender">The sender of the event</param>
318            /// <param name="e">Event arguments.</param>
319            public delegate void NextInstanceEventHandler(object sender, NextInstanceEventArgs e);
320
321            /// <summary>
322            /// The event object managing listeners to the next instance event. This
323            /// event is raised when a second instance of the program is started.
324            /// </summary>
325            public event NextInstanceEventHandler NextInstance;
326
327            /// <summary>
328            /// Broadcasts the NextInstance event.
329            /// </summary>
330            /// <param name="sender">The sender of the event.</param>
331            /// <param name="e">Event arguments.</param>
332            private void OnNextInstance(object sender, NextInstanceEventArgs e)
333            {
334                if (NextInstance != null)
335                    NextInstance(sender, e);
336            }
337
338            /// <summary>
339            /// The prototype of event handlers procesing the ExitInstance event.
340            /// </summary>
341            /// <param name="sender">The sender of the event.</param>
342            /// <param name="e">Event arguments.</param>
343            public delegate void ExitInstanceEventHandler(object sender, EventArgs e);
344
345            /// <summary>
346            /// The event object managing listeners to the exit instance event. This
347            /// event is raised when the first instance of the program is exited.
348            /// </summary>
349            public event ExitInstanceEventHandler ExitInstance;
350
351            /// <summary>
352            /// Broadcasts the ExitInstance event after getting the notification that the
353            /// application is exiting. This event is broadcast only if the program
354            /// completed initialisation.
355            /// </summary>
356            /// <seealso cref="InitInstance"/>
357            /// <param name="sender">The sender of the event.</param>
358            /// <param name="e">Event arguments.</param>
359            private void OnExitInstance(object sender, EventArgs e)
360            {
361                //If the exit event has been broadcast don't repeat.
362                if (Exited)
363                    return;
364
365                Exited = true;
366                if (ExitInstance != null)
367                    ExitInstance(sender, e);
368
369                if (!MainForm.Disposing)
370                    MainForm.Dispose();
371            }
372            #endregion
373
374            #region Instance variables
375            /// <summary>
376            /// The Instance ID of this program, used to group program instances together.
377            /// </summary>
378            private string InstanceID;
379
380            /// <summary>
381            /// The named mutex ensuring that only one instance of the application runs
382            /// at a time.
383            /// </summary>
384            private Mutex GlobalMutex;
385
386            /// <summary>
387            /// The thread maintaining the pipe server for secondary instances to connect to.
388            /// </summary>
389            private Thread PipeServer;
390
391            /// Tracks whether the Exit event has been broadcast. It should only be broadcast
392            /// once in the lifetime of the application.
393            private bool Exited;
394            #endregion
395        }
396
397        /// <summary>
398        /// Holds event data for the <see cref="GUIProgram.InitInstance"/> event.
399        /// </summary>
400        class InitInstanceEventArgs : EventArgs
401        {
402            /// <summary>
403            /// Constructor.
404            /// </summary>
405            public InitInstanceEventArgs()
406            {
407                ShowMainForm = true;
408            }
409
410            /// <summary>
411            /// Gets or sets whether the main form should be shown when the program
412            /// is initialised.
413            /// </summary>
414            public bool ShowMainForm { get; set; }
415        }
416
417        /// <summary>
418        /// Holds event data for the <see cref="GUIProgram.NextInstance"/> event.
419        /// </summary>
420        class NextInstanceEventArgs : EventArgs
421        {
422            /// <summary>
423            /// Constructor.
424            /// </summary>
425            /// <param name="commandLine">The command line that the next instance was
426            /// started with.</param>
427            /// <param name="message">The message that the next instance wanted
428            /// displayed.</param>
429            public NextInstanceEventArgs(string[] commandLine, string message)
430            {
431                CommandLine = commandLine;
432                Message = message;
433            }
434
435            /// <summary>
436            /// The command line that the next instance was executed with.
437            /// </summary>
438            public string[] CommandLine { get; private set; }
439
440            /// <summary>
441            /// The message that the next instance wanted to display, but since a first
442            /// instance already started, it was suppressed.
443            /// </summary>
444            public string Message { get; private set; }
445        }
446    }
447}
Note: See TracBrowser for help on using the repository browser.