com.cloudbees.simplediskusage.QuickDiskUsagePlugin.java Source code

Java tutorial

Introduction

Here is the source code for com.cloudbees.simplediskusage.QuickDiskUsagePlugin.java

Source

/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2015, CloudBees, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.cloudbees.simplediskusage;

import hudson.*;
import hudson.model.Job;
import hudson.model.TaskListener;
import hudson.model.TopLevelItem;
import hudson.security.ACL;
import jenkins.model.Jenkins;
import jenkins.util.Timer;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;

import javax.inject.Singleton;
import javax.servlet.ServletException;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

@Extension
@Singleton
public class QuickDiskUsagePlugin extends Plugin {

    public static final String DEFAULT_DU_COMMAND = "ionice -c 3 du -ks";

    public static final String DU_COMMAND = System.getProperty(QuickDiskUsagePlugin.class.getName() + ".command",
            DEFAULT_DU_COMMAND);

    public static final int QUIET_PERIOD = 15 * 60 * 1000;

    private static Executor ex = Executors.newSingleThreadExecutor(new ThreadFactory() {
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setName("Simple disk usage checker");
            return t;
        }
    });

    private static final Logger logger = Logger.getLogger(QuickDiskUsagePlugin.class.getName());

    private CopyOnWriteArrayList<DiskItem> directoriesUsages = new CopyOnWriteArrayList<>();

    private CopyOnWriteArrayList<JobDiskItem> jobsUsages = new CopyOnWriteArrayList<>();

    private long lastRunStart = 0;

    private long lastRunEnd = 0;

    @Override
    public void start() throws Exception {
        try {
            load();
        } catch (IOException e) {
            logger.log(Level.WARNING, "Failed to load " + getConfigXml(), e);
        }
        if (isRunning()) {
            // It's impossible, the plugin was just loaded. Let's reset end date
            lastRunEnd = lastRunStart;
        }
    }

    public void refreshData() {
        if (!isRunning()) {
            ex.execute(computeDiskUsage);
        }
    }

    public CopyOnWriteArrayList<DiskItem> getDirectoriesUsages() throws IOException {
        if (System.currentTimeMillis() - lastRunEnd >= QUIET_PERIOD) {
            refreshData();
        }
        return directoriesUsages;
    }

    public CopyOnWriteArrayList<JobDiskItem> getJobsUsages() throws IOException {
        if (System.currentTimeMillis() - lastRunEnd >= QUIET_PERIOD) {
            refreshData();
        }
        return jobsUsages;
    }

    public long getLastRunStart() {
        return lastRunStart;
    }

    public long getLastRunEnd() {
        return lastRunEnd;
    }

    public String getSince() {
        return Util.getPastTimeString(System.currentTimeMillis() - lastRunEnd);
    }

    public String getDuration() {
        return Util.getTimeSpanString(lastRunEnd - lastRunStart);
    }

    public boolean isRunning() {
        return lastRunEnd < lastRunStart;
    }

    @RequirePOST
    public void doRefresh(StaplerRequest req, StaplerResponse res) throws IOException, ServletException {
        refreshData();
        res.forwardToPreviousPage(req);
    }

    @RequirePOST
    public void doClean(StaplerRequest req, StaplerResponse res) throws IOException, ServletException {
        // TODO switch to Jenkins.getActiveInstance() once 1.590+ is the baseline
        Jenkins jenkins = Jenkins.getInstance();
        if (jenkins == null) {
            throw new IllegalStateException("Jenkins has not been started, or was already shut down");
        }
        final Job job = jenkins.getItemByFullName(req.getParameter("job"), Job.class);
        Timer.get().submit(new Runnable() {
            @Override
            public void run() {
                try {
                    job.logRotate();
                } catch (Exception e) {
                    logger.log(Level.WARNING, "logRotate failed", e);
                }
            }
        });
        res.forwardToPreviousPage(req);
    }

    private long computeDiskUsage(File path) throws IOException, InterruptedException {
        if (path == null || !path.exists() || !path.isDirectory())
            return -1;
        logger.fine("Estimating usage for: " + path.getAbsolutePath());
        // TODO switch to Jenkins.getActiveInstance() once 1.590+ is the baseline
        Jenkins jenkins = Jenkins.getInstance();
        if (jenkins == null) {
            throw new IllegalStateException("Jenkins has not been started, or was already shut down");
        }

        // this write operation will lock the current thread if filesystem is frozen
        // otherwise reads could block freeze operation and slow down snapshotting
        FilePath jenkinsHome = jenkins.getRootPath();
        if (jenkinsHome != null) {
            jenkinsHome.touch(System.currentTimeMillis());
        } else {
            return -1;
        }

        Launcher.LocalLauncher launcher = new Launcher.LocalLauncher(TaskListener.NULL);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        Proc proc = launcher.launch().cmds(StringUtils.split(DU_COMMAND)).pwd(path).stdout(out).start();

        // give up after 20 seconds and kill 'du' process to prevent reads to put disk down to its knees
        // before the next attempt, we might be blocked by the write operation above
        int status = proc.joinWithTimeout(20, TimeUnit.SECONDS, TaskListener.NULL);

        switch (status) {
        case 0:
            try {
                return Long.parseLong(StringUtils.removeEnd(out.toString("UTF-8"), "\t.\n"));
            } catch (NumberFormatException e) {
                return -1;
            }
        case 143:
            logger.warning("Time to compute the size of '" + path.getCanonicalPath()
                    + "' is too long. 'du' process killed after 20 seconds of activity. You might be experiencing storage slowness.");
            return -1;
        }

        return -1;
    }

    private JobDiskItem computeJobUsage(Job job) throws IOException, InterruptedException {
        return new JobDiskItem(job, computeDiskUsage(job.getRootDir()));
    }

    private void computeJobsUsages() throws IOException, InterruptedException {
        // TODO switch to Jenkins.getActiveInstance() once 1.590+ is the baseline
        Jenkins jenkins = Jenkins.getInstance();
        if (jenkins == null) {
            throw new IllegalStateException("Jenkins has not been started, or was already shut down");
        }
        // Remove useless entries for jobs
        for (JobDiskItem item : jobsUsages) {
            if (!item.getPath().exists() || jenkins.getItemByFullName(item.getFullName(), Job.class) == null) {
                jobsUsages.remove(item);
            }
        }
        // Add or update entries for jobs
        for (Job item : jenkins.getAllItems(Job.class)) {
            if (item instanceof TopLevelItem) {
                JobDiskItem usage = computeJobUsage(item);
                if (usage != null) {
                    if (jobsUsages.contains(usage)) {
                        jobsUsages.remove(usage);
                    }
                    jobsUsages.add(usage);
                }

            }
        }
    }

    private DiskItem computeDirectoryUsage(String displayName, File path) throws IOException, InterruptedException {
        return new DiskItem(displayName, path, computeDiskUsage(path));
    }

    private void computeDirectoriesUsages() throws IOException, InterruptedException {
        // TODO switch to Jenkins.getActiveInstance() once 1.590+ is the baseline
        Jenkins jenkins = Jenkins.getInstance();
        if (jenkins == null) {
            throw new IllegalStateException("Jenkins has not been started, or was already shut down");
        }
        Map<File, String> directoriesToProcess = new HashMap<>();
        // Display JENKINS_HOME size
        directoriesToProcess.put(jenkins.getRootDir(), "JENKINS_HOME");
        // Display JENKINS_HOME first level sub-directories
        File[] jenkinsHomeRootDirectories = jenkins.getRootDir().listFiles();
        if (jenkinsHomeRootDirectories != null) {
            for (File child : jenkinsHomeRootDirectories) {
                if (child.isDirectory()) {
                    directoriesToProcess.put(child, "JENKINS_HOME/" + child.getName());
                }
            }
        }
        // Display java.io.tmpdir size
        directoriesToProcess.put(new File(System.getProperty("java.io.tmpdir")), "java.io.tmpdir");

        // Remove useless entries for directories
        for (DiskItem item : directoriesUsages) {
            if (!item.getPath().exists() || !directoriesToProcess.containsKey(item.getPath())) {
                directoriesUsages.remove(item);
            }
        }

        // Add or update entries for directories
        for (Map.Entry<File, String> item : directoriesToProcess.entrySet()) {
            DiskItem usage = computeDirectoryUsage(item.getValue(), item.getKey());
            if (usage != null) {
                if (directoriesUsages.contains(usage)) {
                    directoriesUsages.remove(usage);
                }
                directoriesUsages.add(usage);
            }
            Thread.sleep(1000); //To keep load average nice and low
        }
    }

    private transient final Runnable computeDiskUsage = new Runnable() {
        public void run() {
            logger.info("Re-estimating disk usage");
            lastRunStart = System.currentTimeMillis();
            SecurityContext impersonate = ACL.impersonate(ACL.SYSTEM);
            // TODO switch to Jenkins.getActiveInstance() once 1.590+ is the baseline
            Jenkins jenkins = Jenkins.getInstance();
            if (jenkins == null) {
                throw new IllegalStateException("Jenkins has not been started, or was already shut down");
            }
            try {
                computeJobsUsages();
                computeDirectoriesUsages();
                logger.info("Finished re-estimating disk usage.");
                lastRunEnd = System.currentTimeMillis();
            } catch (IOException | InterruptedException e) {
                logger.log(Level.INFO, "Unable to run disk usage check", e);
                lastRunEnd = lastRunStart;
            } finally {
                SecurityContextHolder.setContext(impersonate);
            }
            try {
                // Save data
                save();
            } catch (IOException e) {
                logger.log(Level.WARNING, "Failed to save " + getConfigXml(), e);
            }
        }
    };

}