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

Revision 2515, 14.2 KB checked in by lowjoel, 2 years ago (diff)

Set svn:keywords and svn:eol-style on all the source files.

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