package org.simantics.district.maps.server; 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.Optional; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; import org.simantics.district.maps.server.prefs.MapsServerPreferences; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.Yaml; import org.zeroturnaround.exec.InvalidExitValueException; import org.zeroturnaround.exec.ProcessExecutor; import org.zeroturnaround.exec.StartedProcess; import org.zeroturnaround.exec.stream.slf4j.Slf4jDebugOutputStream; 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 SystemProcess process; private Path serverRoot; private AtomicBoolean running = new AtomicBoolean(false); TileserverMapnik(Path serverRoot) { this.serverRoot = serverRoot.normalize(); } public boolean isRunning() throws IOException, InterruptedException { return process == null ? false : process.isAlive() && isReady(); } public boolean isReady() { return running.get(); } public void start(Optional listener) 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)); try { pr.destroyForcefully(); } catch (InvalidExitValueException e) { // ignore, } catch (Exception e) { LOGGER.error("Could not destroy process with pid {}", pid, e); } } // check that npm dependencies are satisfied // if (checkAndInstall(null, listener)) { // checkAndInstall(ADDITIONAL_DEPENDENCIES[0], listener); // checkAndInstall(ADDITIONAL_DEPENDENCIES[1], listener); // } checkConfigJson(); checkTm2Styles(); if (process != null && process.isAlive()) return; Path tileliveTesseraWin = serverRoot.resolve("dist").resolve("node.exe").normalize().toAbsolutePath(); StartedProcess startedProcess = new ProcessExecutor().directory(serverRoot.resolve("dist").toFile()).destroyOnExit().environment(getEnv()) .command(tileliveTesseraWin.toString(), "-c", getConfigJson().toString(), "-p", Integer.toString(MapsServerPreferences.defaultPort())) .redirectOutput(new Slf4jDebugOutputStream(LOGGER) { @Override protected void processLine(String line) { // Convert to UTF-8 string String utf8Line = new String(line.getBytes(), StandardCharsets.UTF_8); log.debug(utf8Line); } }).start(); Process nativeProcess = startedProcess.getProcess(); process = Processes.newStandardProcess(nativeProcess); int pid = PidUtil.getPid(nativeProcess); LOGGER.info("Writing pid-file to {} with pid={}", getPid(), pid); Files.write(getPid(), (pid + "").getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); running.set(true); listener.ifPresent(TileserverStartListener::started); } 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(Optional listener) throws Exception { stop(); start(listener); } // 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 int install(String module, Optional listener) throws Exception { // String tileserverMapnik = tileserverMapnikRoot().toString(); // int retVal; // if (module == null) { // listener.ifPresent(l -> l.installing("Installing tileserver-mapnik")); // 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; // } // // private boolean checkAndInstall(String module, Optional listener) throws Exception { // LOGGER.info("Installing module {}", String.valueOf(module)); // boolean installed = checkIfInstalled(module); // if (!installed) { // int installSuccessfull = install(module, listener); // if (installSuccessfull != 0) { // LOGGER.warn("Installation of module {} failed!", String.valueOf(module)); // listener.ifPresent(l -> l.installationFailed(module, installSuccessfull)); // } // } else { // LOGGER.info("Module {} was already installed", String.valueOf(module)); // } // return !installed; // } @SuppressWarnings("unused") private Path tileserverMapnikRoot() { return serverRoot.resolve("tileserver-mapnik").toAbsolutePath(); } private Path getFonts() { return serverRoot.resolve("fonts").toAbsolutePath(); } private Path getICU() { return serverRoot.resolve("dist/share/icu").toAbsolutePath(); } @SuppressWarnings("unused") 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 = getDataDirectory(); 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 = getStyleDirectory(); 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 Path getStyleDirectory() { return serverRoot.resolve("tm2"); } public Path getDataDirectory() { return serverRoot.resolve("data"); } public Path getCurrentTiles() { return getDataDirectory().resolve(MapsServerPreferences.currentMBTiles()); } @SuppressWarnings("unchecked") public void checkTm2Styles() { Path tm2 = getStyleDirectory(); 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(getCurrentTiles()); 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 = getStyleDirectory(); try (DirectoryStream stream = Files.newDirectoryStream(tm2)) { stream.forEach(p -> { results.add(p.getFileName().toString()); }); } return results; } public static interface TileserverStartListener { void installing(String module); void installationFailed(String module, int returnValue); void started(); } }