]> gerrit.simantics Code Review - simantics/district.git/blob - org.simantics.maps.server/src/org/simantics/district/maps/server/TileserverMapnik.java
Do not fail mapserver startup if destroying existing process fails
[simantics/district.git] / org.simantics.maps.server / src / org / simantics / district / maps / server / TileserverMapnik.java
1 package org.simantics.district.maps.server;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.nio.charset.StandardCharsets;
6 import java.nio.file.DirectoryStream;
7 import java.nio.file.Files;
8 import java.nio.file.Path;
9 import java.nio.file.StandardOpenOption;
10 import java.util.ArrayList;
11 import java.util.HashMap;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Optional;
15 import java.util.concurrent.TimeUnit;
16 import java.util.concurrent.atomic.AtomicBoolean;
17 import java.util.stream.Stream;
18
19 import org.simantics.district.maps.server.prefs.MapsServerPreferences;
20 import org.slf4j.Logger;
21 import org.slf4j.LoggerFactory;
22 import org.yaml.snakeyaml.Yaml;
23 import org.zeroturnaround.exec.InvalidExitValueException;
24 import org.zeroturnaround.exec.ProcessExecutor;
25 import org.zeroturnaround.exec.StartedProcess;
26 import org.zeroturnaround.exec.stream.slf4j.Slf4jDebugOutputStream;
27 import org.zeroturnaround.process.PidProcess;
28 import org.zeroturnaround.process.PidUtil;
29 import org.zeroturnaround.process.Processes;
30 import org.zeroturnaround.process.SystemProcess;
31
32 import com.fasterxml.jackson.core.JsonParseException;
33 import com.fasterxml.jackson.databind.JsonMappingException;
34 import com.fasterxml.jackson.databind.ObjectMapper;
35
36 public class TileserverMapnik {
37
38     private static final Logger LOGGER = LoggerFactory.getLogger(TileserverMapnik.class);
39     
40     private SystemProcess process;
41     private Path serverRoot;
42     
43     private AtomicBoolean running = new AtomicBoolean(false);
44     
45     TileserverMapnik(Path serverRoot) {
46         this.serverRoot = serverRoot.normalize();
47     }
48
49     public boolean isRunning() throws IOException, InterruptedException {
50         return process == null ? false : process.isAlive() && isReady();
51     }
52     
53     public boolean isReady() {
54         return running.get();
55     }
56     
57     public void start(Optional<TileserverStartListener> listener) throws Exception {
58         // check if existing server is left hanging
59         if (Files.exists(getPid())) {
60             String pid = new String(Files.readAllBytes(getPid()));
61             PidProcess pr = Processes.newPidProcess(Integer.parseInt(pid));
62             try {
63                 pr.destroyForcefully();
64             } catch (InvalidExitValueException e) {
65                 // ignore, 
66             } catch (Exception e) {
67                 LOGGER.error("Could not destroy process with pid {}", pid, e);
68             }
69         }
70         
71         // check that npm dependencies are satisfied
72 //        if (checkAndInstall(null, listener)) {
73 //            checkAndInstall(ADDITIONAL_DEPENDENCIES[0], listener);
74 //            checkAndInstall(ADDITIONAL_DEPENDENCIES[1], listener);
75 //        }
76         
77         checkConfigJson();
78         checkTm2Styles();
79         
80         if (process != null && process.isAlive())
81             return;
82         
83         Path tileliveTesseraWin = serverRoot.resolve("dist").resolve("node.exe").normalize().toAbsolutePath();
84         StartedProcess startedProcess = new ProcessExecutor().directory(serverRoot.resolve("dist").toFile()).destroyOnExit().environment(getEnv())
85                 .command(tileliveTesseraWin.toString(), "-c", getConfigJson().toString(), "-p", Integer.toString(MapsServerPreferences.defaultPort()))
86                 .redirectOutput(new Slf4jDebugOutputStream(LOGGER) {
87                     
88                     @Override
89                     protected void processLine(String line) {
90                         // Convert to UTF-8 string
91                         String utf8Line = new String(line.getBytes(), StandardCharsets.UTF_8);
92                         log.debug(utf8Line);
93                     }
94                 }).start();
95         
96         Process nativeProcess = startedProcess.getProcess();
97         process = Processes.newStandardProcess(nativeProcess);
98         int pid = PidUtil.getPid(nativeProcess);
99         LOGGER.info("Writing pid-file to {} with pid={}", getPid(), pid);
100         Files.write(getPid(), (pid + "").getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
101         running.set(true);
102         listener.ifPresent(TileserverStartListener::started);
103     }
104
105     private Map<String, String> getEnv() {
106         Map<String, String> env = new HashMap<>();
107         env.put("MAPNIK_FONT_PATH", getFonts().toString());
108         env.put("ICU_DATA", getICU().toString());
109         return env;
110     }
111
112     public void stop() throws IOException, InterruptedException {
113         if (process != null && process.isAlive()) {
114             // gracefully not supported on windows
115             process.destroyForcefully();
116             if (!process.waitFor(2000, TimeUnit.MILLISECONDS)) {
117                 process.destroyForcefully();
118                 if (process.isAlive())
119                     LOGGER.error("Could not shutdown TileserverMapnik!");
120             }
121             process = null;
122             Files.delete(getPid());
123             running.set(false);
124         }
125     }
126
127     private Path getPid() {
128         return serverRoot.resolve("pid");
129     }
130
131     public void restart(Optional<TileserverStartListener> listener) throws Exception {
132         stop();
133         start(listener);
134     }
135     
136 //    private boolean checkIfInstalled(String module) throws Exception {
137 //        String tileserverMapnik = tileserverMapnikRoot().toString();
138 //        int retVal;
139 //        try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
140 //            if (module == null) {
141 //                retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list");
142 //            } else {
143 //                retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list", module);
144 //            }
145 //            String outputString = new String(output.toByteArray(), StandardCharsets.UTF_8);
146 //        }
147 //        return retVal == 0;
148 //    }
149 //    
150 //    private int install(String module, Optional<TileserverStartListener> listener) throws Exception {
151 //        String tileserverMapnik = tileserverMapnikRoot().toString();
152 //        int retVal;
153 //        if (module == null) {
154 //            listener.ifPresent(l -> l.installing("Installing tileserver-mapnik"));
155 //            retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", "--save");
156 //        } else { 
157 //            retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", module, "--save");
158 //        }
159 //        if (retVal != 0)
160 //            LOGGER.warn("Could not install module " + module == null ? "package.json" : module + "! " + retVal);
161 //        return retVal;
162 //    }
163 //    
164 //    private boolean checkAndInstall(String module, Optional<TileserverStartListener> listener) throws Exception {
165 //        LOGGER.info("Installing module {}", String.valueOf(module));
166 //        boolean installed = checkIfInstalled(module); 
167 //        if (!installed) {
168 //            int installSuccessfull = install(module, listener);
169 //            if (installSuccessfull != 0) {
170 //                LOGGER.warn("Installation of module {} failed!", String.valueOf(module));
171 //                listener.ifPresent(l -> l.installationFailed(module, installSuccessfull));
172 //            }
173 //        } else {
174 //            LOGGER.info("Module {} was already installed", String.valueOf(module));
175 //        }
176 //        return !installed;
177 //    }
178
179     
180     @SuppressWarnings("unused")
181         private Path tileserverMapnikRoot() {
182         return serverRoot.resolve("tileserver-mapnik").toAbsolutePath();
183     }
184     
185     private Path getFonts() {
186         return serverRoot.resolve("fonts").toAbsolutePath();
187     }
188     
189     private Path getICU() {
190         return serverRoot.resolve("dist/share/icu").toAbsolutePath();
191     }
192     
193     @SuppressWarnings("unused")
194         private Path getTessera() {
195         return serverRoot.resolve("tileserver-mapnik/bin/tessera.js").toAbsolutePath();
196     }
197     
198     private Path getConfigJson() {
199         return serverRoot.resolve("config.json").toAbsolutePath();
200     }
201     
202     public List<String> availableMBTiles() throws IOException {
203         Path data = getDataDirectory();
204         List<String> result = new ArrayList<>();
205         try (Stream<Path> paths = Files.walk(data)) {
206             paths.forEach(p -> {
207                 if (!p.equals(data)) {
208                     String tiles = p.getFileName().toString();
209                     result.add(tiles);
210                 }
211             });
212         }
213         return result;
214     }
215     
216     private void checkConfigJson() throws JsonParseException, JsonMappingException, IOException {
217         Path configJson = getConfigJson();
218         Map<String, Object> config = new HashMap<>();
219         Path tm2 = getStyleDirectory();
220         try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
221             stream.forEach(p -> {
222                 Path projectYaml = p.resolve("project.yml");
223                 if (Files.exists(projectYaml)) {
224                     // Good
225                     
226                     Map<String, String> source = new HashMap<>();
227                     
228                     Path tm2style = serverRoot.relativize(projectYaml.getParent());
229                     
230                     String prefix = "tmstyle://../";
231                     String tmStyle = prefix + tm2style.toString(); 
232                     source.put("source", tmStyle);
233                     config.put("/" + projectYaml.getParent().getFileName().toString(), source);
234                 }
235             });
236         }
237         ObjectMapper mapper = new ObjectMapper();
238         mapper.writerWithDefaultPrettyPrinter().writeValue(Files.newOutputStream(configJson, StandardOpenOption.TRUNCATE_EXISTING), config);
239     }
240     
241     public Path getStyleDirectory() {
242         return serverRoot.resolve("tm2");
243     }
244     
245     public Path getDataDirectory() {
246         return serverRoot.resolve("data");
247     }
248     
249     public Path getCurrentTiles() {
250         return getDataDirectory().resolve(MapsServerPreferences.currentMBTiles());
251     }
252     
253     @SuppressWarnings("unchecked")
254     public void checkTm2Styles() {
255         Path tm2 = getStyleDirectory();
256         try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
257             stream.forEach(p -> {
258                 Path projectYaml = p.resolve("project.yml");
259                 Yaml yaml = new Yaml();
260                 Map<String, String> data = null;
261                 try (InputStream input = Files.newInputStream(projectYaml, StandardOpenOption.READ)) {
262                     data = yaml.loadAs(input, Map.class);
263                     
264                     Path tiles = serverRoot.relativize(getCurrentTiles());
265                     
266                     String tmStyle = "mbtiles://../" + tiles.toString();
267                     data.put("source", tmStyle);
268                     
269                 } catch (IOException e) {
270                     LOGGER.error("Could not read yaml", e);
271                 }
272                 try {
273                     Files.write(projectYaml, yaml.dump(data).getBytes(), StandardOpenOption.TRUNCATE_EXISTING);
274                 } catch (IOException e) {
275                     LOGGER.error("Could not write yaml", e);
276                 }
277             });
278         } catch (IOException e) {
279             LOGGER.error("Could not browse tm2 styles", e);
280         }
281     }
282
283     public List<String> listStyles() throws IOException {
284         List<String> results = new ArrayList<>();
285         Path tm2 = getStyleDirectory();
286         try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
287             stream.forEach(p -> {
288                 results.add(p.getFileName().toString());
289             });
290         }
291         return results;
292     }
293
294     public static interface TileserverStartListener {
295         
296         void installing(String module);
297         
298         void installationFailed(String module, int returnValue);
299
300         void started();
301     }
302 }