/* * dCache - http://www.dcache.org/ * * Copyright (C) 2016 Deutsches Elektronen-Synchrotron * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package diskCacheV111.doors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import dmg.cells.nucleus.CellCommandListener; import dmg.cells.nucleus.CellInfoProvider; import dmg.cells.nucleus.CellMessageReceiver; import dmg.cells.nucleus.NoRouteToCellException; import dmg.util.CommandExitException; import dmg.util.StreamEngine; import org.dcache.cells.AbstractCell; import org.dcache.poolmanager.PoolManagerHandler; import org.dcache.util.Args; import org.dcache.util.CDCExecutorServiceDecorator; import org.dcache.util.SequentialExecutor; import org.dcache.util.Transfer; /** * Door cell for line based protocols. * * <p>To be used with LoginManager. The cell reads lines from a StreamEngine * and passes these to an interpreter for processing. * * <p>The cell is able to detect end-of-stream even while the interpreter * is processing a line. */ public class LineBasedDoor extends AbstractCell implements Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(LineBasedDoor.class); /** * Door instances are created by the LoginManager. This is the * stream engine passed to us from the LoginManager upon * instantiation. */ private final StreamEngine engine; private final LineBasedInterpreterFactory factory; private final CountDownLatch shutdownGate = new CountDownLatch(1); /** * Executor for processing commands. */ private final Executor executor; private final PoolManagerHandler poolManager; private LineBasedInterpreter interpreter; private volatile boolean isStartupCompleted; public LineBasedDoor(String cellName, Args args, LineBasedInterpreterFactory factory, StreamEngine engine, ExecutorService executor, PoolManagerHandler poolManagerHandler) { super(cellName, args, executor); this.factory = factory; this.engine = engine; this.executor = new CDCExecutorServiceDecorator<>(executor); this.poolManager = poolManagerHandler; } @Override protected void starting() throws Exception { Transfer.initSession(false, true); super.starting(); LOGGER.debug("Client host: {}", engine.getInetAddress().getHostAddress()); interpreter = factory.create(this, getNucleus().getThisAddress(), engine, executor, poolManager); if (interpreter instanceof CellCommandListener) { addCommandListener(interpreter); } if (interpreter instanceof CellMessageReceiver) { addMessageListener((CellMessageReceiver) interpreter); } } @Override protected void started() { executor.execute(this); isStartupCompleted = true; } private synchronized void shutdownInputStream() { try { Socket socket = engine.getSocket(); if (!socket.isClosed() && !socket.isInputShutdown()) { socket.shutdownInput(); } } catch (IOException e) { LOGGER.info("Failed to shut down input stream of the " + "control channel: {}", e.getMessage()); } } /** * Main loop for command processing. * * Commands are read from the socket and submitted to the command * queue for execution. Upon termination, most of the shutdown * logic is in this method, including: * * - Emergency shutdown of performance marker engine * - Shut down of passive mode server socket * - Abort and cleanup after failed transfers * - Cell shutdown initiation * * Notice that socket and thus input and output streams are not * closed here. See cleanUp() for details on this. */ @Override public void run() { try { SequentialExecutor executor = new SequentialExecutor(this.executor); try { /* Notice that we do not close the input stream, as * doing so would close the socket as well. We don't * want to do that until cleanUp() is called. * * REVISIT: I hope that the StreamEngine does not * maintain any resources that do not get * automatically freed when the socket is closed. */ BufferedReader in = new BufferedReader(new InputStreamReader(engine.getInputStream(), "UTF-8")); String s = in.readLine(); while (s != null) { executor.execute(new Command(s)); s = in.readLine(); } } catch (IOException e) { LOGGER.error("Got error reading data: {}", e.getMessage()); } finally { try { executor.shutdownNow(); interpreter.shutdown(); executor.awaitTermination(); } catch (InterruptedException e) { LOGGER.error("Failed to shut down command processing: {}", e.getMessage()); } LOGGER.debug("End of stream encountered"); } } finally { /* cleanUp() waits for us to open the gate. */ shutdownGate.countDown(); /* Killing the cell will cause cleanUp() to be * called (although from a different thread). */ kill(); } } public void messageArrived(NoRouteToCellException e) { LOGGER.warn(e.getMessage()); } /** * Called by the cell infrastructure when the cell has been killed. * * The socket will be closed by this method. It is quite important * that this does not happen earlier, as several threads use the * output stream. This is the only place where we can be 100% * certain, that all the other threads are done with their job. * * The method blocks until the worker thread has terminated. */ @Override public void stopped() { /* Closing the input stream will cause the FTP command * processing thread to shut down. In case the shutdown was * initiated by the FTP client, this will already have * happened at this point. However if the cell is shut down * explicitly, then we have to shutdown the input stream here. */ shutdownInputStream(); if (isStartupCompleted) { /* The FTP command processing thread will open the gate after * shutdown. */ try { shutdownGate.await(); } catch (InterruptedException e) { /* This should really not happen as nobody is supposed to * interrupt the cell thread, but if it does happen then * we better log it. */ LOGGER.error("Got interrupted exception shutting down input stream"); } } try { /* Closing the socket will also close the input and output * streams of the socket. This in turn will cause the * command poller thread to shut down. */ engine.getSocket().close(); } catch (IOException e) { LOGGER.error("Got I/O exception closing socket: {}", e.getMessage()); } super.stopped(); } @Override public void getInfo(PrintWriter pw) { pw.println(" User Host : " + engine.getInetAddress().getHostAddress()); if (interpreter instanceof CellInfoProvider) { ((CellInfoProvider) interpreter).getInfo(pw); } } private class Command implements Runnable { private final String command; public Command(String command) { this.command = command; } @Override public void run() { try { interpreter.execute(command); } catch (CommandExitException e) { shutdownInputStream(); } catch (RuntimeException e) { LOGGER.error("Bug detected", e); } } } }