1 package org.simantics.district.maps.server;
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;
14 import java.util.Optional;
15 import java.util.concurrent.TimeUnit;
16 import java.util.concurrent.atomic.AtomicBoolean;
17 import java.util.stream.Stream;
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;
32 import com.fasterxml.jackson.core.JsonParseException;
33 import com.fasterxml.jackson.databind.JsonMappingException;
34 import com.fasterxml.jackson.databind.ObjectMapper;
36 public class TileserverMapnik {
38 private static final Logger LOGGER = LoggerFactory.getLogger(TileserverMapnik.class);
40 private SystemProcess process;
41 private Path serverRoot;
43 private AtomicBoolean running = new AtomicBoolean(false);
45 TileserverMapnik(Path serverRoot) {
46 this.serverRoot = serverRoot.normalize();
49 public boolean isRunning() throws IOException, InterruptedException {
50 return process == null ? false : process.isAlive() && isReady();
53 public boolean isReady() {
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));
63 pr.destroyForcefully();
64 } catch (InvalidExitValueException e) {
66 } catch (Exception e) {
67 LOGGER.error("Could not destroy process with pid {}", pid, e);
71 // check that npm dependencies are satisfied
72 // if (checkAndInstall(null, listener)) {
73 // checkAndInstall(ADDITIONAL_DEPENDENCIES[0], listener);
74 // checkAndInstall(ADDITIONAL_DEPENDENCIES[1], listener);
80 if (process != null && process.isAlive())
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) {
89 protected void processLine(String line) {
90 // Convert to UTF-8 string
91 String utf8Line = new String(line.getBytes(), StandardCharsets.UTF_8);
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);
102 listener.ifPresent(TileserverStartListener::started);
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());
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!");
122 Files.delete(getPid());
127 private Path getPid() {
128 return serverRoot.resolve("pid");
131 public void restart(Optional<TileserverStartListener> listener) throws Exception {
136 // private boolean checkIfInstalled(String module) throws Exception {
137 // String tileserverMapnik = tileserverMapnikRoot().toString();
139 // try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
140 // if (module == null) {
141 // retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list");
143 // retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list", module);
145 // String outputString = new String(output.toByteArray(), StandardCharsets.UTF_8);
147 // return retVal == 0;
150 // private int install(String module, Optional<TileserverStartListener> listener) throws Exception {
151 // String tileserverMapnik = tileserverMapnikRoot().toString();
153 // if (module == null) {
154 // listener.ifPresent(l -> l.installing("Installing tileserver-mapnik"));
155 // retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", "--save");
157 // retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", module, "--save");
160 // LOGGER.warn("Could not install module " + module == null ? "package.json" : module + "! " + retVal);
164 // private boolean checkAndInstall(String module, Optional<TileserverStartListener> listener) throws Exception {
165 // LOGGER.info("Installing module {}", String.valueOf(module));
166 // boolean installed = checkIfInstalled(module);
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));
174 // LOGGER.info("Module {} was already installed", String.valueOf(module));
176 // return !installed;
180 @SuppressWarnings("unused")
181 private Path tileserverMapnikRoot() {
182 return serverRoot.resolve("tileserver-mapnik").toAbsolutePath();
185 private Path getFonts() {
186 return serverRoot.resolve("fonts").toAbsolutePath();
189 private Path getICU() {
190 return serverRoot.resolve("dist/share/icu").toAbsolutePath();
193 @SuppressWarnings("unused")
194 private Path getTessera() {
195 return serverRoot.resolve("tileserver-mapnik/bin/tessera.js").toAbsolutePath();
198 private Path getConfigJson() {
199 return serverRoot.resolve("config.json").toAbsolutePath();
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)) {
207 if (!p.equals(data)) {
208 String tiles = p.getFileName().toString();
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)) {
226 Map<String, String> source = new HashMap<>();
228 Path tm2style = serverRoot.relativize(projectYaml.getParent());
230 String prefix = "tmstyle://../";
231 String tmStyle = prefix + tm2style.toString();
232 source.put("source", tmStyle);
233 config.put("/" + projectYaml.getParent().getFileName().toString(), source);
237 ObjectMapper mapper = new ObjectMapper();
238 mapper.writerWithDefaultPrettyPrinter().writeValue(Files.newOutputStream(configJson, StandardOpenOption.TRUNCATE_EXISTING), config);
241 public Path getStyleDirectory() {
242 return serverRoot.resolve("tm2");
245 public Path getDataDirectory() {
246 return serverRoot.resolve("data");
249 public Path getCurrentTiles() {
250 return getDataDirectory().resolve(MapsServerPreferences.currentMBTiles());
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);
264 Path tiles = serverRoot.relativize(getCurrentTiles());
266 String tmStyle = "mbtiles://../" + tiles.toString();
267 data.put("source", tmStyle);
269 } catch (IOException e) {
270 LOGGER.error("Could not read yaml", e);
273 Files.write(projectYaml, yaml.dump(data).getBytes(), StandardOpenOption.TRUNCATE_EXISTING);
274 } catch (IOException e) {
275 LOGGER.error("Could not write yaml", e);
278 } catch (IOException e) {
279 LOGGER.error("Could not browse tm2 styles", e);
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());
294 public static interface TileserverStartListener {
296 void installing(String module);
298 void installationFailed(String module, int returnValue);