/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 org.apache.sshd.client.session;

import java.io.IOException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.sshd.agent.common.AgentForwardSupport;
import org.apache.sshd.client.ClientFactoryManager;
import org.apache.sshd.common.FactoryManager;
import org.apache.sshd.common.SshConstants;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.io.AbstractIoWriteFuture;
import org.apache.sshd.common.io.IoWriteFuture;
import org.apache.sshd.common.session.helpers.AbstractConnectionService;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.server.x11.X11ForwardSupport;

/**
 * Client side <code>ssh-connection</code> service.
 *
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public class ClientConnectionService extends AbstractConnectionService<AbstractClientSession> implements ClientSessionHolder {
    public ClientConnectionService(AbstractClientSession s) throws SshException {
        super(s);
    }

    @Override
    public final ClientSession getClientSession() {
        return getSession();
    }

    @Override
    public void start() {
        ClientSession session = getClientSession();
        if (!session.isAuthenticated()) {
            throw new IllegalStateException("Session is not authenticated");
        }
        startHeartBeat();
    }

    protected void startHeartBeat() {
        ClientSession session = getClientSession();
        long interval = session.getLongProperty(ClientFactoryManager.HEARTBEAT_INTERVAL, ClientFactoryManager.DEFAULT_HEARTBEAT_INTERVAL);
        if (interval > 0L) {
            FactoryManager manager = session.getFactoryManager();
            ScheduledExecutorService service = manager.getScheduledExecutorService();
            service.scheduleAtFixedRate(this::sendHeartBeat, interval, interval, TimeUnit.MILLISECONDS);
            if (log.isDebugEnabled()) {
                log.debug("startHeartbeat - started at interval={}", interval);
            }
        }
    }

    /**
     * Sends a heartbeat message
     * @return The {@link IoWriteFuture} that can be used to wait for the
     * message write completion
     */
    protected IoWriteFuture sendHeartBeat() {
        ClientSession session = getClientSession();
        String request = session.getStringProperty(ClientFactoryManager.HEARTBEAT_REQUEST, ClientFactoryManager.DEFAULT_KEEP_ALIVE_HEARTBEAT_STRING);
        try {
            Buffer buf = session.createBuffer(SshConstants.SSH_MSG_GLOBAL_REQUEST, request.length() + Byte.SIZE);
            buf.putString(request);
            buf.putBoolean(false);
            return session.writePacket(buf);
        } catch (IOException e) {
            if (log.isDebugEnabled()) {
                log.debug("Error (" + e.getClass().getSimpleName() + ") sending keepalive message=" + request + ": " + e.getMessage());
            }

            Throwable t = e;
            return new AbstractIoWriteFuture(request, null) {
                {
                    setValue(t);
                }
            };
        }
    }

    @Override
    public AgentForwardSupport getAgentForwardSupport() {
        throw new IllegalStateException("Server side operation");
    }

    @Override
    public X11ForwardSupport getX11ForwardSupport() {
        throw new IllegalStateException("Server side operation");
    }
}

@@ -914,24 +914,69 @@
 torService;%0A
+import java.util.concurrent.ScheduledFuture;%0A
 import java.
@@ -1793,16 +1793,60 @@
 older %7B%0A
+%0A    private ScheduledFuture%3C?%3E heartBeat;%0A%0A
     publ
@@ -2296,24 +2296,38 @@
 t();%0A    %7D%0A%0A
+    @Override%0A
     protecte
@@ -2337,26 +2337,154 @@
 oid 
-startHeartBeat() %7B
+preClose() %7B%0A        stopHeartBeat();%0A        super.preClose();%0A    %7D%0A%0A    protected synchronized void startHeartBeat() %7B%0A        stopHeartBeat();
 %0A   
@@ -2863,16 +2863,28 @@
         
+ heartBeat =
  service
@@ -3116,24 +3116,190 @@
    %7D%0A    %7D%0A%0A
+    protected synchronized void stopHeartBeat() %7B%0A        if (heartBeat != null) %7B%0A            heartBeat.cancel(true);%0A            heartBeat = null;%0A        %7D%0A    %7D%0A%0A
     /**%0A    
@@ -3900,22 +3900,38 @@
         
-return
+IoWriteFuture future =
  session
@@ -3961,33 +3961,155 @@
     
-%7D catch (IOException e) %7B
+    future.addListener(this::futureDone);%0A            return future;%0A        %7D catch (IOException e) %7B%0A            getSession().exceptionCaught(e);
 %0A   
@@ -4291,25 +4291,24 @@
           %7D%0A
-%0A
             
@@ -4479,32 +4479,218 @@
        %7D%0A    %7D%0A%0A
+    protected void futureDone(IoWriteFuture future) %7B%0A        Throwable t = future.getException();%0A        if (t != null) %7B%0A            getSession().exceptionCaught(t);%0A        %7D%0A    %7D%0A%0A
     @Override%0A  

Make sure the heart beat scheduled job is terminated and that exceptions are propagated to the client session