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

Revision 2661, 14.4 KB checked in by lowjoel, 2 years ago (diff)

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