Merge "Merge branch 'stable-3.2' into master"
diff --git a/.bazelversion b/.bazelversion
index 1545d96..d5c0c99 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-3.5.0
+3.5.1
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 78a621c..c35c9c2 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -388,6 +388,15 @@
     initSsh();
   }
 
+  protected void restart() throws Exception {
+    server = GerritServer.restart(server, createModule(), createSshModule());
+    server.getTestInjector().injectMembers(this);
+    if (resetter != null) {
+      server.getTestInjector().injectMembers(resetter);
+    }
+    initSsh();
+  }
+
   protected void reindexAccount(Account.Id accountId) {
     accountIndexer.index(accountId);
   }
@@ -430,15 +439,18 @@
     baseConfig.setInt("receive", null, "changeUpdateThreads", 4);
     Module module = createModule();
     Module auditModule = createAuditModule();
+    Module sshModule = createSshModule();
     if (classDesc.equals(methodDesc) && !classDesc.sandboxed() && !methodDesc.sandboxed()) {
       if (commonServer == null) {
         commonServer =
-            GerritServer.initAndStart(temporaryFolder, classDesc, baseConfig, module, auditModule);
+            GerritServer.initAndStart(
+                temporaryFolder, classDesc, baseConfig, module, auditModule, sshModule);
       }
       server = commonServer;
     } else {
       server =
-          GerritServer.initAndStart(temporaryFolder, methodDesc, baseConfig, module, auditModule);
+          GerritServer.initAndStart(
+              temporaryFolder, methodDesc, baseConfig, module, auditModule, sshModule);
     }
 
     server.getTestInjector().injectMembers(this);
@@ -536,6 +548,11 @@
     return null;
   }
 
+  /** Override to bind an additional Guice module for SSH injector */
+  public Module createSshModule() {
+    return null;
+  }
+
   protected void initSsh() throws Exception {
     if (testRequiresSsh
         && SshMode.useSsh()
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 5942c0f..0025396 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -323,6 +323,7 @@
    * @param desc server description.
    * @param baseConfig default config values; merged with config from {@code desc}.
    * @param testSysModule additional Guice module to use.
+   * @param testSshModule additional Guice module to use.
    * @return started server.
    * @throws Exception
    */
@@ -331,14 +332,15 @@
       Description desc,
       Config baseConfig,
       @Nullable Module testSysModule,
-      @Nullable Module testAuditModule)
+      @Nullable Module testAuditModule,
+      @Nullable Module testSshModule)
       throws Exception {
     Path site = temporaryFolder.newFolder().toPath();
     try {
       if (!desc.memory()) {
         init(desc, baseConfig, site);
       }
-      return start(desc, baseConfig, site, testSysModule, testAuditModule, null);
+      return start(desc, baseConfig, site, testSysModule, testAuditModule, testSshModule, null);
     } catch (Exception e) {
       throw e;
     }
@@ -354,6 +356,7 @@
    *     initialize this directory. Can be retrieved from the returned instance via {@link
    *     #getSitePath()}.
    * @param testSysModule optional additional module to add to the system injector.
+   * @param testSshModule optional additional module to add to the ssh injector.
    * @param inMemoryRepoManager {@link InMemoryRepositoryManager} that should be used if the site is
    *     started in memory
    * @param additionalArgs additional command-line arguments for the daemon program; only allowed if
@@ -367,6 +370,7 @@
       Path site,
       @Nullable Module testSysModule,
       @Nullable Module testAuditModule,
+      @Nullable Module testSshModule,
       @Nullable InMemoryRepositoryManager inMemoryRepoManager,
       String... additionalArgs)
       throws Exception {
@@ -390,6 +394,9 @@
     if (testSysModule != null) {
       daemon.addAdditionalSysModuleForTesting(testSysModule);
     }
+    if (testSshModule != null) {
+      daemon.addAdditionalSshModuleForTesting(testSshModule);
+    }
     daemon.setEnableSshd(desc.useSsh());
 
     if (desc.memory()) {
@@ -614,7 +621,24 @@
 
     server.close();
     server.daemon.stop();
-    return start(server.desc, cfg, site, null, null, inMemoryRepoManager);
+    return start(server.desc, cfg, site, null, null, null, inMemoryRepoManager);
+  }
+
+  public static GerritServer restart(
+      GerritServer server, @Nullable Module testSysModule, @Nullable Module testSshModule)
+      throws Exception {
+    checkState(server.desc.sandboxed(), "restarting as slave requires @Sandboxed");
+    Config cfg = server.testInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
+    Path site = server.testInjector.getInstance(Key.get(Path.class, SitePath.class));
+
+    InMemoryRepositoryManager inMemoryRepoManager = null;
+    if (hasBinding(server.testInjector, InMemoryRepositoryManager.class)) {
+      inMemoryRepoManager = server.testInjector.getInstance(InMemoryRepositoryManager.class);
+    }
+
+    server.close();
+    server.daemon.stop();
+    return start(server.desc, cfg, site, testSysModule, null, testSshModule, inMemoryRepoManager);
   }
 
   private static boolean hasBinding(Injector injector, Class<?> clazz) {
diff --git a/java/com/google/gerrit/acceptance/SshSession.java b/java/com/google/gerrit/acceptance/SshSession.java
index 6ecf85f..6698657 100644
--- a/java/com/google/gerrit/acceptance/SshSession.java
+++ b/java/com/google/gerrit/acceptance/SshSession.java
@@ -65,6 +65,22 @@
     }
   }
 
+  @SuppressWarnings("resource")
+  public int execAndReturnStatus(String command) throws Exception {
+    ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
+    try {
+      channel.setCommand(command);
+      InputStream err = channel.getErrStream();
+      channel.connect();
+
+      Scanner s = new Scanner(err, UTF_8.name()).useDelimiter("\\A");
+      error = s.hasNext() ? s.next() : null;
+      return channel.getExitStatus();
+    } finally {
+      channel.disconnect();
+    }
+  }
+
   private boolean hasError() {
     return error != null;
   }
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index 43fe4eb..dcb49a5 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -187,7 +187,14 @@
   private GerritServer startImpl(@Nullable Module testSysModule, String... additionalArgs)
       throws Exception {
     return GerritServer.start(
-        serverDesc, baseConfig, sitePaths.site_path, testSysModule, null, null, additionalArgs);
+        serverDesc,
+        baseConfig,
+        sitePaths.site_path,
+        testSysModule,
+        null,
+        null,
+        null,
+        additionalArgs);
   }
 
   protected static void runGerrit(String... args) throws Exception {
diff --git a/java/com/google/gerrit/acceptance/ssh/GracefulCommand.java b/java/com/google/gerrit/acceptance/ssh/GracefulCommand.java
new file mode 100644
index 0000000..ddaf341
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/ssh/GracefulCommand.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.ssh;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.google.gerrit.sshd.CommandMetaData;
+
+@CommandMetaData(
+    name = "graceful",
+    description = "Test command for graceful shutdown",
+    runsAt = MASTER_OR_SLAVE)
+public class GracefulCommand extends TestCommand {
+
+  @Override
+  boolean isGraceful() {
+    return true;
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/ssh/NonGracefulCommand.java b/java/com/google/gerrit/acceptance/ssh/NonGracefulCommand.java
new file mode 100644
index 0000000..ed635c8
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/ssh/NonGracefulCommand.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.ssh;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.google.gerrit.sshd.CommandMetaData;
+
+@CommandMetaData(
+    name = "non-graceful",
+    description = "Test command for immediate shutdown",
+    runsAt = MASTER_OR_SLAVE)
+public class NonGracefulCommand extends TestCommand {
+
+  @Override
+  boolean isGraceful() {
+    return false;
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/ssh/TestCommand.java b/java/com/google/gerrit/acceptance/ssh/TestCommand.java
new file mode 100644
index 0000000..7839578
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/ssh/TestCommand.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.ssh;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.sshd.SshCommand;
+import java.util.concurrent.CyclicBarrier;
+import org.kohsuke.args4j.Option;
+
+public abstract class TestCommand extends SshCommand {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  public static final CyclicBarrier syncPoint = new CyclicBarrier(2);
+
+  @Option(
+      name = "--duration",
+      aliases = {"-d"},
+      required = true,
+      usage = "Duration of the command execution in seconds")
+  private int duration;
+
+  @Override
+  protected void run() throws UnloggedFailure, Failure, Exception {
+    logger.atFine().log("Starting command.");
+    if (isGraceful()) {
+      enableGracefulStop();
+    }
+    try {
+      syncPoint.await();
+      Thread.sleep(duration * 1000);
+      logger.atFine().log("Stopping command.");
+    } catch (Exception e) {
+      throw die("Command ended prematurely.", e);
+    }
+  }
+
+  abstract boolean isGraceful();
+}
diff --git a/java/com/google/gerrit/acceptance/ssh/TestSshCommandModule.java b/java/com/google/gerrit/acceptance/ssh/TestSshCommandModule.java
new file mode 100644
index 0000000..626092b
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/ssh/TestSshCommandModule.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.ssh;
+
+import com.google.gerrit.sshd.CommandModule;
+
+public class TestSshCommandModule extends CommandModule {
+  @Override
+  protected void configure() {
+    command("graceful").to(GracefulCommand.class);
+    command("non-graceful").to(NonGracefulCommand.class);
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index e4d594b..0138290 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -90,6 +90,7 @@
 
     CreateProjectArgs args = new CreateProjectArgs();
     args.setProjectName(name);
+    args.permissionsOnly = projectCreation.permissionOnly().orElse(false);
     args.branch =
         projectCreation.branches().stream().map(RefNames::fullName).collect(toImmutableList());
     args.createEmptyCommit = projectCreation.createEmptyCommit().orElse(true);
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
index 2649dea..00759a0 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
@@ -35,6 +35,8 @@
 
   public abstract Optional<Boolean> createEmptyCommit();
 
+  public abstract Optional<Boolean> permissionOnly();
+
   public abstract Optional<SubmitType> submitType();
 
   abstract ThrowingFunction<TestProjectCreation, Project.NameKey> projectCreator();
@@ -67,6 +69,8 @@
 
     public abstract TestProjectCreation.Builder createEmptyCommit(boolean value);
 
+    public abstract TestProjectCreation.Builder permissionOnly(boolean value);
+
     /** Skips the empty commit on creation. This means that project's branches will not exist. */
     public TestProjectCreation.Builder noEmptyCommit() {
       return createEmptyCommit(false);
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index ba037d9..2eb19aa 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -206,6 +206,7 @@
   private AbstractModule luceneModule;
   private Module emailModule;
   private List<Module> testSysModules = new ArrayList<>();
+  private List<Module> testSshModules = new ArrayList<>();
   private Module auditEventModule;
 
   private Runnable serverStarted;
@@ -337,6 +338,11 @@
   }
 
   @VisibleForTesting
+  public void addAdditionalSshModuleForTesting(@Nullable Module... modules) {
+    testSshModules.addAll(Arrays.asList(modules));
+  }
+
+  @VisibleForTesting
   public void start() throws IOException {
     if (dbInjector == null) {
       dbInjector = createDbInjector(true /* enableMetrics */);
@@ -532,6 +538,8 @@
             replica,
             sysInjector.getInstance(DownloadConfig.class),
             sysInjector.getInstance(LfsPluginAuthCommand.Module.class)));
+
+    modules.addAll(testSshModules);
     if (!replica) {
       modules.add(new IndexCommandsModule(sysInjector));
       modules.add(new SequenceCommandsModule());
diff --git a/java/com/google/gerrit/sshd/AbstractGitCommand.java b/java/com/google/gerrit/sshd/AbstractGitCommand.java
index 8bf6cd5..9efcff2 100644
--- a/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -52,6 +52,7 @@
 
   @Override
   public void start(ChannelSession channel, Environment env) {
+    enableGracefulStop();
     String gitProtocol = env.getEnv().get(GIT_PROTOCOL);
     if (gitProtocol != null) {
       extraParameters = gitProtocol.split(":");
diff --git a/java/com/google/gerrit/sshd/BaseCommand.java b/java/com/google/gerrit/sshd/BaseCommand.java
index ab1f062..a027dd1 100644
--- a/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/java/com/google/gerrit/sshd/BaseCommand.java
@@ -403,6 +403,10 @@
     }
   }
 
+  protected void enableGracefulStop() {
+    context.getSession().setGracefulStop(true);
+  }
+
   protected String getTaskDescription() {
     String[] ta = getTrimmedArguments();
     if (ta != null) {
diff --git a/java/com/google/gerrit/sshd/SshDaemon.java b/java/com/google/gerrit/sshd/SshDaemon.java
index c43bf91..c14ebd8 100644
--- a/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/java/com/google/gerrit/sshd/SshDaemon.java
@@ -90,6 +90,7 @@
 import org.apache.sshd.common.random.Random;
 import org.apache.sshd.common.random.SingletonRandomFactory;
 import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.helpers.AbstractSession;
 import org.apache.sshd.common.session.helpers.DefaultUnknownChannelReferenceHandler;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
@@ -369,14 +370,24 @@
     Collection<IoSession> ioSessions = daemonAcceptor.getManagedSessions().values();
     CountDownLatch allSessionsClosed = new CountDownLatch(ioSessions.size());
     for (IoSession io : ioSessions) {
-      logger.atFine().log("Waiting for session %s to stop.", io.getId());
-      io.addCloseFutureListener(
-          new SshFutureListener<CloseFuture>() {
-            @Override
-            public void operationComplete(CloseFuture future) {
-              allSessionsClosed.countDown();
-            }
-          });
+      AbstractSession serverSession = AbstractSession.getSession(io, true);
+      SshSession sshSession =
+          serverSession != null ? serverSession.getAttribute(SshSession.KEY) : null;
+      if (sshSession != null && sshSession.requiresGracefulStop()) {
+        logger.atFine().log("Waiting for session %s to stop.", io.getId());
+        io.addCloseFutureListener(
+            new SshFutureListener<CloseFuture>() {
+              @Override
+              public void operationComplete(CloseFuture future) {
+                logger.atFine().log("Session %s was stopped.", io.getId());
+                allSessionsClosed.countDown();
+              }
+            });
+      } else {
+        logger.atFine().log("Stopping session %s immediately.", io.getId());
+        io.close(true);
+        allSessionsClosed.countDown();
+      }
     }
     try {
       if (!allSessionsClosed.await(gracefulStopTimeout, TimeUnit.SECONDS)) {
diff --git a/java/com/google/gerrit/sshd/SshSession.java b/java/com/google/gerrit/sshd/SshSession.java
index d6ecc73..b39eaed 100644
--- a/java/com/google/gerrit/sshd/SshSession.java
+++ b/java/com/google/gerrit/sshd/SshSession.java
@@ -35,6 +35,8 @@
   private volatile String authError;
   private volatile String peerAgent;
 
+  private volatile boolean gracefulStop = false;
+
   SshSession(int sessionId, SocketAddress peer) {
     this.sessionId = sessionId;
     this.remoteAddress = peer;
@@ -58,6 +60,14 @@
     return sessionId;
   }
 
+  public boolean requiresGracefulStop() {
+    return gracefulStop;
+  }
+
+  public void setGracefulStop(boolean gracefulStop) {
+    this.gracefulStop = gracefulStop;
+  }
+
   /** Identity of the authenticated user account on the socket. */
   public CurrentUser getUser() {
     return identity;
diff --git a/java/com/google/gerrit/sshd/commands/AproposCommand.java b/java/com/google/gerrit/sshd/commands/AproposCommand.java
index d3db70d..e7a88a1 100644
--- a/java/com/google/gerrit/sshd/commands/AproposCommand.java
+++ b/java/com/google/gerrit/sshd/commands/AproposCommand.java
@@ -39,6 +39,7 @@
 
   @Override
   public void run() throws Exception {
+    enableGracefulStop();
     try {
       List<QueryDocumentationExecutor.DocResult> res = searcher.doQuery(q);
       for (DocResult docResult : res) {
diff --git a/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index ee6f635..134fb03 100644
--- a/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -63,6 +63,7 @@
 
   @Override
   protected void run() throws Failure {
+    enableGracefulStop();
     try {
       BanCommitInput input =
           BanCommitInput.fromCommits(Lists.transform(commitsToBan, ObjectId::getName));
diff --git a/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java b/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
index d70c153..ad8e20d 100644
--- a/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
+++ b/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
@@ -59,6 +59,7 @@
 
   @Override
   protected final void run() throws UnloggedFailure {
+    enableGracefulStop();
     try {
       RevisionResource revision =
           revisions.parse(
diff --git a/java/com/google/gerrit/sshd/commands/CloseConnection.java b/java/com/google/gerrit/sshd/commands/CloseConnection.java
index 093f647..e0b87f8 100644
--- a/java/com/google/gerrit/sshd/commands/CloseConnection.java
+++ b/java/com/google/gerrit/sshd/commands/CloseConnection.java
@@ -57,6 +57,7 @@
 
   @Override
   protected void run() throws Failure {
+    enableGracefulStop();
     SshUtil.forEachSshSession(
         sshDaemon,
         (k, sshSession, abstractSession, ioSession) -> {
diff --git a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index 004a0ba..4da55e2 100644
--- a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -72,6 +72,7 @@
   @Override
   protected void run()
       throws IOException, ConfigInvalidException, UnloggedFailure, PermissionBackendException {
+    enableGracefulStop();
     AccountInput input = new AccountInput();
     input.username = username;
     input.email = email;
diff --git a/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java b/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
index aad96a1..a837ecd 100644
--- a/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
@@ -44,6 +44,7 @@
 
   @Override
   protected void run() throws UnloggedFailure {
+    enableGracefulStop();
     try {
       BranchInput in = new BranchInput();
       in.revision = revision;
diff --git a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 17f80c0..5fd2297 100644
--- a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -102,6 +102,7 @@
   @Override
   protected void run()
       throws Failure, IOException, ConfigInvalidException, PermissionBackendException {
+    enableGracefulStop();
     try {
       GroupResource rsrc = createGroup();
 
diff --git a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index fca7427..f2ab4e8 100644
--- a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -166,6 +166,7 @@
 
   @Override
   protected void run() throws Failure {
+    enableGracefulStop();
     try {
       if (!suggestParent) {
         if (projectName == null) {
diff --git a/java/com/google/gerrit/sshd/commands/FlushCaches.java b/java/com/google/gerrit/sshd/commands/FlushCaches.java
index 2afc009..fe2a897 100644
--- a/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -55,6 +55,7 @@
 
   @Override
   protected void run() throws Failure {
+    enableGracefulStop();
     try {
       if (list) {
         if (all || !caches.isEmpty()) {
diff --git a/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java b/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
index 2073087..28a7804 100644
--- a/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
+++ b/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
@@ -62,6 +62,7 @@
 
   @Override
   public void run() throws Exception {
+    enableGracefulStop();
     verifyCommandLine();
     runGC();
   }
diff --git a/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java b/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
index 0804d08..30dc5c4 100644
--- a/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
@@ -34,6 +34,7 @@
 
   @Override
   protected void run() throws UnloggedFailure {
+    enableGracefulStop();
     try {
       if (versionManager.isKnownIndex(name)) {
         if (versionManager.activateLatestIndex(name)) {
diff --git a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
index fb62b48..1fb0e13 100644
--- a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -52,6 +52,7 @@
 
   @Override
   protected void run() throws UnloggedFailure {
+    enableGracefulStop();
     boolean ok = true;
     for (ChangeResource rsrc : changes.values()) {
       try {
diff --git a/java/com/google/gerrit/sshd/commands/IndexChangesInProjectCommand.java b/java/com/google/gerrit/sshd/commands/IndexChangesInProjectCommand.java
index 56b00a5..168dc19 100644
--- a/java/com/google/gerrit/sshd/commands/IndexChangesInProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexChangesInProjectCommand.java
@@ -43,6 +43,7 @@
 
   @Override
   protected void run() throws UnloggedFailure, Failure, Exception {
+    enableGracefulStop();
     if (projects.isEmpty()) {
       throw die("needs at least one project as command arguments");
     }
diff --git a/java/com/google/gerrit/sshd/commands/IndexStartCommand.java b/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
index f3d349c..5433b17 100644
--- a/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
@@ -38,6 +38,7 @@
 
   @Override
   protected void run() throws UnloggedFailure {
+    enableGracefulStop();
     try {
       if (versionManager.isKnownIndex(name)) {
         if (versionManager.startReindexer(name, force)) {
diff --git a/java/com/google/gerrit/sshd/commands/KillCommand.java b/java/com/google/gerrit/sshd/commands/KillCommand.java
index df74f86..a633a8a 100644
--- a/java/com/google/gerrit/sshd/commands/KillCommand.java
+++ b/java/com/google/gerrit/sshd/commands/KillCommand.java
@@ -47,6 +47,7 @@
 
   @Override
   protected void run() {
+    enableGracefulStop();
     ConfigResource cfgRsrc = new ConfigResource();
     for (String id : taskIds) {
       try {
diff --git a/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index bdf5412..7bf42eb 100644
--- a/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -52,6 +52,7 @@
 
   @Override
   public void run() throws Exception {
+    enableGracefulStop();
     if (listGroups.getUser() != null && !listGroups.getProjects().isEmpty()) {
       throw die("--user and --project options are not compatible.");
     }
diff --git a/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java b/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
index c8b8fa1..1a7be32 100644
--- a/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
@@ -40,6 +40,7 @@
   @SuppressWarnings("unchecked")
   @Override
   protected void run() {
+    enableGracefulStop();
     Map<String, String> logs = new TreeMap<>();
     for (Enumeration<Logger> logger = LogManager.getCurrentLoggers(); logger.hasMoreElements(); ) {
       Logger log = logger.nextElement();
diff --git a/java/com/google/gerrit/sshd/commands/ListMembersCommand.java b/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
index dc1bc6e..3269c2b 100644
--- a/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
@@ -45,6 +45,7 @@
 
   @Override
   public void run() throws Exception {
+    enableGracefulStop();
     impl.display(stdout);
   }
 
diff --git a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index 9f2ffa9..e711d57 100644
--- a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -32,6 +32,7 @@
 
   @Override
   public void run() throws Exception {
+    enableGracefulStop();
     if (!impl.getFormat().isJson()) {
       List<String> showBranch = impl.getShowBranch();
       if (impl.isShowTree() && (showBranch != null) && !showBranch.isEmpty()) {
diff --git a/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index 80aee01..6eb045b 100644
--- a/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -74,6 +74,7 @@
 
   @Override
   protected void run() throws Failure {
+    enableGracefulStop();
     Account.Id userAccountId;
     try {
       userAccountId = accountResolver.resolve(userName).asUnique().account().id();
diff --git a/java/com/google/gerrit/sshd/commands/PluginAdminSshCommand.java b/java/com/google/gerrit/sshd/commands/PluginAdminSshCommand.java
index 7e32615..086081c 100644
--- a/java/com/google/gerrit/sshd/commands/PluginAdminSshCommand.java
+++ b/java/com/google/gerrit/sshd/commands/PluginAdminSshCommand.java
@@ -28,6 +28,7 @@
 
   @Override
   protected final void run() throws UnloggedFailure {
+    enableGracefulStop();
     if (!loader.isRemoteAdminEnabled()) {
       throw die("remote plugin administration is disabled");
     }
diff --git a/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
index 3a952f0..504b239 100644
--- a/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -41,6 +41,7 @@
 
   @Override
   public void run() throws Exception {
+    enableGracefulStop();
     Map<String, PluginInfo> output = list.apply(TopLevelResource.INSTANCE).value();
 
     if (format.isJson()) {
diff --git a/java/com/google/gerrit/sshd/commands/Query.java b/java/com/google/gerrit/sshd/commands/Query.java
index 78485d3..772eabe 100644
--- a/java/com/google/gerrit/sshd/commands/Query.java
+++ b/java/com/google/gerrit/sshd/commands/Query.java
@@ -106,6 +106,7 @@
 
   @Override
   protected void run() throws Exception {
+    enableGracefulStop();
     processor.query(join(query, " "));
   }
 
diff --git a/java/com/google/gerrit/sshd/commands/ReloadConfig.java b/java/com/google/gerrit/sshd/commands/ReloadConfig.java
index cbe3c57..eeb48bb 100644
--- a/java/com/google/gerrit/sshd/commands/ReloadConfig.java
+++ b/java/com/google/gerrit/sshd/commands/ReloadConfig.java
@@ -38,6 +38,7 @@
 
   @Override
   protected void run() throws Failure {
+    enableGracefulStop();
     Multimap<UpdateResult, ConfigUpdateEntry> updates = gerritServerConfigReloader.reloadConfig();
     if (updates.isEmpty()) {
       stdout.println("No config entries updated!");
diff --git a/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
index 166ad68..976e7bd 100644
--- a/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
+++ b/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -46,6 +46,7 @@
 
   @Override
   protected void run() throws Failure {
+    enableGracefulStop();
     try {
       GroupResource rsrc = groups.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(groupName));
       NameInput input = new NameInput();
diff --git a/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 78a7381..b58cc45 100644
--- a/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -167,6 +167,7 @@
 
   @Override
   protected void run() throws UnloggedFailure {
+    enableGracefulStop();
     if (abandonChange) {
       if (restoreChange) {
         throw die("abandon and restore actions are mutually exclusive");
diff --git a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index df1e3ed..43a1670 100644
--- a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -154,6 +154,7 @@
 
   @Override
   public void run() throws Exception {
+    enableGracefulStop();
     user = genericUserFactory.create(id);
 
     validate();
diff --git a/java/com/google/gerrit/sshd/commands/SetHeadCommand.java b/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
index fd7ef75..b6d283e 100644
--- a/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
@@ -43,6 +43,7 @@
 
   @Override
   protected void run() throws Exception {
+    enableGracefulStop();
     HeadInput input = new HeadInput();
     input.ref = newHead;
     try {
diff --git a/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java b/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
index cfdd735..3faf598 100644
--- a/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
@@ -61,6 +61,7 @@
   @SuppressWarnings("unchecked")
   @Override
   protected void run() throws MalformedURLException {
+    enableGracefulStop();
     if (level == LevelOption.RESET) {
       reset();
     } else {
diff --git a/java/com/google/gerrit/sshd/commands/SetMembersCommand.java b/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
index 2511df4..db8e42a 100644
--- a/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
@@ -102,6 +102,7 @@
 
   @Override
   protected void run() throws UnloggedFailure, Failure, Exception {
+    enableGracefulStop();
     try {
       for (AccountGroup.UUID groupUuid : groups) {
         GroupResource resource =
diff --git a/java/com/google/gerrit/sshd/commands/SetParentCommand.java b/java/com/google/gerrit/sshd/commands/SetParentCommand.java
index 406949e..d23f7fa 100644
--- a/java/com/google/gerrit/sshd/commands/SetParentCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetParentCommand.java
@@ -90,6 +90,7 @@
 
   @Override
   protected void run() throws Failure {
+    enableGracefulStop();
     if (oldParent == null && children.isEmpty()) {
       throw die(
           "child projects have to be specified as "
diff --git a/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index 8c9fc9f..9866c4e 100644
--- a/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -132,6 +132,7 @@
 
   @Override
   protected void run() throws Failure {
+    enableGracefulStop();
     ConfigInput configInput = new ConfigInput();
     configInput.requireChangeId = requireChangeID;
     configInput.submitType = submitType;
diff --git a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 5bc5537..95627e1 100644
--- a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -95,6 +95,7 @@
 
   @Override
   protected void run() throws UnloggedFailure {
+    enableGracefulStop();
     boolean ok = true;
     for (ChangeResource rsrc : changes.values()) {
       try {
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 1d756de..ba84179 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -112,6 +112,7 @@
 
   @Override
   protected void run() throws Failure {
+    enableGracefulStop();
     nw = columns - 50;
     Date now = new Date();
     stdout.format(
diff --git a/java/com/google/gerrit/sshd/commands/ShowConnections.java b/java/com/google/gerrit/sshd/commands/ShowConnections.java
index decf5d5..d271364 100644
--- a/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -86,6 +86,7 @@
 
   @Override
   protected void run() throws Failure {
+    enableGracefulStop();
     final IoAcceptor acceptor = daemon.getIoAcceptor();
     if (acceptor == null) {
       throw new Failure(1, "fatal: sshd no longer running");
diff --git a/java/com/google/gerrit/sshd/commands/ShowQueue.java b/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 2ec9e2d..779f2df 100644
--- a/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -85,6 +85,7 @@
 
   @Override
   protected void run() throws Failure {
+    enableGracefulStop();
     maxCommandWidth = wide ? Integer.MAX_VALUE : columns - 8 - 12 - 12 - 4 - 4;
     stdout.print(
         String.format(
diff --git a/java/com/google/gerrit/sshd/commands/VersionCommand.java b/java/com/google/gerrit/sshd/commands/VersionCommand.java
index 8fac979..f8771fb 100644
--- a/java/com/google/gerrit/sshd/commands/VersionCommand.java
+++ b/java/com/google/gerrit/sshd/commands/VersionCommand.java
@@ -25,6 +25,7 @@
 
   @Override
   protected void run() throws Failure {
+    enableGracefulStop();
     String v = Version.getVersion();
     if (v == null) {
       throw new Failure(1, "fatal: version unavailable");
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshDaemonIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshDaemonIT.java
new file mode 100644
index 0000000..827c192
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshDaemonIT.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.ssh;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.restapi.config.ListTasks;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import java.time.LocalDateTime;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@NoHttpd
+@UseSsh
+@Sandboxed
+@RunWith(ConfigSuite.class)
+@SuppressWarnings("unused")
+public class SshDaemonIT extends AbstractDaemonTest {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  @Inject private ListTasks listTasks;
+  @Inject private SitePaths gerritSitePath;
+
+  @ConfigSuite.Parameter protected Config config;
+
+  @ConfigSuite.Config
+  public static Config gracefulConfig() {
+    Config config = new Config();
+    config.setString("sshd", null, "gracefulStopTimeout", "10s");
+    return config;
+  }
+
+  @Override
+  public Module createSshModule() {
+    return new TestSshCommandModule();
+  }
+
+  public Future<Integer> startCommand(String command) throws Exception {
+    Callable<Integer> gracefulSession =
+        () -> {
+          int returnCode = -1;
+          logger.atFine().log("Before Command");
+          returnCode = userSshSession.execAndReturnStatus(command);
+          logger.atFine().log("After Command");
+          return returnCode;
+        };
+
+    ExecutorService executor = Executors.newFixedThreadPool(1);
+    Future<Integer> future = executor.submit(gracefulSession);
+
+    LocalDateTime timeout = LocalDateTime.now().plusSeconds(10);
+
+    TestCommand.syncPoint.await();
+
+    return future;
+  }
+
+  @Test
+  public void NonGracefulCommandIsStoppedImmediately() throws Exception {
+    Future<Integer> future = startCommand("non-graceful -d 5");
+    restart();
+    Assert.assertTrue(future.get() == -1);
+  }
+
+  @Test
+  public void GracefulCommandIsStoppedGracefully() throws Exception {
+    Future<Integer> future = startCommand("graceful -d 5");
+    restart();
+    if (cfg.getTimeUnit("sshd", null, "gracefulStopTimeout", 0, TimeUnit.SECONDS) == 0) {
+      Assert.assertTrue(future.get() == -1);
+    } else {
+      Assert.assertTrue(future.get() == 0);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
index 62dfc63..b888102 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
@@ -41,6 +41,7 @@
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermission;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.server.project.ProjectConfig;
@@ -114,6 +115,13 @@
   }
 
   @Test
+  public void permissionOnly() throws Exception {
+    Project.NameKey key = projectOperations.newProject().permissionOnly(true).create();
+    String head = gApi.projects().name(key.get()).head();
+    assertThat(head).isEqualTo(RefNames.REFS_CONFIG);
+  }
+
+  @Test
   public void getProjectConfig() throws Exception {
     Project.NameKey key = projectOperations.newProject().create();
     assertThat(projectOperations.project(key).getProjectConfig().getProject().getDescription())
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index ff8116b..a8f5af0 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -102,8 +102,8 @@
 
     maven_jar(
         name = "jackson-core",
-        artifact = "com.fasterxml.jackson.core:jackson-core:2.11.2",
-        sha1 = "bc022ab0f0c83c07f9c52c5ab9a6a4932b15cc35",
+        artifact = "com.fasterxml.jackson.core:jackson-core:2.11.3",
+        sha1 = "c2351800432bdbdd8284c3f5a7f0782a352aa84a",
     )
 
     # Google internal dependencies: these are developed at Google, so there is