source: trunk/eraser6/Eraser/Program.GuiProgram.cs @ 1802

Revision 1802, 14.1 KB checked in by lowjoel, 5 years ago (diff)

Merged the CodeReview? Branch back to trunk. (Finally!) Closes #275: Code Review.

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