blob: 6aa75f72e12c4b438c0c38111cb77b45501fee9e [file] [log] [blame]
/*
* 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.netbeans.modules.jshell.launch;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.VirtualMachine;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.debugger.Session;
import org.netbeans.api.project.Project;
import org.openide.util.Exceptions;
/**
*
* Joins the DebuggerEngine being launched and the JShell agent socket from
* the debugged VM.
* The connection (listening socket) is created from StartupExtender when the application
* is being launched. The connection is activated when the project JPDA Debugger
* <p/>
* The JShellConnection can be used even before the handshake completes. When the handshake
* completes (success or failure)
* <p/>
* The Connection transitions between states:
* <ul>
* <li>invalid, not connected, not closed, debuggerSession = null
* <li>[opt] invalid, not connected, not closed, debuggerSession = instance
* <li>valid, was connected, not closed, debuggerSession = instance; or
* <li>valid, was connected, not closed, debuggerSession = null - if the debugger is not available.
* <li>invalid, was connected, not closed; or
* <li>invalid, was connected, closed
* </ul>
* The connection is only valid during its operational time.
* @author sdedic
*/
public final class JShellConnection implements AutoCloseable {
private static final Logger LOG = Logger.getLogger(JShellConnection.class.getName());
private static final int WRITE_TIMEOUT = 10000;
private static final AtomicInteger connId = new AtomicInteger(0);
/**
* The project being run
*/
private final Project project;
/**
* The adress where this Connection expects connection from the agent
*/
private final SocketAddress listenAddress;
/**
* The connection ID, serving also as association key in the debugger and agent.
*/
private final int id;
/**
* The associated debugger session, is launched in debugger.
*/
private final Session debuggerSession;
private final ShellAgent theAgent;
/**
* Control socket for the JShell.
*/
private volatile SocketChannel controlSocket;
private volatile boolean closed;
/**
* Identification of the remote agent.
*/
private final ObjectReference agentHandle;
public Project getProject() {
return project;
}
JShellConnection(ShellAgent agent, SocketChannel controlSocket) throws IOException {
this.id = connId.incrementAndGet();
this.listenAddress = agent.getHandshakeAddress();
this.project = agent.getProject();
this.theAgent = agent;
this.controlSocket = controlSocket;
this.ostm = NIOStreams.createOutputStream(controlSocket, WRITE_TIMEOUT);
this.istm = new CloseInputStream(NIOStreams.createInputStream(controlSocket, this::disconnected));
this.debuggerSession = agent.getDebuggerSession();
LOG.log(Level.FINE, "Allocated connection: {0}", this);
agentHandle = ShellDebuggerUtils.getWorkerHandle(debuggerSession, ((InetSocketAddress)controlSocket.getRemoteAddress()).getPort());
}
private void disconnected(SocketChannel dummy) {
LOG.log(Level.FINE, "Detected disconnect: {0}", this);
theAgent.disconnect(this, true);
}
public SocketAddress getLocalAddress() {
return listenAddress;
}
public int getId() {
return id;
}
/**
* Returns agent objectref, if operating through debugger. {@code null} if
* debugger is no available
* @return agent reference or null.
*/
public ObjectReference getAgentHandle() {
return agentHandle;
}
public int getRemoteAgentId() {
try {
return ((InetSocketAddress)controlSocket.getRemoteAddress()).getPort();
} catch (IOException ex) {
return -1;
}
}
/**
* Simple wrapper, which interprets close() call as local close and will
* fire appropriate disconnect events
*/
private class CloseInputStream extends FilterInputStream {
private boolean closed;
public CloseInputStream(InputStream in) {
super(in);
}
@Override
public void close() throws IOException {
synchronized (this) {
if (closed) {
return;
}
closed = true;
}
try {
super.close();
} finally {
LOG.log(Level.FINE, "Requested to close connection: {0}", this);
theAgent.disconnect(JShellConnection.this, false);
}
}
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("JShellConnection[").append("id = ").append(id).
append(", session = ").append(debuggerSession).
append(", handshake = ").append(listenAddress).
append(", control = ").append(controlSocket).
append("]");
return sb.toString();
}
public ShellAgent getMachineAgent() {
return theAgent;
}
public void close() {
shutDown();
}
/**
* Shuts down the connection, may be called for local or remote close.
*/
void shutDown() {
try {
LOG.log(Level.FINE, "notifyShutdown: closing control socket: {0}", controlSocket);
if (controlSocket != null) {
// assumes both input and output terminate
controlSocket.close();
}
ostm.close();
istm.close();
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
} finally {
synchronized (this) {
closed = true;
notify();
}
}
}
synchronized boolean acceptSocketAndKey(int key, SocketChannel socket, ObjectInputStream istm) throws IOException {
if (closed) {
return false;
}
if (id != key) {
return false;
}
this.ostm = Channels.newOutputStream(socket);
this.istm = istm;
this.controlSocket = socket;
notify();
return true;
}
/**
* True, if the connection is in the process of initialization. Usually between the time the
* debugger connects to the target VM and the remote agent connects back.
*
* @return true, if the connection is initializing
*/
public boolean isInitialized() {
return debuggerSession != null || controlSocket != null;
}
/**
* True, if the connection is still valid. The connection is valid until it is closed
* or its control socket is missing or disconnected
* @return
*/
public synchronized boolean isValid() {
if (closed) {
return false;
}
if (controlSocket == null || !controlSocket.isConnected() || !controlSocket.isOpen()) {
return false;
}
return true;
}
public boolean isClosed() {
return closed;
}
public Session getDebuggerSession() {
return getMachineAgent().getDebuggerSession();
}
public VirtualMachine getVirtualMachine() {
return getMachineAgent().getDebuggerMachine();
}
/**
* Returns true, if the control connection was once established.
* @return
*/
public boolean wasConnected() {
return controlSocket != null;
}
public OutputStream getAgentInput() {
return controlSocket != null ? ostm : null;
}
public InputStream getAgentOutput() {
return controlSocket != null ? istm : null;
}
/**
* Sends instructions to interrupt the running user code
*/
public void interrupt() {
}
private OutputStream ostm;
private InputStream istm;
}