001/*
002 * Syncany, www.syncany.org
003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation, either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018package org.syncany.util;
019
020import java.io.BufferedReader;
021import java.io.File;
022import java.io.FileReader;
023import java.io.FileWriter;
024import java.io.IOException;
025import java.io.InputStreamReader;
026import java.lang.management.ManagementFactory;
027import java.lang.management.RuntimeMXBean;
028import java.lang.reflect.Field;
029import java.lang.reflect.Method;
030import java.util.logging.Level;
031import java.util.logging.Logger;
032
033/**
034 * Utility class to manage a PID file in a platform-independent manner. The
035 * class only offers two few public methods to create a PID file for the current
036 * Java process, and to check whether the process indicated by a PID file is 
037 * still running.   
038 * 
039 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
040*/
041public class PidFileUtil {
042        private static final Logger logger = Logger.getLogger(PidFileUtil.class.getSimpleName());
043        
044        /**
045         * Determines the PID for the current Java process. 
046         * 
047         * <p>This is a non-trivial action, since Java does not offer an easy API. This method tries to
048         * determine the PID using two different methods {@link #getProcessPidImpl1()} and 
049         * {@link #getProcessPidImpl2()} (if the first one fails) and returns the PID it it succeeds.
050         * 
051         * @return The Java process PID, or -1 if the PID cannot be determined
052         */
053        public static int getProcessPid() {
054                try {
055                        return getProcessPidImpl1();
056                }
057                catch (Exception e) {
058                        logger.log(Level.WARNING, "Retrieving Java Process PID failed with first method, trying second ...");
059
060                        try {
061                                return getProcessPidImpl2();
062                        }
063                        catch (Exception e1) {
064                                return -1;
065                        } 
066                }
067        }
068
069        /**
070         * Determines the process identifier (PID) for the currently active Java process and writes this
071         * PID to the given file.
072         * 
073         * @see #getProcessPid()
074         */
075        public static void createPidFile(File pidFile) throws IOException {
076                pidFile.delete();
077                
078                try (FileWriter pidFileWriter = new FileWriter(pidFile)) {
079                        String pidStr = "" + getProcessPid();
080                        
081                        logger.log(Level.INFO, "Writing PID file (for PID " + pidStr + ") to " + pidFile + " ...");
082                        
083                        pidFileWriter.write(pidStr);
084                        pidFileWriter.close();
085                }
086                
087                pidFile.deleteOnExit();         
088        }
089        
090        /**
091         * Determines whether a process is running, based on the given PID file. The method
092         * reads the PID file and then calls {@link #isProcessRunning(int)}. If the PID file
093         * does not exist, it returns <code>false</code>.
094         */
095        public static boolean isProcessRunning(File pidFile) {
096                if (pidFile.exists()) {
097                        try (BufferedReader pidFileReader = new BufferedReader(new FileReader(pidFile))) {
098                                int pid = Integer.parseInt(pidFileReader.readLine());
099                                return isProcessRunning(pid);
100                        }
101                        catch (Exception e) {
102                                logger.log(Level.WARNING, "Cannot read pidfile from " + pidFile + ". Assuming process not running.");
103                                return false;
104                        }
105                }
106                else {
107                        return false;
108                }
109        }
110        
111        /**
112         * Determines whether a process with the given PID is running. Depending on the
113         * underlying OS, this method either calls {@link #isProcessRunningUnixLike(int)} 
114         * or {@link #isProcessRunningWindows(int)}. 
115         */
116        private static boolean isProcessRunning(int pid) {
117                if (EnvironmentUtil.isUnixLikeOperatingSystem()) {
118                        return isProcessRunningUnixLike(pid);
119                }
120                else if (EnvironmentUtil.isWindows()) {
121                        return isProcessRunningWindows(pid);
122                }
123                
124                return false;
125        }       
126        
127        /**
128         * Uses the {@link RuntimeMXBean}'s name to determine the PID. On Linux, this name 
129         * typically has a value like <code>12345@localhost</code> where 12345 is the PID.
130         * However, this is not guaranteed for every VM, so this is only one of two implementations.
131         *  
132         * @see http://stackoverflow.com/a/35885/1440785
133         */
134        private static int getProcessPidImpl1() throws Exception {
135                String pidStr = ManagementFactory.getRuntimeMXBean().getName();
136
137                if (pidStr.contains("@")) {
138                        int processPid = Integer.parseInt(pidStr.split("@")[0]);
139                        
140                        logger.log(Level.INFO, "Java Process PID is " + processPid);
141                        return processPid;
142                }
143                else {
144                        throw new Exception("Cannot find pid from string: " + pidStr);
145                }
146        }
147
148        /**
149         * Uses the private method <code>VMManagement.getProcessId()</code> of Sun's <code>sun.management.VMManagement</code>
150         * class to determine the PID (using reflection to make the relevant fields visible).
151         * 
152         * @see http://stackoverflow.com/a/12066696/1440785
153         */
154        private static int getProcessPidImpl2() throws Exception {
155                RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
156                Field jvmField = runtimeMXBean.getClass().getDeclaredField("jvm");
157                jvmField.setAccessible(true);
158                
159                // The returned object is of the type 'sun.management.VMManagement', but since we
160                // don't need the exact type here and we don't want to reference it in the
161                // imports, we'll just hope it's there.
162                
163                Object vmManagement = jvmField.get(runtimeMXBean); 
164                Method getProcessIdMethod = vmManagement.getClass().getDeclaredMethod("getProcessId");
165                
166                getProcessIdMethod.setAccessible(true);
167                int processPid = (Integer) getProcessIdMethod.invoke(vmManagement);
168                
169                logger.log(Level.INFO, "Java Process PID is " + processPid);
170                return processPid;
171        }
172        
173        /**
174         * Determines whether a process with the given PID is running using the POSIX 
175         * <code>ps -p $pid</code> command.
176         * 
177         * @param pid Process ID (PID) to check
178         * @return True if process is running, false otherwise
179         */
180        private static boolean isProcessRunningUnixLike(int pid) {
181                try {
182                        Runtime runtime = Runtime.getRuntime();
183                        Process process = runtime.exec(new String[] { "/bin/ps", "-p", ""+pid });
184                        
185                        int killProcessExitCode = process.waitFor();
186                        boolean processRunning = killProcessExitCode == 0;
187
188                        logger.log(Level.INFO, "isProcessRunningUnixLike(" + pid + ") returned " + killProcessExitCode + ", process running = " + processRunning);
189                        return processRunning;
190                }
191                catch (Exception e) {
192                        logger.log(Level.SEVERE, "Cannot retrieve status of PID " + pid + "; assuming process not running.");
193                        return false;
194                }
195        }
196
197        /**
198         * Determines whether a process with the given PID is running using the Windows 
199         * <code>tasklist</code> command.
200         * 
201         * @see http://stackoverflow.com/questions/2533984/java-checking-if-any-process-id-is-currently-running-on-windows
202         */
203        private static boolean isProcessRunningWindows(int pid) {
204                try {
205                        Process tasklistProcess = Runtime.getRuntime().exec(new String[] { "cmd", "/c", "tasklist /FI \"PID eq " + pid + "\"" });
206                        BufferedReader tasklistOutputReader = new BufferedReader(new InputStreamReader(tasklistProcess.getInputStream()));
207                        
208                        String line = null;
209                        boolean processRunning = false;
210                        
211                        while ((line = tasklistOutputReader.readLine()) != null) {
212                                if (line.contains(" " + pid + " ")) {
213                                        processRunning = true;
214                                        break;
215                                }
216                        }
217
218                        logger.log(Level.INFO, "isProcessRunningWindows(" + pid + ") returned " + line + ", process running = " + processRunning);
219                        return processRunning;
220                }
221                catch (Exception ex) {
222                        logger.log(Level.SEVERE, "Cannot retrieve status of PID " + pid + "; assuming process not running.");
223                        return false;
224                }
225        }
226}