/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.tomee.livereload; import org.apache.johnzon.mapper.Mapper; import org.apache.openejb.util.LogCategory; import org.apache.openejb.util.Logger; import javax.websocket.CloseReason; import javax.websocket.Session; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.nio.file.ClosedWatchServiceException; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; public class FileWatcher { private final Logger logger; private final Mapper mapper; private final Collection<Session> sessions = new ArrayList<>(); public FileWatcher(final LogCategory logger, final Mapper mapper) { this.logger = Logger.getInstance(logger, FileWatcher.class); this.mapper = mapper; } public synchronized void addSession(final Session session) { sessions.add(session); } public synchronized void removeSession(final Session session) { sessions.remove(session); } public Closeable watch(final String folder) { final File file = new File(folder); if (!file.isDirectory()) { throw new IllegalArgumentException(folder + " is not a directory"); } try { final AtomicBoolean again = new AtomicBoolean(true); final Path path = file.getAbsoluteFile().toPath(); final WatchService watchService = path.getFileSystem().newWatchService(); path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); final Thread watcherThread = new Thread(new Runnable() { @Override public void run() { while (again.get()) { try { final WatchKey key = watchService.poll(1, TimeUnit.SECONDS); // don't use take to not block forever if (key == null) { continue; } for (final WatchEvent<?> event : key.pollEvents()) { final WatchEvent.Kind<?> kind = event.kind(); if (kind != StandardWatchEventKinds.ENTRY_CREATE && kind != StandardWatchEventKinds.ENTRY_DELETE && kind != StandardWatchEventKinds.ENTRY_MODIFY) { continue; } final Path updatedPath = Path.class.cast(event.context()); if (kind == StandardWatchEventKinds.ENTRY_DELETE || updatedPath.toFile().isFile()) { final String path = updatedPath.toString(); if (path.endsWith("___jb_tmp___") || path.endsWith("___jb_old___")) { continue; } else if (path.endsWith("~")) { onChange(path.replace(File.pathSeparatorChar, '/').substring(0, path.length() - 1)); } else { onChange(path.replace(File.pathSeparatorChar, '/')); } } } key.reset(); } catch (final InterruptedException e) { Thread.interrupted(); again.set(false); } catch (final ClosedWatchServiceException cwse) { // ok, we finished there } } } }); watcherThread.setName("livereload-tomee-watcher(" + folder + ")"); watcherThread.start(); return new Closeable() { @Override public void close() throws IOException { synchronized (this) { for (final Session s : sessions) { removeSession(s); try { s.close(new CloseReason(CloseReason.CloseCodes.GOING_AWAY, "container shutdowned")); } catch (final Exception e) { // ok: not important there } } } again.compareAndSet(true, false); try { watchService.close(); } catch (final IOException ioe) { logger.warning("Error closing the watch service for " + folder + "(" + ioe.getMessage() + ")"); } try { watcherThread.join(TimeUnit.MINUTES.toMillis(1)); } catch (final InterruptedException e) { Thread.interrupted(); } } }; } catch (final IOException e) { throw new IllegalArgumentException(e); } } private void onChange(final String path) { final Command command = new Command(); command.setCommand("reload"); command.setPath(path); command.setLiveCss(true); // TODO: check path to set it to false if not relevant? final String asStr = mapper.writeObjectAsString(command); final Collection<Session> copy; synchronized (this) { copy = new ArrayList<>(this.sessions); } int failed = 0; for (final Session s : copy) { try { s.getBasicRemote().sendText(asStr); } catch (final Exception e) { logger.warning("Can't send one livereload update: " + e.getMessage()); failed++; } } if (failed < copy.size()) { logger.info("Updated livereload path: " + path); } } }