<commit_before>/*
 * The MIT License
 * 
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., <NAME>, <NAME>
 * 
 * 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 hudson.slaves;

import hudson.model.*;
import hudson.util.IOException2;
import hudson.util.IOUtils;
import hudson.util.io.ReopenableRotatingFileOutputStream;
import jenkins.model.Jenkins.MasterComputer;
import hudson.remoting.Channel;
import hudson.remoting.VirtualChannel;
import hudson.remoting.Callable;
import hudson.util.StreamTaskListener;
import hudson.util.NullStream;
import hudson.util.RingBufferLogHandler;
import hudson.util.Futures;
import hudson.FilePath;
import hudson.Util;
import hudson.AbortException;
import hudson.remoting.Launcher;
import static hudson.slaves.SlaveComputer.LogHolder.SLAVE_LOG_HANDLER;
import hudson.slaves.OfflineCause.ChannelTermination;
import hudson.util.Secret;

import java.io.File;
import java.io.OutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.security.SecureRandom;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.Handler;
import java.util.List;
import java.util.Collections;
import java.util.ArrayList;
import java.nio.charset.Charset;
import java.util.concurrent.Future;
import java.security.Security;

import hudson.util.io.ReopenableFileOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.security.GeneralSecurityException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.RequestDispatcher;
import jenkins.model.Jenkins;
import jenkins.slaves.JnlpSlaveAgentProtocol;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpRedirect;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponseWrapper;
import org.kohsuke.stapler.ResponseImpl;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.compression.FilterServletOutputStream;

/**
 * {@link Computer} for {@link Slave}s.
 *
 * @author <NAME>
 */
public class SlaveComputer extends Computer {
    private volatile Channel channel;
    private volatile transient boolean acceptingTasks = true;
    private Charset defaultCharset;
    private Boolean isUnix;
    /**
     * Effective {@link ComputerLauncher} that hides the details of
     * how we launch a slave agent on this computer.
     *
     * <p>
     * This is normally the same as {@link Slave#getLauncher()} but
     * can be different. See {@link #grabLauncher(Node)}. 
     */
    private ComputerLauncher launcher;

    /**
     * Perpetually writable log file.
     */
    private final ReopenableFileOutputStream log;

    /**
     * {@link StreamTaskListener} that wraps {@link #log}, hence perpetually writable.
     */
    private final TaskListener taskListener;


    /**
     * Number of failed attempts to reconnect to this node
     * (so that if we keep failing to reconnect, we can stop
     * trying.)
     */
    private transient int numRetryAttempt;

    /**
     * Tracks the status of the last launch operation, which is always asynchronous.
     * This can be used to wait for the completion, or cancel the launch activity.
     */
    private volatile Future<?> lastConnectActivity = null;

    private Object constructed = new Object();

    public SlaveComputer(Slave slave) {
        super(slave);
        this.log = new ReopenableRotatingFileOutputStream(getLogFile(),10);
        this.taskListener = new StreamTaskListener(log);
        assert slave.getNumExecutors()!=0 : "Computer created with 0 executors";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isAcceptingTasks() {
        return acceptingTasks;
    }

    /**
     * @since 1.498
     */
    public String getJnlpMac() {
        return JnlpSlaveAgentProtocol.SLAVE_SECRET.mac(getName());
    }

    /**
     * Allows a {@linkplain hudson.slaves.ComputerLauncher} or a {@linkplain hudson.slaves.RetentionStrategy} to
     * suspend tasks being accepted by the slave computer.
     *
     * @param acceptingTasks {@code true} if the slave can accept tasks.
     */
    public void setAcceptingTasks(boolean acceptingTasks) {
        this.acceptingTasks = acceptingTasks;
    }

    /**
     * True if this computer is a Unix machine (as opposed to Windows machine).
     *
     * @return
     *      null if the computer is disconnected and therefore we don't know whether it is Unix or not.
     */
    public Boolean isUnix() {
        return isUnix;
    }

    @Override
    public Slave getNode() {
        return (Slave)super.getNode();
    }

    @Override
    public String getIcon() {
        Future<?> l = lastConnectActivity;
        if(l!=null && !l.isDone())
            return "computer-flash.gif";
        return super.getIcon();
    }

    /**
     * @deprecated since 2008-05-20.
     */
    @Deprecated @Override
    public boolean isJnlpAgent() {
        return launcher instanceof JNLPLauncher;
    }

    @Override
    public boolean isLaunchSupported() {
        return launcher.isLaunchSupported();
    }

    public ComputerLauncher getLauncher() {
        return launcher;
    }

    protected Future<?> _connect(boolean forceReconnect) {
        if(channel!=null)   return Futures.precomputed(null);
        if(!forceReconnect && isConnecting())
            return lastConnectActivity;
        if(forceReconnect && isConnecting())
            logger.fine("Forcing a reconnect on "+getName());

        closeChannel();
        return lastConnectActivity = Computer.threadPoolForRemoting.submit(new java.util.concurrent.Callable<Object>() {
            public Object call() throws Exception {
                // do this on another thread so that the lengthy launch operation
                // (which is typical) won't block UI thread.
                try {
                    log.rewind();
                    try {
                        for (ComputerListener cl : ComputerListener.all())
                            cl.preLaunch(SlaveComputer.this, taskListener);

                        launcher.launch(SlaveComputer.this, taskListener);
                    } catch (AbortException e) {
                        taskListener.error(e.getMessage());
                        throw e;
                    } catch (IOException e) {
                        Util.displayIOException(e,taskListener);
                        e.printStackTrace(taskListener.error(Messages.ComputerLauncher_unexpectedError()));
                        throw e;
                    } catch (InterruptedException e) {
                        e.printStackTrace(taskListener.error(Messages.ComputerLauncher_abortedLaunch()));
                        throw e;
                    }
                } finally {
                    if (channel==null) {
                        offlineCause = new OfflineCause.LaunchFailed();
                        for (ComputerListener cl : ComputerListener.all())
                            cl.onLaunchFailure(SlaveComputer.this, taskListener);
                    }
                }

                if (channel==null)
                    throw new IOException("Slave failed to connect, even though the launcher didn't report it. See the log output for details.");
                return null;
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void taskAccepted(Executor executor, Queue.Task task) {
        super.taskAccepted(executor, task);
        if (launcher instanceof ExecutorListener) {
            ((ExecutorListener)launcher).taskAccepted(executor, task);
        }
        
        //getNode() can return null at indeterminate times when nodes go offline
        Slave node = getNode();
        if (node != null && node.getRetentionStrategy() instanceof ExecutorListener) {
            ((ExecutorListener)node.getRetentionStrategy()).taskAccepted(executor, task);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void taskCompleted(Executor executor, Queue.Task task, long durationMS) {
        super.taskCompleted(executor, task, durationMS);
        if (launcher instanceof ExecutorListener) {
            ((ExecutorListener)launcher).taskCompleted(executor, task, durationMS);
        }
        RetentionStrategy r = getRetentionStrategy();
        if (r instanceof ExecutorListener) {
            ((ExecutorListener) r).taskCompleted(executor, task, durationMS);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void taskCompletedWithProblems(Executor executor, Queue.Task task, long durationMS, Throwable problems) {
        super.taskCompletedWithProblems(executor, task, durationMS, problems);
        if (launcher instanceof ExecutorListener) {
            ((ExecutorListener)launcher).taskCompletedWithProblems(executor, task, durationMS, problems);
        }
        RetentionStrategy r = getRetentionStrategy();
        if (r instanceof ExecutorListener) {
            ((ExecutorListener) r).taskCompletedWithProblems(executor, task, durationMS, problems);
        }
    }

    @Override
    public boolean isConnecting() {
        Future<?> l = lastConnectActivity;
        return isOffline() && l!=null && !l.isDone();
    }

    public OutputStream openLogFile() {
        try {
            log.rewind();
            return log;
        } catch (IOException e) {
            logger.log(Level.SEVERE, "Failed to create log file "+getLogFile(),e);
            return new NullStream();
        }
    }

    private final Object channelLock = new Object();

    public void setChannel(InputStream in, OutputStream out, TaskListener taskListener, Channel.Listener listener) throws IOException, InterruptedException {
        setChannel(in,out,taskListener.getLogger(),listener);
    }

    /**
     * Creates a {@link Channel} from the given stream and sets that to this slave.
     *
     * @param in
     *      Stream connected to the remote "slave.jar". It's the caller's responsibility to do
     *      buffering on this stream, if that's necessary.
     * @param out
     *      Stream connected to the remote peer. It's the caller's responsibility to do
     *      buffering on this stream, if that's necessary.
     * @param launchLog
     *      If non-null, receive the portion of data in <tt>is</tt> before
     *      the data goes into the "binary mode". This is useful
     *      when the established communication channel might include some data that might
     *      be useful for debugging/trouble-shooting.
     * @param listener
     *      Gets a notification when the channel closes, to perform clean up. Can be null.
     *      By the time this method is called, the cause of the termination is reported to the user,
     *      so the implementation of the listener doesn't need to do that again.
     */
    public void setChannel(InputStream in, OutputStream out, OutputStream launchLog, Channel.Listener listener) throws IOException, InterruptedException {
        Channel channel = new Channel(nodeName,threadPoolForRemoting, Channel.Mode.NEGOTIATE, in,out, launchLog);
        setChannel(channel,launchLog,listener);
    }

    /**
     * Shows {@link Channel#classLoadingCount}.
     * @since 1.495
     */
    public int getClassLoadingCount() throws IOException, InterruptedException {
        return channel.call(new LoadingCount(false));
    }

    /**
     * Shows {@link Channel#classLoadingPrefetchCacheCount}.
     * @return -1 in case that capability is not supported
     * @since XXX prefetch-JENKINS-15120
     */
    public int getClassLoadingPrefetchCacheCount() throws IOException, InterruptedException {
        if (!channel.remoteCapability.supportsPrefetch()) {
            return -1;
        }
        return channel.call(new LoadingPrefetchCacheCount());
    }

    /**
     * Shows {@link Channel#resourceLoadingCount}.
     * @since 1.495
     */
    public int getResourceLoadingCount() throws IOException, InterruptedException {
        return channel.call(new LoadingCount(true));
    }

    /**
     * Shows {@link Channel#classLoadingTime}.
     * @since 1.495<commit_msg>fixed @since<commit_after>/*
 * The MIT License
 * 
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., <NAME>, <NAME>
 * 
 * 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 hudson.slaves;

import hudson.model.*;
import hudson.util.IOException2;
import hudson.util.IOUtils;
import hudson.util.io.ReopenableRotatingFileOutputStream;
import jenkins.model.Jenkins.MasterComputer;
import hudson.remoting.Channel;
import hudson.remoting.VirtualChannel;
import hudson.remoting.Callable;
import hudson.util.StreamTaskListener;
import hudson.util.NullStream;
import hudson.util.RingBufferLogHandler;
import hudson.util.Futures;
import hudson.FilePath;
import hudson.Util;
import hudson.AbortException;
import hudson.remoting.Launcher;
import static hudson.slaves.SlaveComputer.LogHolder.SLAVE_LOG_HANDLER;
import hudson.slaves.OfflineCause.ChannelTermination;
import hudson.util.Secret;

import java.io.File;
import java.io.OutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.security.SecureRandom;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.Handler;
import java.util.List;
import java.util.Collections;
import java.util.ArrayList;
import java.nio.charset.Charset;
import java.util.concurrent.Future;
import java.security.Security;

import hudson.util.io.ReopenableFileOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.security.GeneralSecurityException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.RequestDispatcher;
import jenkins.model.Jenkins;
import jenkins.slaves.JnlpSlaveAgentProtocol;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpRedirect;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponseWrapper;
import org.kohsuke.stapler.ResponseImpl;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.compression.FilterServletOutputStream;

/**
 * {@link Computer} for {@link Slave}s.
 *
 * @author <NAME>
 */
public class SlaveComputer extends Computer {
    private volatile Channel channel;
    private volatile transient boolean acceptingTasks = true;
    private Charset defaultCharset;
    private Boolean isUnix;
    /**
     * Effective {@link ComputerLauncher} that hides the details of
     * how we launch a slave agent on this computer.
     *
     * <p>
     * This is normally the same as {@link Slave#getLauncher()} but
     * can be different. See {@link #grabLauncher(Node)}. 
     */
    private ComputerLauncher launcher;

    /**
     * Perpetually writable log file.
     */
    private final ReopenableFileOutputStream log;

    /**
     * {@link StreamTaskListener} that wraps {@link #log}, hence perpetually writable.
     */
    private final TaskListener taskListener;


    /**
     * Number of failed attempts to reconnect to this node
     * (so that if we keep failing to reconnect, we can stop
     * trying.)
     */
    private transient int numRetryAttempt;

    /**
     * Tracks the status of the last launch operation, which is always asynchronous.
     * This can be used to wait for the completion, or cancel the launch activity.
     */
    private volatile Future<?> lastConnectActivity = null;

    private Object constructed = new Object();

    public SlaveComputer(Slave slave) {
        super(slave);
        this.log = new ReopenableRotatingFileOutputStream(getLogFile(),10);
        this.taskListener = new StreamTaskListener(log);
        assert slave.getNumExecutors()!=0 : "Computer created with 0 executors";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isAcceptingTasks() {
        return acceptingTasks;
    }

    /**
     * @since 1.498
     */
    public String getJnlpMac() {
        return JnlpSlaveAgentProtocol.SLAVE_SECRET.mac(getName());
    }

    /**
     * Allows a {@linkplain hudson.slaves.ComputerLauncher} or a {@linkplain hudson.slaves.RetentionStrategy} to
     * suspend tasks being accepted by the slave computer.
     *
     * @param acceptingTasks {@code true} if the slave can accept tasks.
     */
    public void setAcceptingTasks(boolean acceptingTasks) {
        this.acceptingTasks = acceptingTasks;
    }

    /**
     * True if this computer is a Unix machine (as opposed to Windows machine).
     *
     * @return
     *      null if the computer is disconnected and therefore we don't know whether it is Unix or not.
     */
    public Boolean isUnix() {
        return isUnix;
    }

    @Override
    public Slave getNode() {
        return (Slave)super.getNode();
    }

    @Override
    public String getIcon() {
        Future<?> l = lastConnectActivity;
        if(l!=null && !l.isDone())
            return "computer-flash.gif";
        return super.getIcon();
    }

    /**
     * @deprecated since 2008-05-20.
     */
    @Deprecated @Override
    public boolean isJnlpAgent() {
        return launcher instanceof JNLPLauncher;
    }

    @Override
    public boolean isLaunchSupported() {
        return launcher.isLaunchSupported();
    }

    public ComputerLauncher getLauncher() {
        return launcher;
    }

    protected Future<?> _connect(boolean forceReconnect) {
        if(channel!=null)   return Futures.precomputed(null);
        if(!forceReconnect && isConnecting())
            return lastConnectActivity;
        if(forceReconnect && isConnecting())
            logger.fine("Forcing a reconnect on "+getName());

        closeChannel();
        return lastConnectActivity = Computer.threadPoolForRemoting.submit(new java.util.concurrent.Callable<Object>() {
            public Object call() throws Exception {
                // do this on another thread so that the lengthy launch operation
                // (which is typical) won't block UI thread.
                try {
                    log.rewind();
                    try {
                        for (ComputerListener cl : ComputerListener.all())
                            cl.preLaunch(SlaveComputer.this, taskListener);

                        launcher.launch(SlaveComputer.this, taskListener);
                    } catch (AbortException e) {
                        taskListener.error(e.getMessage());
                        throw e;
                    } catch (IOException e) {
                        Util.displayIOException(e,taskListener);
                        e.printStackTrace(taskListener.error(Messages.ComputerLauncher_unexpectedError()));
                        throw e;
                    } catch (InterruptedException e) {
                        e.printStackTrace(taskListener.error(Messages.ComputerLauncher_abortedLaunch()));
                        throw e;
                    }
                } finally {
                    if (channel==null) {
                        offlineCause = new OfflineCause.LaunchFailed();
                        for (ComputerListener cl : ComputerListener.all())
                            cl.onLaunchFailure(SlaveComputer.this, taskListener);
                    }
                }

                if (channel==null)
                    throw new IOException("Slave failed to connect, even though the launcher didn't report it. See the log output for details.");
                return null;
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void taskAccepted(Executor executor, Queue.Task task) {
        super.taskAccepted(executor, task);
        if (launcher instanceof ExecutorListener) {
            ((ExecutorListener)launcher).taskAccepted(executor, task);
        }
        
        //getNode() can return null at indeterminate times when nodes go offline
        Slave node = getNode();
        if (node != null && node.getRetentionStrategy() instanceof ExecutorListener) {
            ((ExecutorListener)node.getRetentionStrategy()).taskAccepted(executor, task);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void taskCompleted(Executor executor, Queue.Task task, long durationMS) {
        super.taskCompleted(executor, task, durationMS);
        if (launcher instanceof ExecutorListener) {
            ((ExecutorListener)launcher).taskCompleted(executor, task, durationMS);
        }
        RetentionStrategy r = getRetentionStrategy();
        if (r instanceof ExecutorListener) {
            ((ExecutorListener) r).taskCompleted(executor, task, durationMS);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void taskCompletedWithProblems(Executor executor, Queue.Task task, long durationMS, Throwable problems) {
        super.taskCompletedWithProblems(executor, task, durationMS, problems);
        if (launcher instanceof ExecutorListener) {
            ((ExecutorListener)launcher).taskCompletedWithProblems(executor, task, durationMS, problems);
        }
        RetentionStrategy r = getRetentionStrategy();
        if (r instanceof ExecutorListener) {
            ((ExecutorListener) r).taskCompletedWithProblems(executor, task, durationMS, problems);
        }
    }

    @Override
    public boolean isConnecting() {
        Future<?> l = lastConnectActivity;
        return isOffline() && l!=null && !l.isDone();
    }

    public OutputStream openLogFile() {
        try {
            log.rewind();
            return log;
        } catch (IOException e) {
            logger.log(Level.SEVERE, "Failed to create log file "+getLogFile(),e);
            return new NullStream();
        }
    }

    private final Object channelLock = new Object();

    public void setChannel(InputStream in, OutputStream out, TaskListener taskListener, Channel.Listener listener) throws IOException, InterruptedException {
        setChannel(in,out,taskListener.getLogger(),listener);
    }

    /**
     * Creates a {@link Channel} from the given stream and sets that to this slave.
     *
     * @param in
     *      Stream connected to the remote "slave.jar". It's the caller's responsibility to do
     *      buffering on this stream, if that's necessary.
     * @param out
     *      Stream connected to the remote peer. It's the caller's responsibility to do
     *      buffering on this stream, if that's necessary.
     * @param launchLog
     *      If non-null, receive the portion of data in <tt>is</tt> before
     *      the data goes into the "binary mode". This is useful
     *      when the established communication channel might include some data that might
     *      be useful for debugging/trouble-shooting.
     * @param listener
     *      Gets a notification when the channel closes, to perform clean up. Can be null.
     *      By the time this method is called, the cause of the termination is reported to the user,
     *      so the implementation of the listener doesn't need to do that again.
     */
    public void setChannel(InputStream in, OutputStream out, OutputStream launchLog, Channel.Listener listener) throws IOException, InterruptedException {
        Channel channel = new Channel(nodeName,threadPoolForRemoting, Channel.Mode.NEGOTIATE, in,out, launchLog);
        setChannel(channel,launchLog,listener);
    }

    /**
     * Shows {@link Channel#classLoadingCount}.
     * @since 1.495
     */
    public int getClassLoadingCount() throws IOException, InterruptedException {
        return channel.call(new LoadingCount(false));
    }

    /**
     * Shows {@link Channel#classLoadingPrefetchCacheCount}.
     * @return -1 in case that capability is not supported
     * @since 1.519
     */
    public int getClassLoadingPrefetchCacheCount() throws IOException, InterruptedException {
        if (!channel.remoteCapability.supportsPrefetch()) {
            return -1;
        }
        return channel.call(new LoadingPrefetchCacheCount());
    }

    /**
     * Shows {@link Channel#resourceLoadingCount}.
     * @since 1.495
     */
    public int getResourceLoadingCount() throws IOException, InterruptedException {
        return channel.call(new LoadingCount(true));
    }

    /**
     * Shows {@link Channel#classLoadingTime}.
     * @since 1.495
{'fraction_non_alphanumeric': 0.0884555497886854, 'fraction_numerical': 0.0032253280937198786, 'mean_word_length': 2.7196635410921126, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5825558', 'n_tokens_mistral': 7341, 'n_tokens_neox': 6946, 'n_words': 2131}