1 /*******************************************************************************
2 
3     PID file that prevents multiple instances of an app opening in the same dir.
4 
5     Copyright:
6         Copyright (c) 2018 dunnhumby Germany GmbH.
7         All rights reserved.
8 
9     License:
10         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
11         Alternatively, this file may be distributed under the terms of the Tango
12         3-Clause BSD License (see LICENSE_BSD.txt for details).
13 
14 *******************************************************************************/
15 
16 module ocean.application.components.PidLock;
17 
18 import ocean.meta.types.Qualifiers;
19 
20 /// ditto
21 public struct PidLock
22 {
23     import ocean.core.Enforce;
24     import ocean.meta.codegen.Identifier;
25     import ocean.text.convert.Formatter;
26     import ocean.text.util.StringC;
27     import ocean.sys.ErrnoException;
28     import ocean.util.config.ConfigParser;
29 
30     import core.stdc.errno;
31     import core.stdc.stdio;
32     import core.stdc.stdlib;
33     import core.stdc.string;
34     import core.sys.posix.unistd: write, close, ftruncate, getpid, unlink;
35     import core.sys.posix.sys.stat;
36     import core.sys.posix.fcntl;
37 
38     /***************************************************************************
39 
40         Fd referring to the pidfile.
41 
42     ***************************************************************************/
43 
44     private int lock_fd;
45 
46     /***************************************************************************
47 
48         Indicator if the pidfile is locked by this instance.
49 
50     ***************************************************************************/
51 
52     private bool is_locked;
53 
54     /***************************************************************************
55 
56         Path to the pidlock file.
57 
58     ***************************************************************************/
59 
60     private mstring pidlock_path;
61 
62     /***************************************************************************
63 
64         Parse the configuration file options to set up the pid lock.
65 
66         Params:
67             config = configuration instance
68 
69     ***************************************************************************/
70 
71     public void parseConfig ( ConfigParser config )
72     {
73         this.pidlock_path = config.get("PidLock", "path", "").dup;
74     }
75 
76     /***************************************************************************
77 
78         Tries to lock the pid file.
79 
80         Throws:
81             Exception if the locking is not successful.
82 
83     ***************************************************************************/
84 
85     public void lock ( )
86     {
87         if (this.pidlock_path.length)
88         {
89             istring msg =
90                 idup("Couldn't lock the pid lock file '" ~ this.pidlock_path
91                  ~ "'. Probably another instance of the application is running.");
92             enforce(this.tryLockPidFile(), msg);
93         }
94     }
95 
96     /***************************************************************************
97 
98         Cleans up behind and releases the lock file.
99 
100         Throws:
101             ErrnoException if any of the system calls fails
102 
103     ***************************************************************************/
104 
105     public void unlock ( )
106     {
107         if (!this.is_locked)
108         {
109             return;
110         }
111 
112         // releases the lock and closes the lock file
113         this.enforcePosix!(close)(this.lock_fd);
114         this.enforcePosix!(unlink)(StringC.toCString(this.pidlock_path));
115     }
116 
117     /***************************************************************************
118 
119         Called to try to create and lock the pidlock file. On success
120         this will create and lock the file pointed by `this.pidlock_path`,
121         and it will write out the application pid into it.
122 
123         Params:
124             pidlock = path to the pidlock file
125 
126         Returns:
127             true if the lock has been successful, false otherwise.
128 
129         Throws:
130             ErrnoException if any of the system calls fails for unexpected
131             reasons
132 
133     ***************************************************************************/
134 
135     private bool tryLockPidFile ( )
136     {
137         this.lock_fd = this.enforcePosix!(open)(StringC.toCString(this.pidlock_path),
138                 O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
139 
140         // TODO these will be available in tango v1.6.0
141         enum F_SETLK = 6;
142 
143         // Lock the pidfile
144         flock fl;
145         fl.l_type = F_WRLCK;
146         fl.l_whence = SEEK_SET;
147         fl.l_start = 0;
148         fl.l_len = 0;
149 
150         if (fcntl(this.lock_fd, F_SETLK, &fl) == -1)
151         {
152             // Region already locked, can't acquire a lock
153             if (errno == EAGAIN || errno == EACCES)
154             {
155                 return false;
156             }
157             else
158             {
159                 throw (new ErrnoException).useGlobalErrno("fcntl");
160             }
161         }
162 
163         // Clear the any previous contents of the file
164         this.enforcePosix!(ftruncate)(this.lock_fd, 0);
165 
166         char[512] buf;
167         auto pid_string = snformat(buf, "{}\n", getpid());
168 
169         this.writeNonInterrupted(this.lock_fd, pid_string);
170 
171         this.is_locked = true;
172         return true;
173     }
174 
175     /***************************************************************************
176 
177         Calls the function with the provided arguments and throws
178         new ErrnoException on failure (indicated by -1 return code)
179 
180         Params:
181             Func = function to call
182             args = arguments to call the function with
183 
184         Returns:
185             return value of Func(args) on success
186 
187         Throws:
188             new ErrnoException on failure.
189 
190     ***************************************************************************/
191 
192     static private int enforcePosix(alias Func, Args...)(Args args)
193     {
194         auto ret = Func(args);
195 
196         if (ret == -1)
197         {
198             throw (new ErrnoException).useGlobalErrno(identifier!(Func));
199         }
200 
201         return ret;
202     }
203 
204     /***************************************************************************
205 
206         Writes the content of buffer to fd, repeating if interrupted by
207         signal.
208 
209         Params:
210             fd = file descriptor to write to
211             buf = buffer contents to write
212 
213         Throws:
214             ErrnoException on failure.
215 
216     ***************************************************************************/
217 
218     static private void writeNonInterrupted(int fd, char[] buf)
219     {
220         int count;
221         while (count < buf.length)
222         {
223             auto written = write(fd, buf.ptr + count,
224                 buf.length - count);
225 
226             if (written < 0)
227             {
228                 if (errno == EINTR)
229                 {
230                     continue;
231                 }
232                 else
233                 {
234                     throw (new ErrnoException).useGlobalErrno("write");
235                 }
236             }
237 
238             count += written;
239         }
240     }
241 }