LCOV - code coverage report
Current view: top level - sshd - BaseCommand.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 164 199 82.4 %
Date: 2022-11-19 15:00:39 Functions: 35 44 79.5 %

          Line data    Source code
       1             : // Copyright (C) 2009 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.sshd;
      16             : 
      17             : import static java.nio.charset.StandardCharsets.UTF_8;
      18             : 
      19             : import com.google.common.base.Joiner;
      20             : import com.google.common.flogger.FluentLogger;
      21             : import com.google.common.util.concurrent.Atomics;
      22             : import com.google.gerrit.common.Nullable;
      23             : import com.google.gerrit.entities.Project;
      24             : import com.google.gerrit.extensions.annotations.PluginName;
      25             : import com.google.gerrit.extensions.registration.DynamicMap;
      26             : import com.google.gerrit.extensions.restapi.AuthException;
      27             : import com.google.gerrit.server.AccessPath;
      28             : import com.google.gerrit.server.CurrentUser;
      29             : import com.google.gerrit.server.DynamicOptions;
      30             : import com.google.gerrit.server.IdentifiedUser;
      31             : import com.google.gerrit.server.RequestCleanup;
      32             : import com.google.gerrit.server.git.ProjectRunnable;
      33             : import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
      34             : import com.google.gerrit.server.permissions.GlobalPermission;
      35             : import com.google.gerrit.server.permissions.PermissionBackend;
      36             : import com.google.gerrit.server.permissions.PermissionBackendException;
      37             : import com.google.gerrit.server.project.NoSuchChangeException;
      38             : import com.google.gerrit.server.project.NoSuchProjectException;
      39             : import com.google.gerrit.sshd.SshScope.Context;
      40             : import com.google.gerrit.util.cli.CmdLineParser;
      41             : import com.google.gerrit.util.cli.EndOfOptionsHandler;
      42             : import com.google.inject.Inject;
      43             : import com.google.inject.Injector;
      44             : import java.io.BufferedWriter;
      45             : import java.io.IOException;
      46             : import java.io.InputStream;
      47             : import java.io.InterruptedIOException;
      48             : import java.io.OutputStream;
      49             : import java.io.OutputStreamWriter;
      50             : import java.io.PrintWriter;
      51             : import java.io.StringWriter;
      52             : import java.nio.charset.Charset;
      53             : import java.util.Arrays;
      54             : import java.util.concurrent.Future;
      55             : import java.util.concurrent.ScheduledThreadPoolExecutor;
      56             : import java.util.concurrent.atomic.AtomicReference;
      57             : import org.apache.sshd.common.SshException;
      58             : import org.apache.sshd.server.Environment;
      59             : import org.apache.sshd.server.ExitCallback;
      60             : import org.apache.sshd.server.channel.ChannelSession;
      61             : import org.apache.sshd.server.command.Command;
      62             : import org.kohsuke.args4j.Argument;
      63             : import org.kohsuke.args4j.CmdLineException;
      64             : import org.kohsuke.args4j.Option;
      65             : 
      66             : public abstract class BaseCommand implements Command {
      67          18 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      68             : 
      69          18 :   public static final Charset ENC = UTF_8;
      70             : 
      71             :   private static final int PRIVATE_STATUS = 1 << 30;
      72             :   static final int STATUS_CANCEL = PRIVATE_STATUS | 1;
      73             :   static final int STATUS_NOT_FOUND = PRIVATE_STATUS | 2;
      74             :   public static final int STATUS_NOT_ADMIN = PRIVATE_STATUS | 3;
      75             : 
      76             :   @SuppressWarnings("unused") // unused here, but triggers logic in EndOfOptionsHandler
      77             :   @Option(name = "--", usage = "end of options", handler = EndOfOptionsHandler.class)
      78             :   private boolean endOfOptions;
      79             : 
      80             :   protected InputStream in;
      81             :   protected OutputStream out;
      82             :   protected OutputStream err;
      83             : 
      84             :   protected ExitCallback exit;
      85             : 
      86             :   @Inject protected CurrentUser user;
      87             : 
      88             :   @Inject private SshScope sshScope;
      89             : 
      90             :   @Inject private CmdLineParser.Factory cmdLineParserFactory;
      91             : 
      92             :   @Inject protected RequestCleanup cleanup;
      93             : 
      94             :   @Inject @CommandExecutor private ScheduledThreadPoolExecutor executor;
      95             : 
      96             :   @Inject private PermissionBackend permissionBackend;
      97             : 
      98             :   @Inject private SshScope.Context context;
      99             : 
     100             :   /** Commands declared by a plugin can be scoped by the plugin name. */
     101             :   @Inject(optional = true)
     102             :   @PluginName
     103             :   private String pluginName;
     104             : 
     105             :   @Inject protected Injector injector;
     106             : 
     107             :   @Inject protected DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
     108             : 
     109             :   /** The task, as scheduled on a worker thread. */
     110             :   private final AtomicReference<Future<?>> task;
     111             : 
     112             :   /** Text of the command line which lead up to invoking this instance. */
     113          10 :   private String commandName = "";
     114             : 
     115             :   /** Unparsed command line options. */
     116             :   private String[] argv;
     117             : 
     118             :   /** trimmed command line arguments. */
     119             :   private String[] trimmedArgv;
     120             : 
     121          10 :   public BaseCommand() {
     122          10 :     task = Atomics.newReference();
     123          10 :   }
     124             : 
     125             :   @Override
     126             :   public void setInputStream(InputStream in) {
     127           9 :     this.in = in;
     128           9 :   }
     129             : 
     130             :   @Override
     131             :   public void setOutputStream(OutputStream out) {
     132           9 :     this.out = out;
     133           9 :   }
     134             : 
     135             :   @Override
     136             :   public void setErrorStream(OutputStream err) {
     137           9 :     this.err = err;
     138           9 :   }
     139             : 
     140             :   @Override
     141             :   public void setExitCallback(ExitCallback callback) {
     142           9 :     this.exit = callback;
     143           9 :   }
     144             : 
     145             :   @Nullable
     146             :   protected String getPluginName() {
     147           9 :     return pluginName;
     148             :   }
     149             : 
     150             :   protected String getName() {
     151           9 :     return commandName;
     152             :   }
     153             : 
     154             :   void setName(String prefix) {
     155           9 :     this.commandName = prefix;
     156           9 :   }
     157             : 
     158             :   public String[] getArguments() {
     159           9 :     return argv;
     160             :   }
     161             : 
     162             :   public void setArguments(String[] argv) {
     163           9 :     this.argv = argv;
     164           9 :   }
     165             : 
     166             :   /**
     167             :    * Trim the argument if it is spanning multiple lines.
     168             :    *
     169             :    * @return the arguments where all the multiple-line fields are trimmed.
     170             :    */
     171             :   protected String[] getTrimmedArguments() {
     172           9 :     if (trimmedArgv == null && argv != null) {
     173           9 :       trimmedArgv = new String[argv.length];
     174           9 :       for (int i = 0; i < argv.length; i++) {
     175           8 :         String arg = argv[i];
     176           8 :         int indexOfMultiLine = arg.indexOf("\n");
     177           8 :         if (indexOfMultiLine > -1) {
     178           0 :           arg = arg.substring(0, indexOfMultiLine) + " [trimmed]";
     179             :         }
     180           8 :         trimmedArgv[i] = arg;
     181             :       }
     182             :     }
     183           9 :     return trimmedArgv;
     184             :   }
     185             : 
     186             :   @Override
     187             :   public void destroy(ChannelSession channel) {
     188           9 :     Future<?> future = task.getAndSet(null);
     189           9 :     if (future != null && !future.isDone()) {
     190           7 :       future.cancel(true);
     191             :     }
     192           9 :   }
     193             : 
     194             :   /**
     195             :    * Pass all state into the command, then run its start method.
     196             :    *
     197             :    * <p>This method copies all critical state, like the input and output streams, into the supplied
     198             :    * command. The caller must still invoke {@code cmd.start()} if wants to pass control to the
     199             :    * command.
     200             :    *
     201             :    * @param cmd the command that will receive the current state.
     202             :    */
     203             :   protected void provideStateTo(Command cmd) {
     204           9 :     cmd.setInputStream(in);
     205           9 :     cmd.setOutputStream(out);
     206           9 :     cmd.setErrorStream(err);
     207           9 :     cmd.setExitCallback(exit);
     208           9 :   }
     209             : 
     210             :   /**
     211             :    * Parses the command line argument, injecting parsed values into fields.
     212             :    *
     213             :    * <p>This method must be explicitly invoked to cause a parse.
     214             :    *
     215             :    * @param pluginOptions which helps to define and parse options provided from plugins
     216             :    * @throws UnloggedFailure if the command line arguments were invalid.
     217             :    * @see Option
     218             :    * @see Argument
     219             :    */
     220             :   protected void parseCommandLine(DynamicOptions pluginOptions) throws UnloggedFailure {
     221           9 :     parseCommandLine(this, pluginOptions);
     222           9 :   }
     223             : 
     224             :   /**
     225             :    * Parses the command line argument, injecting parsed values into fields.
     226             :    *
     227             :    * <p>This method must be explicitly invoked to cause a parse.
     228             :    *
     229             :    * @param options object whose fields declare Option and Argument annotations to describe the
     230             :    *     parameters of the command. Usually {@code this}.
     231             :    * @param pluginOptions which helps to define and parse options provided from plugins
     232             :    * @throws UnloggedFailure if the command line arguments were invalid.
     233             :    * @see Option
     234             :    * @see Argument
     235             :    */
     236             :   protected void parseCommandLine(Object options, DynamicOptions pluginOptions)
     237             :       throws UnloggedFailure {
     238           9 :     final CmdLineParser clp = newCmdLineParser(options);
     239           9 :     pluginOptions.setBean(options);
     240           9 :     pluginOptions.startLifecycleListeners();
     241           9 :     pluginOptions.parseDynamicBeans(clp);
     242           9 :     pluginOptions.setDynamicBeans();
     243           9 :     pluginOptions.onBeanParseStart();
     244             :     try {
     245           9 :       clp.parseArgument(argv);
     246           1 :     } catch (IllegalArgumentException | CmdLineException err) {
     247           1 :       if (!clp.wasHelpRequestedByOption()) {
     248           1 :         throw new UnloggedFailure(1, "fatal: " + err.getMessage());
     249             :       }
     250           9 :     }
     251             : 
     252           9 :     if (clp.wasHelpRequestedByOption()) {
     253           1 :       StringWriter msg = new StringWriter();
     254           1 :       clp.printDetailedUsage(commandName, msg);
     255           1 :       msg.write(usage());
     256           1 :       throw new UnloggedFailure(1, msg.toString());
     257             :     }
     258           9 :     pluginOptions.onBeanParseEnd();
     259           9 :   }
     260             : 
     261             :   protected String usage() {
     262           1 :     return "";
     263             :   }
     264             : 
     265             :   /** Construct a new parser for this command's received command line. */
     266             :   protected CmdLineParser newCmdLineParser(Object options) {
     267           9 :     return cmdLineParserFactory.create(options);
     268             :   }
     269             : 
     270             :   /**
     271             :    * Spawn a function into its own thread.
     272             :    *
     273             :    * <p>Typically this should be invoked within {@link Command#start(ChannelSession, Environment)},
     274             :    * such as:
     275             :    *
     276             :    * <pre>
     277             :    * startThread(new CommandRunnable() {
     278             :    *   public void run() throws Exception {
     279             :    *     runImp();
     280             :    *   }
     281             :    * },
     282             :    * accessPath);
     283             :    * </pre>
     284             :    *
     285             :    * <p>If the function throws an exception, it is translated to a simple message for the client, a
     286             :    * non-zero exit code, and the stack trace is logged.
     287             :    *
     288             :    * @param thunk the runnable to execute on the thread, performing the command's logic.
     289             :    * @param accessPath the path used by the end user for running the SSH command
     290             :    */
     291             :   protected void startThread(final CommandRunnable thunk, AccessPath accessPath) {
     292           9 :     final TaskThunk tt = new TaskThunk(thunk, accessPath);
     293             : 
     294           9 :     if (isAdminHighPriorityCommand()) {
     295             :       // Admin commands should not block the main work threads (there
     296             :       // might be an interactive shell there), nor should they wait
     297             :       // for the main work threads.
     298             :       //
     299           1 :       new Thread(tt, tt.toString()).start();
     300             :     } else {
     301           9 :       task.set(executor.submit(tt));
     302             :     }
     303           9 :   }
     304             : 
     305             :   private boolean isAdminHighPriorityCommand() {
     306           9 :     if (getClass().getAnnotation(AdminHighPriorityCommand.class) != null) {
     307             :       try {
     308           1 :         permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
     309           1 :         return true;
     310           0 :       } catch (AuthException | PermissionBackendException e) {
     311           0 :         return false;
     312             :       }
     313             :     }
     314           9 :     return false;
     315             :   }
     316             : 
     317             :   /**
     318             :    * Terminate this command and return a result code to the remote client.
     319             :    *
     320             :    * <p>Commands should invoke this at most once. Once invoked, the command may lose access to
     321             :    * request based resources as any callbacks previously registered with {@link RequestCleanup} will
     322             :    * fire.
     323             :    *
     324             :    * @param rc exit code for the remote client.
     325             :    */
     326             :   protected void onExit(int rc) {
     327           8 :     exit.onExit(rc);
     328           8 :     if (cleanup != null) {
     329           8 :       cleanup.run();
     330             :     }
     331           8 :   }
     332             : 
     333             :   /** Wrap the supplied output stream in a UTF-8 encoded PrintWriter. */
     334             :   protected static PrintWriter toPrintWriter(OutputStream o) {
     335           5 :     return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, ENC)));
     336             :   }
     337             : 
     338             :   private int handleError(Throwable e) {
     339           3 :     if ((e.getClass() == IOException.class && "Pipe closed".equals(e.getMessage()))
     340             :         || //
     341           3 :         (e.getClass() == SshException.class && "Already closed".equals(e.getMessage()))
     342             :         || //
     343           3 :         e.getClass() == InterruptedIOException.class) {
     344             :       // This is sshd telling us the client just dropped off while
     345             :       // we were waiting for a read or a write to complete. Either
     346             :       // way its not really a fatal error. Don't log it.
     347             :       //
     348           0 :       return 127;
     349             :     }
     350             : 
     351           3 :     if (!(e instanceof UnloggedFailure)) {
     352           3 :       final StringBuilder m = new StringBuilder();
     353           3 :       m.append("Internal server error");
     354           3 :       if (user.isIdentifiedUser()) {
     355           3 :         final IdentifiedUser u = user.asIdentifiedUser();
     356           3 :         m.append(" (user ");
     357           3 :         m.append(u.getUserName().orElse(null));
     358           3 :         m.append(" account ");
     359           3 :         m.append(u.getAccountId());
     360           3 :         m.append(")");
     361             :       }
     362           3 :       m.append(" during ");
     363           3 :       m.append(context.getCommandLine());
     364           3 :       logCauseIfRelevant(e, m);
     365             :     }
     366             : 
     367           3 :     if (e instanceof Failure) {
     368           2 :       final Failure f = (Failure) e;
     369             :       try {
     370           2 :         err.write((f.getMessage() + "\n").getBytes(ENC));
     371           2 :         err.flush();
     372           0 :       } catch (IOException e2) {
     373             :         // Ignored
     374           0 :       } catch (RuntimeException e2) {
     375           0 :         logger.atWarning().withCause(e2).log("Cannot send failure message to client");
     376           2 :       }
     377           2 :       return f.exitCode;
     378             :     }
     379             : 
     380             :     try {
     381           0 :       err.write("fatal: internal server error\n".getBytes(ENC));
     382           0 :       err.flush();
     383           2 :     } catch (IOException e2) {
     384             :       // Ignored
     385           0 :     } catch (RuntimeException e2) {
     386           0 :       logger.atWarning().withCause(e2).log("Cannot send internal server error message to client");
     387           2 :     }
     388           2 :     return 128;
     389             :   }
     390             : 
     391             :   private void logCauseIfRelevant(Throwable e, StringBuilder message) {
     392           3 :     String zeroLength = "length=0";
     393           3 :     String streamAlreadyClosed = "stream is already closed";
     394           3 :     boolean isZeroLength = false;
     395             : 
     396           3 :     if (streamAlreadyClosed.equals(e.getMessage())) {
     397           0 :       StackTraceElement[] stackTrace = e.getStackTrace();
     398           0 :       isZeroLength = Arrays.stream(stackTrace).anyMatch(s -> s.toString().contains(zeroLength));
     399             :     }
     400           3 :     if (!isZeroLength) {
     401           3 :       logger.atSevere().withCause(e).log("%s", message);
     402             :     }
     403           3 :   }
     404             : 
     405             :   protected UnloggedFailure die(String msg) {
     406           1 :     return new UnloggedFailure(1, "fatal: " + msg);
     407             :   }
     408             : 
     409             :   protected UnloggedFailure die(String msg, Throwable why) {
     410           0 :     return new UnloggedFailure(1, "fatal: " + msg, why);
     411             :   }
     412             : 
     413             :   protected UnloggedFailure die(Throwable why) {
     414           0 :     return new UnloggedFailure(1, "fatal: " + why.getMessage(), why);
     415             :   }
     416             : 
     417             :   protected void writeError(String type, String msg) {
     418             :     try {
     419           0 :       err.write((type + ": " + msg + "\n").getBytes(ENC));
     420           0 :     } catch (IOException e) {
     421             :       // Ignored
     422           0 :     }
     423           0 :   }
     424             : 
     425             :   protected void enableGracefulStop() {
     426           9 :     context.getSession().setGracefulStop(true);
     427           9 :   }
     428             : 
     429             :   protected String getTaskDescription() {
     430           9 :     String[] ta = getTrimmedArguments();
     431           9 :     if (ta != null) {
     432           9 :       return commandName + " " + Joiner.on(" ").join(ta);
     433             :     }
     434           0 :     return commandName;
     435             :   }
     436             : 
     437             :   private String getTaskName() {
     438           9 :     StringBuilder m = new StringBuilder();
     439           9 :     m.append(getTaskDescription());
     440           9 :     if (user.isIdentifiedUser()) {
     441           9 :       IdentifiedUser u = user.asIdentifiedUser();
     442           9 :       if (u.getUserName().isPresent()) {
     443           9 :         m.append(" (").append(u.getUserName().get()).append(")");
     444             :       }
     445             :     }
     446           9 :     return m.toString();
     447             :   }
     448             : 
     449             :   private final class TaskThunk implements CancelableRunnable, ProjectRunnable {
     450             :     private final CommandRunnable thunk;
     451             :     private final String taskName;
     452             :     private final AccessPath accessPath;
     453             : 
     454             :     private Project.NameKey projectName;
     455             : 
     456           9 :     private TaskThunk(final CommandRunnable thunk, AccessPath accessPath) {
     457           9 :       this.thunk = thunk;
     458           9 :       this.taskName = getTaskName();
     459           9 :       this.accessPath = accessPath;
     460           9 :     }
     461             : 
     462             :     @Override
     463             :     public void cancel() {
     464           0 :       synchronized (this) {
     465           0 :         final Context old = sshScope.set(context);
     466             :         try {
     467           0 :           onExit(STATUS_CANCEL);
     468             :         } finally {
     469           0 :           sshScope.set(old);
     470             :         }
     471           0 :       }
     472           0 :     }
     473             : 
     474             :     @Override
     475             :     public void run() {
     476           9 :       synchronized (this) {
     477           9 :         final Thread thisThread = Thread.currentThread();
     478           9 :         final String thisName = thisThread.getName();
     479           9 :         int rc = 0;
     480           9 :         context.getSession().setAccessPath(accessPath);
     481           9 :         final Context old = sshScope.set(context);
     482             :         try {
     483           9 :           context.start();
     484           9 :           thisThread.setName("SSH " + taskName);
     485             : 
     486             :           try {
     487           9 :             if (thunk instanceof ProjectCommandRunnable) {
     488           5 :               try (DynamicOptions pluginOptions = new DynamicOptions(injector, dynamicBeans)) {
     489           4 :                 ((ProjectCommandRunnable) thunk).executeParseCommand(pluginOptions);
     490           4 :                 projectName = ((ProjectCommandRunnable) thunk).getProjectName();
     491           4 :                 thunk.run();
     492             :               }
     493             :             } else {
     494           5 :               thunk.run();
     495             :             }
     496           0 :           } catch (NoSuchProjectException e) {
     497           0 :             throw new UnloggedFailure(1, e.getMessage());
     498           0 :           } catch (NoSuchChangeException e) {
     499           0 :             throw new UnloggedFailure(1, e.getMessage() + " no such change");
     500           9 :           }
     501             : 
     502           9 :           out.flush();
     503           9 :           err.flush();
     504           3 :         } catch (Exception e) {
     505             :           try {
     506           2 :             out.flush();
     507           2 :           } catch (Exception e2) {
     508             :             // Ignored
     509           2 :           }
     510             :           try {
     511           2 :             err.flush();
     512           2 :           } catch (Exception e2) {
     513             :             // Ignored
     514           2 :           }
     515           3 :           rc = handleError(e);
     516             :         } finally {
     517             :           try {
     518           9 :             onExit(rc);
     519             :           } finally {
     520           9 :             sshScope.set(old);
     521           9 :             thisThread.setName(thisName);
     522             :           }
     523             :         }
     524           9 :       }
     525           9 :     }
     526             : 
     527             :     @Override
     528             :     public String toString() {
     529           1 :       return taskName;
     530             :     }
     531             : 
     532             :     @Override
     533             :     public Project.NameKey getProjectNameKey() {
     534           0 :       return projectName;
     535             :     }
     536             : 
     537             :     @Override
     538             :     public String getRemoteName() {
     539           0 :       return null;
     540             :     }
     541             : 
     542             :     @Override
     543             :     public boolean hasCustomizedPrint() {
     544           0 :       return false;
     545             :     }
     546             :   }
     547             : 
     548             :   /** Runnable function which can throw an exception. */
     549             :   @FunctionalInterface
     550             :   public interface CommandRunnable {
     551             :     void run() throws Exception;
     552             :   }
     553             : 
     554             :   /** Runnable function which can retrieve a project name related to the task */
     555             :   public interface ProjectCommandRunnable extends CommandRunnable {
     556             :     // execute parser command before running, in order to be able to retrieve
     557             :     // project name
     558             :     void executeParseCommand(DynamicOptions pluginOptions) throws Exception;
     559             : 
     560             :     Project.NameKey getProjectName();
     561             :   }
     562             : 
     563             :   /** Thrown from {@link CommandRunnable#run()} with client message and code. */
     564             :   public static class Failure extends Exception {
     565             :     private static final long serialVersionUID = 1L;
     566             : 
     567             :     final int exitCode;
     568             : 
     569             :     /**
     570             :      * Create a new failure.
     571             :      *
     572             :      * @param exitCode exit code to return the client, which indicates the failure status of this
     573             :      *     command. Should be between 1 and 255, inclusive.
     574             :      * @param msg message to also send to the client's stderr.
     575             :      */
     576             :     public Failure(int exitCode, String msg) {
     577           1 :       this(exitCode, msg, null);
     578           1 :     }
     579             : 
     580             :     /**
     581             :      * Create a new failure.
     582             :      *
     583             :      * @param exitCode exit code to return the client, which indicates the failure status of this
     584             :      *     command. Should be between 1 and 255, inclusive.
     585             :      * @param msg message to also send to the client's stderr.
     586             :      * @param why stack trace to include in the server's log, but is not sent to the client's
     587             :      *     stderr.
     588             :      */
     589             :     public Failure(int exitCode, String msg, Throwable why) {
     590           2 :       super(msg, why);
     591           2 :       this.exitCode = exitCode;
     592           2 :     }
     593             :   }
     594             : 
     595             :   /** Thrown from {@link CommandRunnable#run()} with client message and code. */
     596             :   public static class UnloggedFailure extends Failure {
     597             :     private static final long serialVersionUID = 1L;
     598             : 
     599             :     /**
     600             :      * Create a new failure.
     601             :      *
     602             :      * @param msg message to also send to the client's stderr.
     603             :      */
     604             :     public UnloggedFailure(String msg) {
     605           0 :       this(1, msg);
     606           0 :     }
     607             : 
     608             :     /**
     609             :      * Create a new failure.
     610             :      *
     611             :      * @param exitCode exit code to return the client, which indicates the failure status of this
     612             :      *     command. Should be between 1 and 255, inclusive.
     613             :      * @param msg message to also send to the client's stderr.
     614             :      */
     615             :     public UnloggedFailure(int exitCode, String msg) {
     616           1 :       this(exitCode, msg, null);
     617           1 :     }
     618             : 
     619             :     /**
     620             :      * Create a new failure.
     621             :      *
     622             :      * @param exitCode exit code to return the client, which indicates the failure status of this
     623             :      *     command. Should be between 1 and 255, inclusive.
     624             :      * @param msg message to also send to the client's stderr.
     625             :      * @param why stack trace to include in the server's log, but is not sent to the client's
     626             :      *     stderr.
     627             :      */
     628             :     public UnloggedFailure(int exitCode, String msg, Throwable why) {
     629           1 :       super(exitCode, msg, why);
     630           1 :     }
     631             :   }
     632             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750