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