1   /*
2    * Copyright 2017 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package com.linecorp.centraldogma.server;
17  
18  import static com.google.common.base.MoreObjects.firstNonNull;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.nio.file.AtomicMoveNotSupportedException;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.nio.file.StandardCopyOption;
26  
27  import javax.annotation.Nullable;
28  
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  import com.beust.jcommander.JCommander;
33  import com.beust.jcommander.Parameter;
34  import com.beust.jcommander.converters.FileConverter;
35  
36  import com.linecorp.armeria.common.util.SystemInfo;
37  
38  /**
39   * Entry point of a standalone server. Use {@link CentralDogmaBuilder} to embed a server.
40   */
41  public final class Main {
42  
43      private static final Logger logger = LoggerFactory.getLogger(Main.class);
44  
45      static {
46          try {
47              // Install the java.util.logging to SLF4J bridge.
48              final Class<?> bridgeHandler =
49                      Class.forName("org.slf4j.bridge.SLF4JBridgeHandler", true, Main.class.getClassLoader());
50              bridgeHandler.getMethod("removeHandlersForRootLogger").invoke(null);
51              bridgeHandler.getMethod("install").invoke(null);
52              logger.debug("Installed the java.util.logging-to-SLF4J bridge.");
53          } catch (Throwable cause) {
54              logger.debug("Failed to install the java.util.logging-to-SLF4J bridge:", cause);
55          }
56      }
57  
58      enum State {
59          NONE,
60          INITIALIZED,
61          STARTED,
62          STOPPED,
63          DESTROYED
64      }
65  
66      private static final File DEFAULT_DATA_DIR =
67              new File(System.getProperty("user.dir", ".") + File.separatorChar + "data");
68  
69      private static final File DEFAULT_CONFIG_FILE =
70              new File(System.getProperty("user.dir", ".") +
71                       File.separatorChar + "conf" +
72                       File.separatorChar + "dogma.json");
73  
74      private static final File DEFAULT_PID_FILE =
75              new File(System.getProperty("user.dir", ".") +
76                       File.separatorChar + "dogma.pid");
77  
78      @Nullable
79      @Parameter(names = "-config", description = "The path to the config file", converter = FileConverter.class)
80      private File configFile;
81  
82      @Nullable
83      @Parameter(names = "-pidfile",
84              description = "The path to the file containing the pid of the server" +
85                            " (defaults to ./dogma.pid)",
86              converter = FileConverter.class)
87      private File pidFile;
88  
89      /**
90       * Note that {@link Boolean} was used in lieu of {@code boolean} so that JCommander does not print the
91       * default value of this option.
92       */
93      @Nullable
94      @Parameter(names = { "-help", "-h" }, description = "Prints the usage", help = true)
95      private Boolean help;
96  
97      private State state = State.NONE;
98      @Nullable
99      private CentralDogma dogma;
100     @Nullable
101     private PidFile procIdFile;
102     private boolean procIdFileCreated;
103 
104     private Main(String[] args) {
105         final JCommander commander = new JCommander(this);
106         commander.setProgramName(getClass().getName());
107         commander.parse(args);
108 
109         if (help != null && help) {
110             commander.usage();
111         } else {
112             procIdFile = new PidFile(firstNonNull(pidFile, DEFAULT_PID_FILE));
113             state = State.INITIALIZED;
114         }
115     }
116 
117     synchronized void start() throws Exception {
118         switch (state) {
119             case NONE:
120                 throw new IllegalStateException("not initialized");
121             case STARTED:
122                 throw new IllegalStateException("started already");
123             case DESTROYED:
124                 throw new IllegalStateException("can't start after destruction");
125             default:
126                 break;
127         }
128 
129         final File configFile = findConfigFile(this.configFile, DEFAULT_CONFIG_FILE);
130 
131         final CentralDogma dogma;
132         if (configFile == null) {
133             dogma = new CentralDogmaBuilder(DEFAULT_DATA_DIR).build();
134         } else {
135             dogma = CentralDogma.forConfig(configFile);
136         }
137 
138         dogma.start().get();
139 
140         this.dogma = dogma;
141         state = State.STARTED;
142 
143         // The server would be stopped even if we fail to create the PID file from here,
144         // because the state has been updated.
145         assert procIdFile != null;
146         procIdFile.create();
147         procIdFileCreated = true;
148     }
149 
150     @Nullable
151     private static File findConfigFile(@Nullable File file, File defaultFile) {
152         if (file != null) {
153             if (file.isFile() && file.canRead()) {
154                 return file;
155             } else {
156                 throw new IllegalStateException("cannot access the specified config file: " + file);
157             }
158         }
159 
160         // Try to use the default config file if not specified.
161         if (defaultFile.isFile() && defaultFile.canRead()) {
162             return defaultFile;
163         }
164         return null;
165     }
166 
167     synchronized void stop() throws Exception {
168         switch (state) {
169             case NONE:
170             case INITIALIZED:
171             case STOPPED:
172                 return;
173             case DESTROYED:
174                 throw new IllegalStateException("can't stop after destruction");
175         }
176 
177         final CentralDogma dogma = this.dogma;
178         assert dogma != null;
179         this.dogma = null;
180         dogma.stop().get();
181 
182         state = State.STOPPED;
183     }
184 
185     void destroy() {
186         switch (state) {
187             case NONE:
188                 return;
189             case STARTED:
190                 throw new IllegalStateException("can't destroy while running");
191             case DESTROYED:
192                 return;
193         }
194 
195         assert procIdFile != null;
196         if (procIdFileCreated) {
197             try {
198                 procIdFile.destroy();
199             } catch (IOException e) {
200                 logger.warn("Failed to destroy the PID file:", e);
201             }
202         }
203 
204         state = State.DESTROYED;
205     }
206 
207     /**
208      * Starts a new Central Dogma server.
209      */
210     public static void main(String[] args) throws Exception {
211         final Main main = new Main(args);
212 
213         // Register the shutdown hook.
214         Runtime.getRuntime().addShutdownHook(new Thread("Central Dogma shutdown hook") {
215             @Override
216             public void run() {
217                 try {
218                     main.stop();
219                 } catch (Exception e) {
220                     logger.warn("Failed to stop the Central Dogma:", e);
221                 }
222 
223                 try {
224                     main.destroy();
225                 } catch (Exception e) {
226                     logger.warn("Failed to destroy the Central Dogma:", e);
227                 }
228             }
229         });
230 
231         // Exit if initialization failed.
232         if (main.state != State.INITIALIZED) {
233             System.exit(1);
234             return;
235         }
236 
237         try {
238             main.start();
239         } catch (Throwable cause) {
240             logger.error("Failed to start the Central Dogma:", cause);
241             // Trigger the shutdown hook.
242             System.exit(1);
243         }
244     }
245 
246     /**
247      * Manages a process ID file for the Central Dogma server.
248      */
249     static final class PidFile {
250 
251         private final File file;
252 
253         private PidFile(File file) {
254             this.file = file;
255         }
256 
257         void create() throws IOException {
258             if (file.exists()) {
259                 throw new IllegalStateException("Failed to create a PID file. A file already exists: " +
260                                                 file.getPath());
261             }
262 
263             final int pid = SystemInfo.pid();
264             final Path temp = Files.createTempFile("central-dogma", ".tmp");
265             Files.write(temp, Integer.toString(pid).getBytes());
266             try {
267                 Files.move(temp, file.toPath(), StandardCopyOption.ATOMIC_MOVE);
268             } catch (AtomicMoveNotSupportedException e) {
269                 Files.move(temp, file.toPath());
270             }
271 
272             logger.debug("A PID file has been created: {}", file);
273         }
274 
275         void destroy() throws IOException {
276             if (Files.deleteIfExists(file.toPath())) {
277                 logger.debug("Successfully deleted the PID file: {}", file);
278             }
279         }
280     }
281 }