Welcome to mirror list, hosted at ThFree Co, Russian Federation.

SingleInstance.cs « Server « Duplicati - github.com/duplicati/duplicati.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 8fb16f8fd4b351cbdec22acde0633a85c1e44225 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
#region Disclaimer / License
// Copyright (C) 2015, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
// 
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
// 
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
// 
#endregion
using System;
using System.Collections.Generic;
using System.Text;
using Duplicati.Library.IO;
using Duplicati.Library.Utility;

namespace Duplicati.Server
{
    /// <summary>
    /// The purpose of this class is to ensure that there is ever only one instance of the application running.
    /// This class is based on file-locking, making it much more cross-platform portable than other versions,
    /// that depend on memory communication, such as pipes or shared memory.
    /// </summary>
    public class SingleInstance : IDisposable
    {
        /// <summary>
        /// An exception that can be thrown to indicate a second instance was running
        /// </summary>
        [Serializable]
        public class MultipleInstanceException : Exception
        {
            /// <summary>
            /// Constructs the new exception
            /// </summary>
            public MultipleInstanceException()
                : base()
            {
            }

            /// <summary>
            /// Constructs the new exception
            /// </summary>
            /// <param name="message">The message</param>
            public MultipleInstanceException(string message)
                : base(message)
            {
            }

            /// <summary>
            /// Constructs the new exception
            /// </summary>
            /// <param name="message">The message</param>
            /// <param name="innerException">The inner exception</param>
            public MultipleInstanceException(string message, Exception innerException)
                : base(message, innerException)
            {
            }
        }

        /// <summary>
        /// The folder where control files are placed
        /// </summary>
        /// <remarks>
        /// This directory is referenced in the common filters in FilterGroups.cs.
        /// If it is ever changed, the filter should be updated as well.
        /// </remarks>
        private const string CONTROL_DIR = "control_dir_v2";
        /// <summary>
        /// The file that is locked by the first process
        /// </summary>
        private const string CONTROL_FILE = "lock_v2";
        /// <summary>
        /// The prefix on files that communicate with the first instance
        /// </summary>
        private const string COMM_FILE_PREFIX = "other_invocation_v2_";

        /// <summary>
        /// The delegate that is used to inform the first instance of the second invocation
        /// </summary>
        /// <param name="commandlineargs">The commandlinearguments for the second invocation</param>
        public delegate void SecondInstanceDelegate(string[] commandlineargs);

        /// <summary>
        /// When the user tries to launch the application the second time, this event is raised
        /// </summary>
        public event SecondInstanceDelegate SecondInstanceDetected;

        /// <summary>
        /// The file that is locked to prevent other access
        /// </summary>
        private IDisposable m_file;

        /// <summary>
        /// The folder where control files are placed
        /// </summary>
        private readonly string m_controldir;

        /// <summary>
        /// The full path to the locking file
        /// </summary>
        private readonly string m_lockfilename;

        /// <summary>
        /// The watcher that allows interprocess communication
        /// </summary>
        private System.IO.FileSystemWatcher m_filewatcher;

        /// <summary>
        /// Gets a value indicating if this is the first instance of the application
        /// </summary>
        public bool IsFirstInstance { get { return m_file != null; } }

        /// <summary>
        /// Constructs a new SingleInstance object
        /// </summary>
        /// <param name="basefolder">The folder in which the control file structure is placed</param>
        ///
        public SingleInstance(string basefolder)
        {
            if (!System.IO.Directory.Exists(basefolder))
                System.IO.Directory.CreateDirectory(basefolder);

            m_controldir = System.IO.Path.Combine(basefolder, CONTROL_DIR);
            if (!System.IO.Directory.Exists(m_controldir))
                System.IO.Directory.CreateDirectory(m_controldir);

            m_lockfilename = System.IO.Path.Combine(m_controldir, CONTROL_FILE);
            m_file = null;
            
            System.IO.Stream temp_fs = null;

            try
            {
                if (Library.Utility.Utility.IsClientLinux)
                    temp_fs = UnixSupport.File.OpenExclusive(m_lockfilename, System.IO.FileAccess.Write);
                else
                    temp_fs = System.IO.File.Open(m_lockfilename, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None);
                
                if (temp_fs != null)
                {
                    System.IO.StreamWriter sw = new System.IO.StreamWriter(temp_fs);
                    sw.WriteLine(System.Diagnostics.Process.GetCurrentProcess().Id);
                    sw.Flush();
                    //Do not dispose sw as that would dispose the stream
                    m_file = temp_fs;
                }
            }
            catch
            {
                if (temp_fs != null)
                    try { temp_fs.Dispose(); }
                    catch {}
            }

            //If we have write access
            if (m_file != null)
            {
                m_filewatcher = new System.IO.FileSystemWatcher(m_controldir);
                m_filewatcher.Created += new System.IO.FileSystemEventHandler(m_filewatcher_Created);
                m_filewatcher.EnableRaisingEvents = true;

                DateTime startup = System.IO.File.GetLastWriteTime(m_lockfilename);

                //Clean up any files that were created before the app launched
                foreach(string s in SystemIO.IO_OS(Utility.IsClientWindows).GetFiles(m_controldir))
                    if (s != m_lockfilename && System.IO.File.GetCreationTime(s) < startup)
                        try { System.IO.File.Delete(s); }
                        catch { }
            }
            else
            {
                //Wait for the initial process to signal that the filewatcher is activated
                int retrycount = 5;
                while (retrycount > 0 && new System.IO.FileInfo(m_lockfilename).Length == 0)
                {
                    System.Threading.Thread.Sleep(500);
                    retrycount--;
                }

                //HACK: the unix file lock does not allow us to read the file length when the file is locked
                if (new System.IO.FileInfo(m_lockfilename).Length == 0)
                    if (!Library.Utility.Utility.IsClientLinux)
                        throw new Exception("The file was locked, but had no data");

                //Notify the other process that we have started
                string filename = System.IO.Path.Combine(m_controldir, COMM_FILE_PREFIX + Guid.NewGuid().ToString());

                //Write out the commandline arguments
                string[] cmdargs = System.Environment.GetCommandLineArgs();
                using (System.IO.StreamWriter sw = new System.IO.StreamWriter(Library.Utility.Utility.IsClientLinux ? UnixSupport.File.OpenExclusive(filename, System.IO.FileAccess.Write) : new System.IO.FileStream(filename, System.IO.FileMode.CreateNew, System.IO.FileAccess.Write, System.IO.FileShare.None)))
                    for (int i = 1; i < cmdargs.Length; i++) //Skip the first, as that is the filename
                        sw.WriteLine(cmdargs[i]);

                //Wait for the other process to delete the file, indicating that it is processed
                retrycount = 5;
                while (retrycount > 0 && System.IO.File.Exists(filename))
                {
                    System.Threading.Thread.Sleep(500);
                    retrycount--;
                }

                //This may happen if the other process is closing as we write the command
                if (System.IO.File.Exists(filename))
                {
                    //Try to clean up, so the other process does not spuriously show this
                    try { System.IO.File.Delete(filename); }
                    catch { }

                    throw new Exception("The lock file was locked, but the locking process did not respond to the start command");
                }
            }
        }

        /// <summary>
        /// The event that is raised when a new file is created in the control dir
        /// </summary>
        /// <param name="sender">The event sender</param>
        /// <param name="e">The file event arguments</param>
        private void m_filewatcher_Created(object sender, System.IO.FileSystemEventArgs e)
        {
            //Retry 5 times if the other process is slow on releasing the file lock
            int retrycount = 5;
            //Indicator and holder of arguments passed
            string[] commandline = null;

            //HACK: Linux has some locking issues
            //The problem is that there is no atomic open-and-lock operation, so the other process
            // needs a little time to create+lock the file. This is not really a fix, but an
            // ugly workaround. This functionality is only used to allow a new instance to signal
            // the running instance, so errors here would only affect that functionality
            if (Library.Utility.Utility.IsClientLinux)
                System.Threading.Thread.Sleep(1000);

            do
            {
                try
                {
                    //If the other process deleted the file, just quit
                    if (!System.IO.File.Exists(e.FullPath))
                        return;

                    List<string> args = new List<string>();
                    using (System.IO.StreamReader sr = new System.IO.StreamReader(Duplicati.Library.Utility.Utility.IsClientLinux ? UnixSupport.File.OpenExclusive(e.FullPath, System.IO.FileAccess.ReadWrite) : new System.IO.FileStream(e.FullPath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.None)))
                    while(!sr.EndOfStream)
                        args.Add(sr.ReadLine());

                    commandline = args.ToArray();

                    //Remove the file to notify the other process that we have correctly processed the call
                    System.IO.File.Delete(e.FullPath);
                }
                catch
                {
                }

                //If file-reading failed, wait a little before retry
                if (commandline == null)
                {
                    System.Threading.Thread.Sleep(500);
                    retrycount--;
                }

            } while (retrycount > 0 && commandline == null);

            //If this happens, we detected the file, but was unable to read it's contents
            if (commandline == null)
            {
                //There is nothing we can do :(
            }
            else
            {
                //If we read the data but did not delete the file, the other end still hangs
                //and waits for us to clean up, so try again.
                retrycount = 5;
                while (retrycount > 0 && System.IO.File.Exists(e.FullPath))
                {
                    try
                    {
                        System.IO.File.Delete(e.FullPath);
                    }
                    catch
                    {
                        //Wait before the retry
                        System.Threading.Thread.Sleep(500);
                    }

                    retrycount--;
                }

                //If this happens, the other process will give an error message
                if (System.IO.File.Exists(e.FullPath))
                {
                    //There is nothing we can do :(
                }

                //Finally inform this instance about the call
                if (SecondInstanceDetected != null)
                    SecondInstanceDetected(commandline); 
            }

        }

        #region IDisposable Members

        public void Dispose()
        {
            if (m_filewatcher != null)
            {
                m_filewatcher.EnableRaisingEvents = false;
                m_filewatcher.Created -= new System.IO.FileSystemEventHandler(m_filewatcher_Created);
                m_filewatcher.Dispose();
                m_filewatcher = null;
            }

            if (m_file != null)
            {
                m_file.Dispose();

                try { System.IO.File.Delete(m_lockfilename); }
                catch { }

                m_file = null;
            }
        }

        #endregion
    }
}