org.dcache.xrootd.standalone.DataServerHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.dcache.xrootd.standalone.DataServerHandler.java

Source

/**
 * Copyright (C) 2011-2018 dCache.org <support@dcache.org>
 *
 * This file is part of xrootd4j.
 *
 * xrootd4j is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * xrootd4j 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with xrootd4j.  If not, see http://www.gnu.org/licenses/.
 */
package org.dcache.xrootd.standalone;

import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import io.netty.channel.ChannelHandlerContext;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

import org.dcache.xrootd.core.XrootdException;
import org.dcache.xrootd.core.XrootdRequestHandler;
import org.dcache.xrootd.protocol.messages.CloseRequest;
import org.dcache.xrootd.protocol.messages.DirListRequest;
import org.dcache.xrootd.protocol.messages.DirListResponse;
import org.dcache.xrootd.protocol.messages.GenericReadRequestMessage.EmbeddedReadRequest;
import org.dcache.xrootd.protocol.messages.LocateRequest;
import org.dcache.xrootd.protocol.messages.LocateResponse;
import org.dcache.xrootd.protocol.messages.MkDirRequest;
import org.dcache.xrootd.protocol.messages.MvRequest;
import org.dcache.xrootd.protocol.messages.OkResponse;
import org.dcache.xrootd.protocol.messages.OpenRequest;
import org.dcache.xrootd.protocol.messages.OpenResponse;
import org.dcache.xrootd.protocol.messages.PrepareRequest;
import org.dcache.xrootd.protocol.messages.ProtocolRequest;
import org.dcache.xrootd.protocol.messages.ProtocolResponse;
import org.dcache.xrootd.protocol.messages.QueryRequest;
import org.dcache.xrootd.protocol.messages.QueryResponse;
import org.dcache.xrootd.protocol.messages.ReadRequest;
import org.dcache.xrootd.protocol.messages.ReadVRequest;
import org.dcache.xrootd.protocol.messages.RmDirRequest;
import org.dcache.xrootd.protocol.messages.RmRequest;
import org.dcache.xrootd.protocol.messages.SetRequest;
import org.dcache.xrootd.protocol.messages.SetResponse;
import org.dcache.xrootd.protocol.messages.StatRequest;
import org.dcache.xrootd.protocol.messages.StatResponse;
import org.dcache.xrootd.protocol.messages.StatxRequest;
import org.dcache.xrootd.protocol.messages.StatxResponse;
import org.dcache.xrootd.protocol.messages.SyncRequest;
import org.dcache.xrootd.protocol.messages.WriteRequest;
import org.dcache.xrootd.protocol.messages.ZeroCopyReadResponse;
import org.dcache.xrootd.stream.ChunkedFileChannelReadResponse;
import org.dcache.xrootd.stream.ChunkedFileReadvResponse;
import org.dcache.xrootd.util.FileStatus;

import static org.dcache.xrootd.protocol.XrootdProtocol.*;

public class DataServerHandler extends XrootdRequestHandler {
    private static final Logger _log = LoggerFactory.getLogger(DataServerHandler.class);

    /**
     * Maximum frame size of a read or readv reply. Does not include the size
     * of the frame header.
     */
    private static final int MAX_FRAME_SIZE = 2 << 20;

    private final List<RandomAccessFile> _openFiles = new ArrayList<>();

    private final DataServerConfiguration _configuration;

    public DataServerHandler(DataServerConfiguration configuration) {
        _configuration = configuration;
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {
        if (t instanceof ClosedChannelException) {
            _log.info("Connection closed");
        } else if (t instanceof RuntimeException || t instanceof Error) {
            Thread me = Thread.currentThread();
            me.getUncaughtExceptionHandler().uncaughtException(me, t);
        } else {
            _log.warn(t.toString());
        }
        // TODO: If not already closed, we should probably close the
        // channel.
    }

    @Override
    protected ProtocolResponse doOnProtocolRequest(ChannelHandlerContext ctx, ProtocolRequest msg) {
        return new ProtocolResponse(msg, DATA_SERVER);
    }

    @Override
    protected StatResponse doOnStat(ChannelHandlerContext ctx, StatRequest req) throws XrootdException {
        File file = getFile(req.getPath());
        if (!file.exists()) {
            throw new XrootdException(kXR_NotFound, "No such file");
        } else {
            FileStatus fs = getFileStatusOf(file);
            return new StatResponse(req, fs);
        }
    }

    @Override
    protected StatxResponse doOnStatx(ChannelHandlerContext ctx, StatxRequest req) throws XrootdException {
        if (req.getPaths().length == 0) {
            throw new XrootdException(kXR_ArgMissing, "no paths specified");
        }

        String[] paths = req.getPaths();
        String[] opaques = req.getOpaques();
        int[] flags = new int[paths.length];
        for (int i = 0; i < paths.length; i++) {
            File file = getFile(paths[i]);
            if (!file.exists()) {
                flags[i] = kXR_other;
            } else {
                flags[i] = getFileStatusFlagsOf(file);
            }
        }

        return new StatxResponse(req, flags);
    }

    @Override
    protected OkResponse<RmRequest> doOnRm(ChannelHandlerContext ctx, RmRequest req) throws XrootdException {
        if (req.getPath().isEmpty()) {
            throw new XrootdException(kXR_ArgMissing, "no path specified");
        }

        File file = getFile(req.getPath());
        if (!file.exists()) {
            throw new XrootdException(kXR_NotFound, "No such directory or file: " + file);
        } else if (!file.isFile()) {
            throw new XrootdException(kXR_NotFile, "Not a file: " + file);
        } else if (!file.delete()) {
            throw new XrootdException(kXR_IOError, "Failed to delete file: " + file);
        }
        return withOk(req);
    }

    @Override
    protected OkResponse<RmDirRequest> doOnRmDir(ChannelHandlerContext ctx, RmDirRequest req)
            throws XrootdException {
        if (req.getPath().isEmpty()) {
            throw new XrootdException(kXR_ArgMissing, "no path specified");
        }

        File file = getFile(req.getPath());
        if (!file.exists()) {
            throw new XrootdException(kXR_NotFound, "No such directory or file: " + file);
        } else if (!file.isDirectory()) {
            throw new XrootdException(kXR_IOError, "Not a directory: " + file);
        } else if (!file.delete()) {
            throw new XrootdException(kXR_IOError, "Failed to delete directory: " + file);
        }
        return withOk(req);
    }

    @Override
    protected OkResponse<MkDirRequest> doOnMkDir(ChannelHandlerContext ctx, MkDirRequest req)
            throws XrootdException {
        if (req.getPath().isEmpty()) {
            throw new XrootdException(kXR_ArgMissing, "no path specified");
        }

        File file = getFile(req.getPath());
        if (file.exists()) {
            throw new XrootdException(kXR_IOError, "Path exists: " + file);
        }
        if (req.shouldMkPath()) {
            if (!file.mkdirs()) {
                throw new XrootdException(kXR_IOError, "Failed to create directories: " + file);
            }
        } else {
            if (!file.mkdir()) {
                throw new XrootdException(kXR_IOError, "Failed to create directory: " + file);
            }
        }
        return withOk(req);
    }

    @Override
    protected OkResponse<MvRequest> doOnMv(ChannelHandlerContext ctx, MvRequest req) throws XrootdException {
        String sourcePath = req.getSourcePath();
        if (sourcePath.isEmpty()) {
            throw new XrootdException(kXR_ArgMissing, "No source path specified");
        }

        String targetPath = req.getTargetPath();
        if (targetPath.isEmpty()) {
            throw new XrootdException(kXR_ArgMissing, "No target path specified");
        }

        File sourceFile = getFile(req.getSourcePath());
        if (!sourceFile.exists()) {
            throw new XrootdException(kXR_NotFound, "No such file");
        }
        File targetFile = getFile(req.getTargetPath());
        if (!sourceFile.renameTo(targetFile)) {
            throw new XrootdException(kXR_IOError, "Failed to move file");
        }
        return withOk(req);
    }

    @Override
    protected DirListResponse doOnDirList(ChannelHandlerContext context, DirListRequest request)
            throws XrootdException {
        String listPath = request.getPath();
        if (listPath.isEmpty()) {
            throw new XrootdException(kXR_ArgMissing, "no source path specified");
        }

        Path dir = getFile(listPath).toPath();
        try (DirectoryStream<Path> paths = Files.newDirectoryStream(dir)) {
            DirListResponse.Builder builder = DirListResponse.builder(request);
            for (Path path : paths) {
                builder.add(path.getFileName().toString(),
                        request.isDirectoryStat() ? getFileStatusOf(path.toFile()) : null);
                if (builder.count() >= 1000) {
                    respond(context, builder.buildPartial());
                }
            }
            return builder.buildFinal();
        } catch (FileNotFoundException e) {
            throw new XrootdException(kXR_NotFound, "No such directory: " + dir);
        } catch (NotDirectoryException e) {
            throw new XrootdException(kXR_IOError, "Not a directory: " + dir);
        } catch (IOException e) {
            throw new XrootdException(kXR_IOError, "IO Error: " + dir);
        }
    }

    @Override
    protected OkResponse<PrepareRequest> doOnPrepare(ChannelHandlerContext ctx, PrepareRequest msg) {
        return withOk(msg);
    }

    /**
     * Obtains the right mover instance using an opaque token in the
     * request and instruct the mover to open the file in the request.
     * Associates the mover with the file-handle that is produced during
     * processing
     */
    @Override
    protected OpenResponse doOnOpen(ChannelHandlerContext ctx, OpenRequest msg) throws XrootdException {
        try {
            File file = getFile(msg.getPath());
            if (file.isDirectory()) {
                throw new XrootdException(kXR_isDirectory, "Not a file: " + file);
            }

            File parent = file.getParentFile();

            RandomAccessFile raf;
            if (msg.isReadWrite()) {
                if (msg.isMkPath() && !parent.exists() && !parent.mkdirs()) {
                    throw new XrootdException(kXR_IOError, "Failed to create directories: " + parent);
                }
                if (msg.isNew() && !file.createNewFile()) {
                    throw new XrootdException(kXR_IOError, "Failed to create file: " + file);
                }
                raf = new RandomAccessFile(file, "rw");
            } else {
                raf = new RandomAccessFile(file, "r");
            }

            try {
                if (msg.isReadWrite() && msg.isDelete()) {
                    raf.setLength(0);
                }

                FileStatus stat = null;
                if (msg.isRetStat()) {
                    stat = getFileStatusOf(file);
                }

                int fd = addOpenFile(raf);
                raf = null;
                return new OpenResponse(msg, fd, null, null, stat);
            } finally {
                if (raf != null) {
                    raf.close();
                }
            }
        } catch (FileNotFoundException e) {
            throw new XrootdException(kXR_NotFound, e.getMessage());
        } catch (IOException e) {
            throw new XrootdException(kXR_IOError, e.getMessage());
        }
    }

    /**
     * Use the file descriptor retrieved from the mover upon open and let it
     * obtain a reader object on the pool. The reader object will be placed
     * in a queue, from which it can be taken when sending read information
     * to the client.
     * @param ctx Received from the netty pipeline
     * @param msg The actual request
     */
    @Override
    protected Object doOnRead(ChannelHandlerContext ctx, ReadRequest msg) throws XrootdException {
        RandomAccessFile raf = getOpenFile(msg.getFileHandle());
        if (msg.bytesToRead() == 0) {
            return withOk(msg);
        } else if (_configuration.useZeroCopy) {
            try {
                return new ZeroCopyReadResponse(msg, raf.getChannel());
            } catch (IOException e) {
                throw new XrootdException(kXR_IOError, e.getMessage());
            }
        } else {
            return new ChunkedFileChannelReadResponse(msg, MAX_FRAME_SIZE, raf.getChannel());
        }
    }

    /**
     * Vector reads consist of several embedded read requests, which can even
     * contain different file handles. All the descriptors for the file
     * handles are looked up and passed to a vector reader. The vector reader
     * will use the descriptors connection to the mover that "owns" them to
     * update the mover's meta-information such as the number of bytes
     * transferred or the time of the last update.
     *
     * @param ctx received from the netty pipeline
     * @param msg The actual request.
     */
    @Override
    protected ChunkedFileReadvResponse doOnReadV(ChannelHandlerContext ctx, ReadVRequest msg)
            throws XrootdException {
        EmbeddedReadRequest[] requests = msg.getReadRequestList();
        if (requests == null || requests.length == 0) {
            throw new XrootdException(kXR_ArgMissing, "Request contains no vector");
        }

        return new ChunkedFileReadvResponse(msg, MAX_FRAME_SIZE, _openFiles);
    }

    /**
     * Retrieves the file descriptor obtained upon open and invokes
     * its write operation. The file descriptor will propagate necessary
     * function calls to the mover.
     *
     * @param ctx received from the netty pipeline
     * @param msg the actual request
     */
    @Override
    protected OkResponse<WriteRequest> doOnWrite(ChannelHandlerContext ctx, WriteRequest msg)
            throws XrootdException {
        try {
            FileChannel channel = getOpenFile(msg.getFileHandle()).getChannel();
            channel.position(msg.getWriteOffset());
            msg.getData(channel);
            return withOk(msg);
        } catch (IOException e) {
            throw new XrootdException(kXR_IOError, e.getMessage());
        }
    }

    /**
     * Retrieves the right mover based on the request's file-handle and
     * invokes its sync-operation.
     *
     * @param ctx received from the netty pipeline
     * @param msg The actual request
     */
    @Override
    protected OkResponse<SyncRequest> doOnSync(ChannelHandlerContext ctx, SyncRequest msg) throws XrootdException {
        try {
            getOpenFile(msg.getFileHandle()).getFD().sync();
            return withOk(msg);
        } catch (IOException e) {
            throw new XrootdException(kXR_IOError, e.getMessage());
        }
    }

    /**
     * Retrieves the right descriptor based on the request's file-handle and
     * invokes its close information.
     *
     * @param ctx received from the netty pipeline
     * @param msg The actual request
     */
    @Override
    protected OkResponse<CloseRequest> doOnClose(ChannelHandlerContext ctx, CloseRequest msg)
            throws XrootdException {
        try {
            closeOpenFile(msg.getFileHandle());
            return withOk(msg);
        } catch (IOException e) {
            throw new XrootdException(kXR_IOError, e.getMessage());
        }
    }

    @Override
    protected LocateResponse doOnLocate(ChannelHandlerContext ctx, LocateRequest msg) throws XrootdException {
        File file = getFile(stripLeadingAsterix(msg.getPath()));
        if (!file.exists()) {
            return new LocateResponse(msg);
        } else {
            return new LocateResponse(msg,
                    new LocateResponse.InfoElement((InetSocketAddress) ctx.channel().localAddress(),
                            LocateResponse.Node.SERVER,
                            file.canWrite() ? LocateResponse.Access.WRITE : LocateResponse.Access.READ));
        }
    }

    @Override
    protected QueryResponse doOnQuery(ChannelHandlerContext ctx, QueryRequest msg) throws XrootdException {
        switch (msg.getReqcode()) {
        case kXR_Qconfig:
            StringBuilder s = new StringBuilder();
            for (String name : msg.getArgs().split(" ")) {
                switch (name) {
                case "bind_max":
                    s.append(0);
                    break;
                case "readv_ior_max":
                    s.append(MAX_FRAME_SIZE);
                    break;
                case "readv_iov_max":
                    s.append(Integer.MAX_VALUE);
                    break;
                case "csname":
                    s.append("1:ADLER32");
                    break;
                case "version":
                    s.append("xrootd4j");
                    break;
                default:
                    s.append(name);
                    break;
                }
                s.append('\n');
            }
            return new QueryResponse(msg, s.toString());

        case kXR_Qcksum:
            try {
                HashCode hash = com.google.common.io.Files.asByteSource(getFile(msg.getArgs()))
                        .hash(Hashing.adler32());
                return new QueryResponse(msg, "ADLER32 " + hash);
            } catch (FileNotFoundException e) {
                throw new XrootdException(kXR_NotFound, e.getMessage());
            } catch (IOException e) {
                throw new XrootdException(kXR_IOError, e.getMessage());
            }

        default:
            throw new XrootdException(kXR_Unsupported, "Unsupported kXR_query reqcode: " + msg.getReqcode());
        }
    }

    @Override
    protected SetResponse doOnSet(ChannelHandlerContext ctx, SetRequest request) throws XrootdException {
        /* The xrootd spec states that we should include 80 characters in our log.
         */
        final String APPID_PREFIX = "appid ";
        final int APPID_PREFIX_LENGTH = APPID_PREFIX.length();
        final int APPID_MSG_LENGTH = 80;
        String data = request.getData();
        if (data.startsWith(APPID_PREFIX)) {
            _log.info(data.substring(APPID_PREFIX_LENGTH,
                    Math.min(APPID_PREFIX_LENGTH + APPID_MSG_LENGTH, data.length())));
        }
        return new SetResponse(request, "");
    }

    private String stripLeadingAsterix(String s) {
        return s.startsWith("*") ? s.substring(1) : s;
    }

    private int addOpenFile(RandomAccessFile raf) {
        for (int i = 0; i < _openFiles.size(); i++) {
            if (_openFiles.get(i) == null) {
                _openFiles.set(i, raf);
                return i;
            }
        }
        _openFiles.add(raf);
        return _openFiles.size() - 1;
    }

    private RandomAccessFile getOpenFile(int fd) throws XrootdException {
        if (fd >= 0 && fd < _openFiles.size()) {
            RandomAccessFile raf = _openFiles.get(fd);
            if (raf != null) {
                return raf;
            }
        }
        throw new XrootdException(kXR_FileNotOpen, "Invalid file descriptor");
    }

    private void closeOpenFile(int fd) throws XrootdException, IOException {
        getOpenFile(fd).close();
        _openFiles.set(fd, null);
    }

    private File getFile(String path) throws XrootdException {
        String normalized = FilenameUtils.normalize(path);
        if (normalized == null) {
            throw new XrootdException(kXR_ArgInvalid, "Invalid path: " + path);
        }
        return new File(_configuration.root, normalized);
    }

    private int getFileStatusFlagsOf(File file) {
        int flags = 0;
        if (file.isDirectory()) {
            flags |= kXR_isDir;
        }
        if (!file.isFile() && !file.isDirectory()) {
            flags |= kXR_other;
        }
        if (file.canExecute()) {
            flags |= kXR_xset;
        }
        if (file.canRead()) {
            flags |= kXR_readable;
        }
        if (file.canWrite()) {
            flags |= kXR_writable;
        }
        return flags;
    }

    private FileStatus getFileStatusOf(File file) {
        int flags = getFileStatusFlagsOf(file);
        return new FileStatus(0, file.length(), flags, file.lastModified() / 1000);
    }
}