package org.simantics.district.maps.server; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.Yaml; import org.zeroturnaround.exec.ProcessExecutor; import org.zeroturnaround.exec.StartedProcess; import org.zeroturnaround.exec.stream.slf4j.Slf4jStream; import org.zeroturnaround.process.PidProcess; import org.zeroturnaround.process.PidUtil; import org.zeroturnaround.process.Processes; import org.zeroturnaround.process.SystemProcess; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; public class TileserverMapnik { private static final Logger LOGGER = LoggerFactory.getLogger(TileserverMapnik.class); private static final String[] ADDITIONAL_DEPENDENCIES = new String[] { "tilelive-vector@3.9.4", "tilelive-tmstyle@0.6.0" }; private SystemProcess process; private Path serverRoot; private int port; private AtomicBoolean running = new AtomicBoolean(false); TileserverMapnik(Path serverRoot, int port) { this.serverRoot = serverRoot.normalize(); this.port = port; } public void setPort(int port) { this.port = port; } public boolean isRunning() throws IOException, InterruptedException { return process == null ? false : process.isAlive() && isReady(); } public boolean isReady() { return running.get(); } public void start() throws Exception { // check if existing server is left hanging if (Files.exists(getPid())) { String pid = new String(Files.readAllBytes(getPid())); PidProcess pr = Processes.newPidProcess(Integer.parseInt(pid)); pr.destroyForcefully(); } // check that npm dependencies are satisfied if (checkAndInstall(null)) { checkAndInstall(ADDITIONAL_DEPENDENCIES[0]); checkAndInstall(ADDITIONAL_DEPENDENCIES[1]); } checkConfigJson(); checkTm2Styles(); if (process != null && process.isAlive()) return; StartedProcess startedProcess = new ProcessExecutor().directory(serverRoot.resolve("tileserver-mapnik").toFile()).destroyOnExit().environment(getEnv()) .command(NodeJS.executable().toString(), getTessera().toString(), "-c", getConfigJson().toString()) .redirectOutput(Slf4jStream.ofCaller().asDebug()).start(); Process nativeProcess = startedProcess.getProcess(); process = Processes.newStandardProcess(nativeProcess); int pid = PidUtil.getPid(nativeProcess); Files.write(getPid(), (pid + "").getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); running.set(true); } private Map getEnv() { Map env = new HashMap<>(); env.put("MAPNIK_FONT_PATH", getFonts().toString()); env.put("ICU_DATA", getICU().toString()); return env; } public void stop() throws IOException, InterruptedException { if (process != null && process.isAlive()) { // gracefully not supported on windows process.destroyForcefully(); if (!process.waitFor(2000, TimeUnit.MILLISECONDS)) { process.destroyForcefully(); if (process.isAlive()) LOGGER.error("Could not shutdown TileserverMapnik!"); } process = null; Files.delete(getPid()); running.set(false); } } private Path getPid() { return serverRoot.resolve("pid"); } public void restart() throws Exception { stop(); start(); } private boolean checkIfInstalled(String module) throws Exception { String tileserverMapnik = tileserverMapnikRoot().toString(); int retVal; try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { if (module == null) { retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list"); } else { retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list", module); } String outputString = new String(output.toByteArray(), StandardCharsets.UTF_8); } return retVal == 0; } private boolean install(String module) throws Exception { String tileserverMapnik = tileserverMapnikRoot().toString(); int retVal; if (module == null) retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", "--save"); else retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", module, "--save"); if (retVal != 0) LOGGER.warn("Could not install module " + module == null ? "package.json" : module + "! " + retVal); return retVal == 0; } private boolean checkAndInstall(String module) throws Exception { boolean installed = checkIfInstalled(module); if (!installed) install(module); return !installed; } private Path tileserverMapnikRoot() { return serverRoot.resolve("tileserver-mapnik").toAbsolutePath(); } private Path getFonts() { return serverRoot.resolve("fonts").toAbsolutePath(); } private Path getICU() { return serverRoot.resolve("tileserver-mapnik/node_modules/tilelive-vector/node_modules/mapnik/lib/binding/node-v46-win32-x64/share/icu").toAbsolutePath(); } private Path getTessera() { return serverRoot.resolve("tileserver-mapnik/bin/tessera.js").toAbsolutePath(); } private Path getConfigJson() { return serverRoot.resolve("config.json").toAbsolutePath(); } public List availableMBTiles() throws IOException { Path data = serverRoot.resolve("data").toAbsolutePath(); List result = new ArrayList<>(); try (Stream paths = Files.walk(data)) { paths.forEach(p -> { if (!p.equals(data)) { String tiles = p.getFileName().toString(); result.add(tiles); } }); } return result; } private void checkConfigJson() throws JsonParseException, JsonMappingException, IOException { Path configJson = getConfigJson(); Map config = new HashMap<>(); Path tm2 = serverRoot.resolve("tm2").toAbsolutePath(); try (DirectoryStream stream = Files.newDirectoryStream(tm2)) { stream.forEach(p -> { Path projectYaml = p.resolve("project.yml"); if (Files.exists(projectYaml)) { // Good Map source = new HashMap<>(); Path tm2style = serverRoot.relativize(projectYaml.getParent()); String prefix = "tmstyle://../"; String tmStyle = prefix + tm2style.toString(); source.put("source", tmStyle); config.put("/" + projectYaml.getParent().getFileName().toString(), source); } }); } ObjectMapper mapper = new ObjectMapper(); mapper.writerWithDefaultPrettyPrinter().writeValue(Files.newOutputStream(configJson, StandardOpenOption.TRUNCATE_EXISTING), config); } public void checkTm2Styles() { Path tm2 = serverRoot.resolve("tm2").toAbsolutePath(); try (DirectoryStream stream = Files.newDirectoryStream(tm2)) { stream.forEach(p -> { Path projectYaml = p.resolve("project.yml"); Yaml yaml = new Yaml(); Map data = null; try (InputStream input = Files.newInputStream(projectYaml, StandardOpenOption.READ)) { data = yaml.loadAs(input, Map.class); Path tiles = serverRoot.relativize(serverRoot.resolve("data").resolve("helsinki_finland.mbtiles"));//.toAbsolutePath().toString().replace("\\", "/"); String tmStyle = "mbtiles://../" + tiles.toString(); data.put("source", tmStyle); } catch (IOException e) { LOGGER.error("Could not read yaml", e); } try { Files.write(projectYaml, yaml.dump(data).getBytes(), StandardOpenOption.TRUNCATE_EXISTING); } catch (IOException e) { LOGGER.error("Could not write yaml", e); } }); } catch (IOException e) { LOGGER.error("Could not browse tm2 styles", e); } } public List listStyles() throws IOException { List results = new ArrayList<>(); Path tm2 = serverRoot.resolve("tm2").toAbsolutePath(); try (DirectoryStream stream = Files.newDirectoryStream(tm2)) { stream.forEach(p -> { results.add(p.getFileName().toString()); }); } return results; } }