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.ProcessExecutor;
24 import org.zeroturnaround.exec.StartedProcess;
25 import org.zeroturnaround.exec.stream.slf4j.Slf4jDebugOutputStream;
26 import org.zeroturnaround.process.PidProcess;
27 import org.zeroturnaround.process.PidUtil;
28 import org.zeroturnaround.process.Processes;
29 import org.zeroturnaround.process.SystemProcess;
31 import com.fasterxml.jackson.core.JsonParseException;
32 import com.fasterxml.jackson.databind.JsonMappingException;
33 import com.fasterxml.jackson.databind.ObjectMapper;
35 public class TileserverMapnik {
37 private static final Logger LOGGER = LoggerFactory.getLogger(TileserverMapnik.class);
39 private SystemProcess process;
40 private Path serverRoot;
42 private AtomicBoolean running = new AtomicBoolean(false);
44 TileserverMapnik(Path serverRoot) {
45 this.serverRoot = serverRoot.normalize();
48 public boolean isRunning() throws IOException, InterruptedException {
49 return process == null ? false : process.isAlive() && isReady();
52 public boolean isReady() {
56 public void start(Optional<TileserverStartListener> listener) throws Exception {
57 // check if existing server is left hanging
58 if (Files.exists(getPid())) {
59 String pid = new String(Files.readAllBytes(getPid()));
60 PidProcess pr = Processes.newPidProcess(Integer.parseInt(pid));
61 pr.destroyForcefully();
64 // check that npm dependencies are satisfied
65 // if (checkAndInstall(null, listener)) {
66 // checkAndInstall(ADDITIONAL_DEPENDENCIES[0], listener);
67 // checkAndInstall(ADDITIONAL_DEPENDENCIES[1], listener);
73 if (process != null && process.isAlive())
76 Path tileliveTesseraWin = serverRoot.resolve("dist").resolve("node.exe").normalize().toAbsolutePath();
77 StartedProcess startedProcess = new ProcessExecutor().directory(serverRoot.resolve("dist").toFile()).destroyOnExit().environment(getEnv())
78 .command(tileliveTesseraWin.toString(), "-c", getConfigJson().toString(), "-p", Integer.toString(MapsServerPreferences.defaultPort()))
79 .redirectOutput(new Slf4jDebugOutputStream(LOGGER) {
82 protected void processLine(String line) {
83 // Convert to UTF-8 string
84 String utf8Line = new String(line.getBytes(), StandardCharsets.UTF_8);
89 Process nativeProcess = startedProcess.getProcess();
90 process = Processes.newStandardProcess(nativeProcess);
91 int pid = PidUtil.getPid(nativeProcess);
92 LOGGER.info("Writing pid-file to {} with pid={}", getPid(), pid);
93 Files.write(getPid(), (pid + "").getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
95 listener.ifPresent(TileserverStartListener::started);
98 private Map<String, String> getEnv() {
99 Map<String, String> env = new HashMap<>();
100 env.put("MAPNIK_FONT_PATH", getFonts().toString());
101 env.put("ICU_DATA", getICU().toString());
105 public void stop() throws IOException, InterruptedException {
106 if (process != null && process.isAlive()) {
107 // gracefully not supported on windows
108 process.destroyForcefully();
109 if (!process.waitFor(2000, TimeUnit.MILLISECONDS)) {
110 process.destroyForcefully();
111 if (process.isAlive())
112 LOGGER.error("Could not shutdown TileserverMapnik!");
115 Files.delete(getPid());
120 private Path getPid() {
121 return serverRoot.resolve("pid");
124 public void restart(Optional<TileserverStartListener> listener) throws Exception {
129 // private boolean checkIfInstalled(String module) throws Exception {
130 // String tileserverMapnik = tileserverMapnikRoot().toString();
132 // try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
133 // if (module == null) {
134 // retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list");
136 // retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list", module);
138 // String outputString = new String(output.toByteArray(), StandardCharsets.UTF_8);
140 // return retVal == 0;
143 // private int install(String module, Optional<TileserverStartListener> listener) throws Exception {
144 // String tileserverMapnik = tileserverMapnikRoot().toString();
146 // if (module == null) {
147 // listener.ifPresent(l -> l.installing("Installing tileserver-mapnik"));
148 // retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", "--save");
150 // retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", module, "--save");
153 // LOGGER.warn("Could not install module " + module == null ? "package.json" : module + "! " + retVal);
157 // private boolean checkAndInstall(String module, Optional<TileserverStartListener> listener) throws Exception {
158 // LOGGER.info("Installing module {}", String.valueOf(module));
159 // boolean installed = checkIfInstalled(module);
161 // int installSuccessfull = install(module, listener);
162 // if (installSuccessfull != 0) {
163 // LOGGER.warn("Installation of module {} failed!", String.valueOf(module));
164 // listener.ifPresent(l -> l.installationFailed(module, installSuccessfull));
167 // LOGGER.info("Module {} was already installed", String.valueOf(module));
169 // return !installed;
173 @SuppressWarnings("unused")
174 private Path tileserverMapnikRoot() {
175 return serverRoot.resolve("tileserver-mapnik").toAbsolutePath();
178 private Path getFonts() {
179 return serverRoot.resolve("fonts").toAbsolutePath();
182 private Path getICU() {
183 return serverRoot.resolve("dist/share/icu").toAbsolutePath();
186 @SuppressWarnings("unused")
187 private Path getTessera() {
188 return serverRoot.resolve("tileserver-mapnik/bin/tessera.js").toAbsolutePath();
191 private Path getConfigJson() {
192 return serverRoot.resolve("config.json").toAbsolutePath();
195 public List<String> availableMBTiles() throws IOException {
196 Path data = getDataDirectory();
197 List<String> result = new ArrayList<>();
198 try (Stream<Path> paths = Files.walk(data)) {
200 if (!p.equals(data)) {
201 String tiles = p.getFileName().toString();
209 private void checkConfigJson() throws JsonParseException, JsonMappingException, IOException {
210 Path configJson = getConfigJson();
211 Map<String, Object> config = new HashMap<>();
212 Path tm2 = getStyleDirectory();
213 try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
214 stream.forEach(p -> {
215 Path projectYaml = p.resolve("project.yml");
216 if (Files.exists(projectYaml)) {
219 Map<String, String> source = new HashMap<>();
221 Path tm2style = serverRoot.relativize(projectYaml.getParent());
223 String prefix = "tmstyle://../";
224 String tmStyle = prefix + tm2style.toString();
225 source.put("source", tmStyle);
226 config.put("/" + projectYaml.getParent().getFileName().toString(), source);
230 ObjectMapper mapper = new ObjectMapper();
231 mapper.writerWithDefaultPrettyPrinter().writeValue(Files.newOutputStream(configJson, StandardOpenOption.TRUNCATE_EXISTING), config);
234 public Path getStyleDirectory() {
235 return serverRoot.resolve("tm2");
238 public Path getDataDirectory() {
239 return serverRoot.resolve("data");
242 public Path getCurrentTiles() {
243 return getDataDirectory().resolve(MapsServerPreferences.currentMBTiles());
246 @SuppressWarnings("unchecked")
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);
257 Path tiles = serverRoot.relativize(getCurrentTiles());
259 String tmStyle = "mbtiles://../" + tiles.toString();
260 data.put("source", tmStyle);
262 } catch (IOException e) {
263 LOGGER.error("Could not read yaml", e);
266 Files.write(projectYaml, yaml.dump(data).getBytes(), StandardOpenOption.TRUNCATE_EXISTING);
267 } catch (IOException e) {
268 LOGGER.error("Could not write yaml", e);
271 } catch (IOException e) {
272 LOGGER.error("Could not browse tm2 styles", e);
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());
287 public static interface TileserverStartListener {
289 void installing(String module);
291 void installationFailed(String module, int returnValue);