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