proxy progress - backend and frontend

This commit is contained in:
rusefi 2020-07-25 19:17:28 -04:00
parent f42e08c8e5
commit 98404eb69b
13 changed files with 223 additions and 64 deletions

View File

@ -1,8 +1,6 @@
package com.rusefi;
import com.devexperts.logging.Logging;
import com.opensr5.Logger;
import com.rusefi.core.MessagesCentral;
import com.rusefi.io.IoStream;
import com.rusefi.io.commands.HelloCommand;
import com.rusefi.io.tcp.BinaryProtocolProxy;
@ -13,45 +11,70 @@ import com.rusefi.server.rusEFISSLContext;
import com.rusefi.tools.online.HttpUtil;
import com.rusefi.tools.online.ProxyClient;
import java.io.Closeable;
import java.io.IOException;
import static com.devexperts.logging.Logging.getLogging;
public class LocalApplicationProxy {
public class LocalApplicationProxy implements Closeable {
private static final Logging log = getLogging(LocalApplicationProxy.class);
public static final int SERVER_PORT_FOR_APPLICATIONS = HttpUtil.getIntProperty("applications.port", 8002);
private final ApplicationRequest applicationRequest;
private final ServerHolder serverHolder;
private final IoStream authenticatorToProxyStream;
public LocalApplicationProxy(ApplicationRequest applicationRequest) {
public LocalApplicationProxy(ApplicationRequest applicationRequest, ServerHolder serverHolder, IoStream authenticatorToProxyStream) {
this.applicationRequest = applicationRequest;
this.serverHolder = serverHolder;
this.authenticatorToProxyStream = authenticatorToProxyStream;
}
public ApplicationRequest getApplicationRequest() {
return applicationRequest;
}
/**
* @param serverPortForRemoteUsers port on which rusEFI proxy accepts authenticator connections
* @param applicationRequest remote session we want to connect to
* @param localApplicationPort local port we would bind for TunerStudio to connect to
* @param localApplicationPort local port we would bind for TunerStudio to connect to
* @param jsonHttpPort
* @param disconnectListener
* @param connectionListener
*/
public static ServerHolder startAndRun(int serverPortForRemoteUsers, ApplicationRequest applicationRequest, int localApplicationPort, int jsonHttpPort, TcpIoStream.DisconnectListener disconnectListener) throws IOException {
public static ServerHolder startAndRun(int serverPortForRemoteUsers, ApplicationRequest applicationRequest, int localApplicationPort, int jsonHttpPort, TcpIoStream.DisconnectListener disconnectListener, ConnectionListener connectionListener) throws IOException {
String version = HttpUtil.executeGet(ProxyClient.getHttpAddress(jsonHttpPort) + ProxyClient.VERSION_PATH);
log.info("Server says version=" + version);
if (!version.contains(ProxyClient.BACKEND_VERSION))
throw new IOException("Unexpected backend version " + version + " while we want " + ProxyClient.BACKEND_VERSION);
IoStream authenticatorToProxyStream = new TcpIoStream("authenticatorToProxyStream ", rusEFISSLContext.getSSLSocket(HttpUtil.RUSEFI_PROXY_HOSTNAME, serverPortForRemoteUsers), disconnectListener);
LocalApplicationProxy localApplicationProxy = new LocalApplicationProxy(applicationRequest);
log.info("Pushing " + applicationRequest);
localApplicationProxy.run(authenticatorToProxyStream);
LocalApplicationProxy.sendHello(authenticatorToProxyStream, applicationRequest);
return BinaryProtocolProxy.createProxy(authenticatorToProxyStream, localApplicationPort);
ServerHolder serverHolder = BinaryProtocolProxy.createProxy(authenticatorToProxyStream, localApplicationPort);
LocalApplicationProxy localApplicationProxy = new LocalApplicationProxy(applicationRequest, serverHolder, authenticatorToProxyStream);
connectionListener.onConnected(localApplicationProxy);
return serverHolder;
}
public void run(IoStream authenticatorToProxyStream) throws IOException {
public static void sendHello(IoStream authenticatorToProxyStream, ApplicationRequest applicationRequest) throws IOException {
log.info("Pushing " + applicationRequest);
// right from connection push session authentication data
new HelloCommand(applicationRequest.toJson()).handle(authenticatorToProxyStream);
}
public static void start(String[] strings) {
}
@Override
public void close() {
serverHolder.close();
authenticatorToProxyStream.close();
}
public interface ConnectionListener {
ConnectionListener VOID = localApplicationProxy -> {
};
void onConnected(LocalApplicationProxy localApplicationProxy);
}
}

View File

@ -7,38 +7,37 @@ import java.util.Objects;
public class ApplicationRequest {
private static final String SESSION = "session";
private static final String USER_ID = "user_id";
private final SessionDetails sessionDetails;
private final int targetUserId;
private final UserDetails targetUser;
public ApplicationRequest(SessionDetails sessionDetails, int targetUserId) {
public ApplicationRequest(SessionDetails sessionDetails, UserDetails targetUser) {
this.sessionDetails = sessionDetails;
this.targetUserId = targetUserId;
this.targetUser = targetUser;
}
public SessionDetails getSessionDetails() {
return sessionDetails;
}
public int getTargetUserId() {
return targetUserId;
public UserDetails getTargetUser() {
return targetUser;
}
public String toJson() {
JSONObject jsonObject = new JSONObject();
jsonObject.put(SESSION, sessionDetails.toJson());
jsonObject.put(USER_ID, targetUserId);
targetUser.put(jsonObject);
return jsonObject.toJSONString();
}
public static ApplicationRequest valueOf(String jsonString) {
JSONObject jsonObject = HttpUtil.parse(jsonString);
long targetUserId = (Long) jsonObject.get(USER_ID);
UserDetails userDetails = UserDetails.valueOf(jsonObject);
SessionDetails session = SessionDetails.valueOf((String) jsonObject.get(SESSION));
return new ApplicationRequest(session, (int)targetUserId);
return new ApplicationRequest(session, userDetails);
}
@Override
@ -46,20 +45,20 @@ public class ApplicationRequest {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ApplicationRequest that = (ApplicationRequest) o;
return targetUserId == that.targetUserId &&
return targetUser == that.targetUser &&
sessionDetails.equals(that.sessionDetails);
}
@Override
public int hashCode() {
return Objects.hash(sessionDetails, targetUserId);
return Objects.hash(sessionDetails, targetUser);
}
@Override
public String toString() {
return "ApplicationRequest{" +
"sessionDetails=" + sessionDetails +
", targetUserId=" + targetUserId +
", targetUserId=" + targetUser +
'}';
}
}

View File

@ -19,6 +19,11 @@ public class UserDetails {
return new UserDetails(userName, (int) userId);
}
void put(JSONObject jsonObject) {
jsonObject.put(USER_ID, getUserId());
jsonObject.put(USERNAME, getUserName());
}
public String getUserName() {
return userName;
}

View File

@ -78,11 +78,13 @@ public class FullServerTest {
assertTrue("controllerRegistered", controllerRegistered.await(READ_IMAGE_TIMEOUT, TimeUnit.MILLISECONDS));
SessionDetails authenticatorSessionDetails = new SessionDetails(controllerInfo, MockRusEfiDevice.TEST_TOKEN_3, networkConnectorResult.getOneTimeToken());
ApplicationRequest applicationRequest = new ApplicationRequest(authenticatorSessionDetails, userId);
ApplicationRequest applicationRequest = new ApplicationRequest(authenticatorSessionDetails, userDetailsResolver.apply(MockRusEfiDevice.TEST_TOKEN_1));
// start authenticator
int authenticatorPort = 7004; // local port on which authenticator accepts connections from Tuner Studio
LocalApplicationProxy.startAndRun(serverPortForRemoteUsers, applicationRequest, authenticatorPort, httpPort, TcpIoStream.DisconnectListener.VOID);
LocalApplicationProxy.startAndRun(serverPortForRemoteUsers, applicationRequest, authenticatorPort, httpPort,
TcpIoStream.DisconnectListener.VOID,
LocalApplicationProxy.ConnectionListener.VOID);
CountDownLatch connectionEstablishedCountDownLatch = new CountDownLatch(1);

View File

@ -185,12 +185,11 @@ covered by FullServerTest
TestHelper.runApplicationConnectorBlocking(backend, serverPortForRemoteUsers);
SessionDetails sessionDetails = MockRusEfiDevice.createTestSession(MockRusEfiDevice.TEST_TOKEN_1, Fields.TS_SIGNATURE);
ApplicationRequest applicationRequest = new ApplicationRequest(sessionDetails, 123);
ApplicationRequest applicationRequest = new ApplicationRequest(sessionDetails, createTestUserResolver().apply(MockRusEfiDevice.TEST_TOKEN_1));
// start authenticator
IoStream authenticatorToProxyStream = TestHelper.secureConnectToLocalhost(serverPortForRemoteUsers, logger);
LocalApplicationProxy localApplicationProxy = new LocalApplicationProxy(applicationRequest);
localApplicationProxy.run(authenticatorToProxyStream);
LocalApplicationProxy.sendHello(authenticatorToProxyStream, applicationRequest);
assertTrue(disconnectedCountDownLatch.await(30, TimeUnit.SECONDS));
}

View File

@ -184,7 +184,7 @@ public class Backend implements Closeable {
return;
}
ControllerKey controllerKey = new ControllerKey(applicationRequest.getTargetUserId(), applicationRequest.getSessionDetails().getControllerInfo());
ControllerKey controllerKey = new ControllerKey(applicationRequest.getTargetUser().getUserId(), applicationRequest.getSessionDetails().getControllerInfo());
ControllerConnectionState state;
synchronized (lock) {
state = acquire(controllerKey, userDetails);
@ -318,19 +318,28 @@ public class Backend implements Closeable {
* that's different from controllers since we periodically pull outputs from controllers which allows us to detect disconnects
*/
private void runApplicationConnectionsCleanup() {
List<ApplicationConnectionState> inactiveApplications = new ArrayList<>();
List<ApplicationConnectionState> applications;
synchronized (lock) {
long now = System.currentTimeMillis();
for (ApplicationConnectionState client : applications) {
if (now - client.getClientStream().getStreamStats().getPreviousPacketArrivalTime() > applicationTimeout)
inactiveApplications.add(client);
applications = new ArrayList<>(this.applications);
}
long now = System.currentTimeMillis();
for (ApplicationConnectionState client : applications) {
if (now - client.getClientStream().getStreamStats().getPreviousPacketArrivalTime() > applicationTimeout) {
log.error("Kicking out application " + client);
close(client);
} else {
log.info("Looks alive " + client);
}
}
for (ApplicationConnectionState inactiveClient : inactiveApplications) {
log.error("Kicking out application " + inactiveClient);
close(inactiveClient);
List<ControllerConnectionState> controllers;
synchronized (lock) {
controllers = new ArrayList<>(this.controllers);
}
for (ControllerConnectionState controllerConnectionState : controllers) {
log.info("State: " + controllerConnectionState);
}
}

View File

@ -75,6 +75,15 @@ public class ControllerConnectionState {
FileUtil.close(clientSocket);
}
@Override
public String toString() {
return "ControllerConnectionState{" +
"userDetails=" + userDetails +
", isClosed=" + isClosed +
", twoKindSemaphore=" + twoKindSemaphore +
'}';
}
public void requestControllerInfo() throws IOException {
HelloCommand.send(stream);
String jsonString = HelloCommand.getHelloResponse(incomingData);

View File

@ -52,4 +52,12 @@ public class TwoKindSemaphore {
public UserDetails getOwner() {
return owner;
}
@Override
public String toString() {
return "TwoKindSemaphore{" +
"semaphore=" + semaphore +
", owner=" + owner +
'}';
}
}

View File

@ -19,7 +19,7 @@ public class SessionDetailsTest {
public void testApplicationRequest() {
ControllerInfo ci = new ControllerInfo("name", "make", "code", "sign");
SessionDetails sd = new SessionDetails(ci, "auth", 123);
ApplicationRequest ar = new ApplicationRequest(sd, 321);
ApplicationRequest ar = new ApplicationRequest(sd, new UserDetails("", 321));
String json = ar.toJson();
ApplicationRequest fromJson = ApplicationRequest.valueOf(json);

View File

@ -10,15 +10,15 @@ import java.lang.reflect.Field;
import java.util.function.Supplier;
/**
* TsPlugin launcher creates an instance of this class via reflection.
* {@link TsPluginLauncher} creates an instance of this class via reflection.
*/
public class PluginEntry implements TsPluginBody {
private final JPanel content = new JPanel(new BorderLayout());
private final JTabbedPane tabbedPane = new JTabbedPane();
/**
* the real constructor - this one is invoked via reflection
*/
@SuppressWarnings("unused")
public PluginEntry() {
this(ControllerAccess::getInstance);
}
@ -35,6 +35,7 @@ public class PluginEntry implements TsPluginBody {
BroadcastTab broadcastTab = new BroadcastTab();
RemoteTab remoteTab = new RemoteTab();
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("Upload", uploadTab.getContent());
tabbedPane.addTab("Broadcast", broadcastTab.getContent());
tabbedPane.addTab("Remote ECU", remoteTab.getContent());

View File

@ -9,6 +9,7 @@ import com.rusefi.io.tcp.TcpIoStream;
import com.rusefi.server.ApplicationRequest;
import com.rusefi.server.ControllerInfo;
import com.rusefi.server.SessionDetails;
import com.rusefi.server.UserDetails;
import com.rusefi.tools.online.HttpUtil;
import com.rusefi.tools.online.ProxyClient;
import com.rusefi.tools.online.PublicSession;
@ -18,6 +19,8 @@ import org.putgemin.VerticalFlowLayout;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Executor;
@ -43,12 +46,27 @@ public class RemoteTab {
}
};
private final JButton disconnect = new JButton("Disconnect");
private final Executor listDownloadExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory("online list downloader"));
public RemoteTab() {
JButton refresh = new JButton("Refresh List");
refresh.addActionListener(e -> requestListDownload());
disconnect.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
LocalApplicationProxy localApplicationProxy = RemoteTabController.INSTANCE.getLocalApplicationProxy();
if (localApplicationProxy != null)
localApplicationProxy.close();
RemoteTabController.INSTANCE.setState(RemoteTabController.State.NOT_CONNECTED);
requestListDownload();
}
});
JTextField applicationPort = new JTextField() {
@Override
public Dimension getPreferredSize() {
@ -78,7 +96,13 @@ public class RemoteTab {
content.add(topLines, BorderLayout.NORTH);
content.add(list, BorderLayout.CENTER);
list.add(new JLabel("Requesting list of ECUs"));
requestListDownload();
LocalApplicationProxy currentState = RemoteTabController.INSTANCE.getLocalApplicationProxy();
if (currentState == null) {
requestListDownload();
} else {
setConnectedStatus(currentState.getApplicationRequest().getTargetUser());
}
}
private String getLocalPort() {
@ -99,58 +123,99 @@ public class RemoteTab {
}
private void showList(List<PublicSession> userDetails) {
if (RemoteTabController.INSTANCE.getState() != RemoteTabController.State.NOT_CONNECTED)
return;
list.removeAll();
if (userDetails.isEmpty()) {
list.add(new JLabel("No ECUs are broadcasting at the moment :("));
} else {
JPanel verticalPanel = new JPanel(new VerticalFlowLayout());
JScrollPane scroll = new JScrollPane(verticalPanel, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
list.add(scroll);
for (PublicSession user : userDetails) {
list.add(createPanel(user));
verticalPanel.add(createSessionControl(user));
}
}
AutoupdateUtil.trueLayout(list);
}
private JComponent createPanel(PublicSession publicSession) {
JComponent userPanel = new JPanel(new FlowLayout());
userPanel.add(new JLabel(publicSession.getUserDetails().getUserName()));
private JComponent createSessionControl(PublicSession publicSession) {
JComponent topLine = new JPanel(new FlowLayout());
topLine.add(new JLabel(publicSession.getUserDetails().getUserName()));
ControllerInfo controllerInfo = publicSession.getControllerInfo();
userPanel.add(new JLabel(controllerInfo.getVehicleName() + " " + controllerInfo.getEngineMake() + " " + controllerInfo.getEngineCode()));
topLine.add(new JLabel(controllerInfo.getVehicleName() + " " + controllerInfo.getEngineMake() + " " + controllerInfo.getEngineCode()));
userPanel.add(new URLLabel(SignatureHelper.getUrl(controllerInfo.getSignature())));
JPanel bottomPanel = new JPanel(new FlowLayout());
if (publicSession.isUsed()) {
userPanel.add(new JLabel(" used by " + publicSession.getOwnerName()));
bottomPanel.add(new JLabel(" Used by " + publicSession.getOwnerName()));
} else {
JButton connect = new JButton("Connect");
connect.addActionListener(event -> {
setStatus("Connecting to " + publicSession.getUserDetails().getUserName());
new Thread(() -> runAuthenticator(publicSession, controllerInfo), "Authenticator").start();
});
userPanel.add(connect);
JButton connect = new JButton("Connect to " + publicSession.getUserDetails().getUserName());
connect.addActionListener(event -> connecToToProxy(publicSession, controllerInfo));
bottomPanel.add(connect);
}
JPanel userPanel = new JPanel(new BorderLayout());
userPanel.add(topLine, BorderLayout.NORTH);
userPanel.add(new URLLabel(SignatureHelper.getUrl(controllerInfo.getSignature())), BorderLayout.CENTER);
userPanel.add(bottomPanel, BorderLayout.SOUTH);
userPanel.setBorder(BorderFactory.createLineBorder(Color.BLACK));
return userPanel;
}
private void setStatus(String text) {
private void connecToToProxy(PublicSession publicSession, ControllerInfo controllerInfo) {
RemoteTabController.INSTANCE.setState(RemoteTabController.State.CONNECTING);
setStatus("Connecting to " + publicSession.getUserDetails().getUserName());
LocalApplicationProxy.ConnectionListener connectionListener = new LocalApplicationProxy.ConnectionListener() {
@Override
public void onConnected(LocalApplicationProxy localApplicationProxy) {
RemoteTabController.INSTANCE.setConnected(localApplicationProxy);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
setConnectedStatus(publicSession.getUserDetails());
}
});
}
};
new Thread(() -> {
runAuthenticator(publicSession, controllerInfo, connectionListener);
}, "Authenticator").start();
}
private void setConnectedStatus(UserDetails userDetails) {
setStatus("Connected to " + userDetails.getUserName(),
new JLabel("You can now connect your TunerStudio to IP address localhost and port " + getLocalPort()),
disconnect);
}
private void setStatus(String text, JComponent... extra) {
list.removeAll();
list.add(new JLabel(text));
for (JComponent component : extra)
list.add(component);
AutoupdateUtil.trueLayout(list);
}
private void runAuthenticator(PublicSession publicSession, ControllerInfo controllerInfo) {
private void runAuthenticator(PublicSession publicSession, ControllerInfo controllerInfo, LocalApplicationProxy.ConnectionListener connectionListener) {
SessionDetails sessionDetails = new SessionDetails(controllerInfo, AuthTokenPanel.getAuthToken(),
Integer.parseInt(oneTimePasswordControl.getText()));
ApplicationRequest applicationRequest = new ApplicationRequest(sessionDetails, publicSession.getUserDetails().getUserId());
ApplicationRequest applicationRequest = new ApplicationRequest(sessionDetails, publicSession.getUserDetails());
try {
AtomicReference<ServerHolder> serverHolderAtomicReference = new AtomicReference<>();
TcpIoStream.DisconnectListener disconnectListener = () -> SwingUtilities.invokeLater(() -> {
setStatus("Disconnected");
RemoteTabController.INSTANCE.setState(RemoteTabController.State.NOT_CONNECTED);
ServerHolder serverHolder = serverHolderAtomicReference.get();
if (serverHolder != null)
serverHolder.close();
@ -160,7 +225,7 @@ public class RemoteTab {
LocalApplicationProxy.SERVER_PORT_FOR_APPLICATIONS,
applicationRequest,
Integer.parseInt(getLocalPort()),
HttpUtil.PROXY_JSON_API_HTTP_PORT, disconnectListener);
HttpUtil.PROXY_JSON_API_HTTP_PORT, disconnectListener, connectionListener);
serverHolderAtomicReference.set(serverHolder);
} catch (IOException e) {
setStatus("IO error: " + e);

View File

@ -0,0 +1,37 @@
package com.rusefi.ts_plugin;
import com.rusefi.LocalApplicationProxy;
public enum RemoteTabController {
/**
* TunerStudio likes to close plugin panel, we need a singleton to preserve the state
*/
INSTANCE;
private State state = State.NOT_CONNECTED;
private LocalApplicationProxy localApplicationProxy;
public synchronized void setState(State state) {
this.state = state;
localApplicationProxy = null;
}
public synchronized State getState() {
return state;
}
public synchronized void setConnected(LocalApplicationProxy localApplicationProxy) {
setState(State.CONNECTED);
this.localApplicationProxy = localApplicationProxy;
}
public LocalApplicationProxy getLocalApplicationProxy() {
return localApplicationProxy;
}
public enum State {
NOT_CONNECTED,
CONNECTING,
CONNECTED
}
}

View File

@ -17,7 +17,7 @@ public class TsPluginLauncher implements ApplicationPlugin {
private final JPanel content = new JPanel(new VerticalFlowLayout());
public TsPluginLauncher() {
System.out.println("TsPluginLauncher " + this);
System.out.println("init " + this);
}
@Override
@ -46,6 +46,7 @@ public class TsPluginLauncher implements ApplicationPlugin {
@Override
public boolean displayPlugin(String signature) {
System.out.println("displayPlugin " + signature);
// todo: smarter implementation one day
return true;
}
@ -63,7 +64,8 @@ public class TsPluginLauncher implements ApplicationPlugin {
@Override
public JComponent getPluginPanel() {
synchronized (this) {
// only create content if TS is actually planning to display this plugin instance
// lazy initialization since TunerStudio creates one instance only to get version information without any
// intentions to display the UI
if (content.getComponents().length == 0) {
System.out.println("Create Updater " + this);
Updater updater = new Updater();
@ -75,7 +77,7 @@ public class TsPluginLauncher implements ApplicationPlugin {
@Override
public void close() {
System.out.printf("TsPlugin#close");
System.out.println("close " + this);
}
@Override