001/*
002 * SonarQube, open source software quality management tool.
003 * Copyright (C) 2008-2014 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * SonarQube is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * SonarQube is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.process;
021
022import org.apache.commons.io.IOUtils;
023import org.slf4j.LoggerFactory;
024
025import java.io.File;
026import java.io.IOException;
027import java.io.RandomAccessFile;
028import java.nio.MappedByteBuffer;
029import java.nio.channels.FileChannel;
030
031/**
032 * Process inter-communication to :
033 * <ul>
034 *   <li>share status of child process</li>
035 *   <li>stop child process</li>
036 * </ul>
037 *
038 * <p/>
039 * It relies on files shared by both processes. Following alternatives were considered but not selected :
040 * <ul>
041 *   <li>JMX beans over RMI: network issues (mostly because of Java reverse-DNS) + requires to configure and open a new port</li>
042 *   <li>simple socket protocol: same drawbacks are RMI connection</li>
043 *   <li>java.lang.Process#destroy(): shutdown hooks are not executed on some OS (mostly MSWindows)</li>
044 *   <li>execute OS-specific commands (for instance kill on *nix): OS-specific, so hell to support. Moreover how to get identify a process ?</li>
045 * </ul>
046 */
047public class ProcessCommands {
048
049  /**
050   * The ByteBuffer will contains :
051   * <ul>
052   *   <li>First byte will contains 0x00 until stop command is issued = 0xFF</li>
053   *   <li>Then each 10 bytes will be reserved for each process</li>
054   * </ul>
055   *
056   * Description of ten bytes of each process :
057   * <ul>
058   *   <li>First byte will contains the state 0x00 until READY 0x01</li>
059   *   <li>The second byte will contains the request for stopping 0x00 or STOP (0xFF)</li>
060   *   <li>The next 8 bytes contains a long (System.currentTimeInMillis for ping)</li>
061   * </ul>
062   */
063  final MappedByteBuffer mappedByteBuffer;
064  private final RandomAccessFile sharedMemory;
065  private static final int MAX_PROCESSES = 50;
066  private static final int BYTE_LENGTH_FOR_ONE_PROCESS = 1 + 1 + 8;
067
068  // With this shared memory we can handle up to MAX_PROCESSES processes
069  private static final int MAX_SHARED_MEMORY = BYTE_LENGTH_FOR_ONE_PROCESS * MAX_PROCESSES;
070
071  public static final byte STOP = (byte) 0xFF;
072  public static final byte READY = (byte) 0x01;
073  public static final byte EMPTY = (byte) 0x00;
074
075  private int processNumber;
076
077  public ProcessCommands(File directory, int processNumber) {
078    // processNumber should not excess MAX_PROCESSES and must not be below -1
079    assert processNumber <= MAX_PROCESSES : "Incorrect process number";
080    assert processNumber >= -1 : "Incorrect process number";
081
082    this.processNumber = processNumber;
083    if (!directory.isDirectory() || !directory.exists()) {
084      throw new IllegalArgumentException("Not a valid directory: " + directory);
085    }
086
087    try {
088      sharedMemory = new RandomAccessFile(new File(directory, "sharedmemory"), "rw");
089      mappedByteBuffer = sharedMemory.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, MAX_SHARED_MEMORY);
090      cleanData();
091    } catch (IOException e) {
092      throw new IllegalArgumentException("Unable to create shared memory : ", e);
093    }
094  }
095
096  public boolean isReady() {
097    return canBeMonitored() && mappedByteBuffer.get(offset()) == READY;
098  }
099
100  /**
101   * To be executed by child process to declare that it's ready
102   */
103  public void setReady() {
104    if (canBeMonitored()) {
105      mappedByteBuffer.put(offset(), READY);
106    }
107  }
108
109  public void ping() {
110    if (canBeMonitored()) {
111      mappedByteBuffer.putLong(2 + offset(), System.currentTimeMillis());
112    }
113  }
114
115  public long getLastPing() {
116    if (canBeMonitored()) {
117      return mappedByteBuffer.getLong(2 + offset());
118    } else {
119      return -1;
120    }
121  }
122
123  /**
124   * To be executed by monitor process to ask for child process termination
125   */
126  public void askForStop() {
127    mappedByteBuffer.put(offset() + 1, STOP);
128  }
129
130  public boolean askedForStop() {
131    return mappedByteBuffer.get(offset() + 1) == STOP;
132  }
133
134  public void endWatch() {
135    IOUtils.closeQuietly(sharedMemory);
136  }
137
138  int offset() {
139    return BYTE_LENGTH_FOR_ONE_PROCESS * processNumber;
140  }
141
142  private boolean canBeMonitored() {
143    boolean result = processNumber >= 0 && processNumber < MAX_PROCESSES;
144    if (!result) {
145      LoggerFactory.getLogger(getClass()).info("This process cannot be monitored. Process Id : [{}]", processNumber);
146    }
147    return result;
148  }
149
150  private void cleanData() {
151    for (int i = 0; i < BYTE_LENGTH_FOR_ONE_PROCESS; i++) {
152      mappedByteBuffer.put(offset() + i, EMPTY);
153    }
154  }
155
156  public static int getMaxProcesses() {
157    return MAX_PROCESSES;
158  }
159}