forked from j62/ctbrec
Fix NPE in ExternalBrowser
This commit is contained in:
parent
ef2e354d65
commit
8a71668508
|
@ -1,47 +1,37 @@
|
||||||
package ctbrec.ui;
|
package ctbrec.ui;
|
||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.*;
|
import ctbrec.Config;
|
||||||
|
import ctbrec.OS;
|
||||||
|
import ctbrec.io.ProcessOutputLogger;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.*;
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.Arrays;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.json.JSONObject;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import ctbrec.Config;
|
@Slf4j
|
||||||
import ctbrec.OS;
|
public class ExternalBrowser implements AutoCloseable { // NOSONAR singleton is wanted
|
||||||
import ctbrec.io.ProcessOutputLogger;
|
|
||||||
|
|
||||||
public class ExternalBrowser implements AutoCloseable {
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ExternalBrowser.class);
|
|
||||||
private static final ExternalBrowser INSTANCE = new ExternalBrowser();
|
private static final ExternalBrowser INSTANCE = new ExternalBrowser();
|
||||||
private Lock lock = new ReentrantLock();
|
private static final String MSGID = "msgid";
|
||||||
|
private final Lock lock = new ReentrantLock();
|
||||||
|
private Socket socket;
|
||||||
|
|
||||||
private Consumer<String> messageListener;
|
private Consumer<String> messageListener;
|
||||||
private InputStream in;
|
private InputStream in;
|
||||||
private OutputStream out;
|
private OutputStream out;
|
||||||
private Socket socket; // NOSONAR
|
|
||||||
private Thread reader;
|
private Thread reader;
|
||||||
private volatile boolean stopped = true;
|
private volatile boolean stopped = true;
|
||||||
private volatile boolean browserReady = false;
|
private volatile boolean browserReady = false;
|
||||||
private Object browserReadyLock = new Object();
|
private final Object browserReadyLock = new Object();
|
||||||
|
|
||||||
private Map<String, CompletableFuture<Object>> responseFutures = new HashMap<>();
|
private final Map<String, CompletableFuture<Object>> responseFutures = new HashMap<>();
|
||||||
private Runnable onReadyCallback;
|
private Runnable onReadyCallback;
|
||||||
|
|
||||||
public static ExternalBrowser getInstance() {
|
public static ExternalBrowser getInstance() {
|
||||||
|
@ -49,7 +39,7 @@ public class ExternalBrowser implements AutoCloseable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void run(JSONObject jsonConfig, Consumer<String> messageListener) throws InterruptedException, IOException {
|
public void run(JSONObject jsonConfig, Consumer<String> messageListener) throws InterruptedException, IOException {
|
||||||
LOG.debug("Running browser with config {}", jsonConfig);
|
log.debug("Running browser with config {}", jsonConfig);
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
stopped = false;
|
stopped = false;
|
||||||
|
@ -62,7 +52,7 @@ public class ExternalBrowser implements AutoCloseable {
|
||||||
Process p = new ProcessBuilder(cmdline).start();
|
Process p = new ProcessBuilder(cmdline).start();
|
||||||
new Thread(new ProcessOutputLogger(p.getInputStream(), "ExternalBrowser stdout")).start();
|
new Thread(new ProcessOutputLogger(p.getInputStream(), "ExternalBrowser stdout")).start();
|
||||||
new Thread(new ProcessOutputLogger(p.getErrorStream(), "ExternalBrowser stderr")).start();
|
new Thread(new ProcessOutputLogger(p.getErrorStream(), "ExternalBrowser stderr")).start();
|
||||||
LOG.debug("Browser started: {}", Arrays.toString(cmdline));
|
log.debug("Browser started: {}", Arrays.toString(cmdline));
|
||||||
|
|
||||||
connectToRemoteControlSocket();
|
connectToRemoteControlSocket();
|
||||||
while (!browserReady) {
|
while (!browserReady) {
|
||||||
|
@ -70,10 +60,10 @@ public class ExternalBrowser implements AutoCloseable {
|
||||||
browserReadyLock.wait(100);
|
browserReadyLock.wait(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(LOG.isTraceEnabled()) {
|
if (log.isTraceEnabled()) {
|
||||||
LOG.debug("Connected to remote control server. Sending config {}", jsonConfig);
|
log.debug("Connected to remote control server. Sending config {}", jsonConfig);
|
||||||
} else {
|
} else {
|
||||||
LOG.debug("Connected to remote control server. Sending config");
|
log.debug("Connected to remote control server. Sending config");
|
||||||
}
|
}
|
||||||
out.write(jsonConfig.toString().getBytes(UTF_8));
|
out.write(jsonConfig.toString().getBytes(UTF_8));
|
||||||
out.write('\n');
|
out.write('\n');
|
||||||
|
@ -81,14 +71,14 @@ public class ExternalBrowser implements AutoCloseable {
|
||||||
|
|
||||||
Optional.ofNullable(onReadyCallback).ifPresent(Runnable::run);
|
Optional.ofNullable(onReadyCallback).ifPresent(Runnable::run);
|
||||||
|
|
||||||
LOG.debug("Waiting for browser to terminate");
|
log.debug("Waiting for browser to terminate");
|
||||||
p.waitFor();
|
p.waitFor();
|
||||||
int exitValue = p.exitValue();
|
int exitValue = p.exitValue();
|
||||||
reader = null;
|
reader = null;
|
||||||
in = null;
|
in = null;
|
||||||
out = null;
|
out = null;
|
||||||
this.messageListener = null;
|
this.messageListener = null;
|
||||||
LOG.debug("Browser Process terminated with {}", exitValue);
|
log.debug("Browser Process terminated with {}", exitValue);
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
|
@ -102,11 +92,11 @@ public class ExternalBrowser implements AutoCloseable {
|
||||||
out = socket.getOutputStream();
|
out = socket.getOutputStream();
|
||||||
reader = new Thread(this::readBrowserOutput);
|
reader = new Thread(this::readBrowserOutput);
|
||||||
reader.start();
|
reader.start();
|
||||||
LOG.debug("Connected to control socket");
|
log.debug("Connected to control socket");
|
||||||
return;
|
return;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
if(i == 19) {
|
if (i == 19) {
|
||||||
LOG.error("Connection to remote control socket failed", e);
|
log.error("Connection to remote control socket failed", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,9 +112,9 @@ public class ExternalBrowser implements AutoCloseable {
|
||||||
|
|
||||||
public CompletableFuture<Object> executeJavaScript(String javaScript) throws IOException {
|
public CompletableFuture<Object> executeJavaScript(String javaScript) throws IOException {
|
||||||
String id = UUID.randomUUID().toString();
|
String id = UUID.randomUUID().toString();
|
||||||
var future = new CompletableFuture<Object>();
|
var future = new CompletableFuture<>();
|
||||||
var script = new JSONObject();
|
var script = new JSONObject();
|
||||||
script.put("msgid", id);
|
script.put(MSGID, id);
|
||||||
script.put("execute", javaScript);
|
script.put("execute", javaScript);
|
||||||
if (out != null) {
|
if (out != null) {
|
||||||
out.write(script.toString().getBytes(UTF_8));
|
out.write(script.toString().getBytes(UTF_8));
|
||||||
|
@ -140,7 +130,7 @@ public class ExternalBrowser implements AutoCloseable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
if(stopped) {
|
if (stopped) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stopped = true;
|
stopped = true;
|
||||||
|
@ -148,27 +138,29 @@ public class ExternalBrowser implements AutoCloseable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readBrowserOutput() {
|
private void readBrowserOutput() {
|
||||||
LOG.debug("Browser output reader started");
|
log.debug("Browser output reader started");
|
||||||
try (var br = new BufferedReader(new InputStreamReader(in))) {
|
try (var br = new BufferedReader(new InputStreamReader(in))) {
|
||||||
String line;
|
String line;
|
||||||
synchronized (browserReadyLock) {
|
synchronized (browserReadyLock) {
|
||||||
browserReady = true;
|
browserReady = true;
|
||||||
browserReadyLock.notifyAll();
|
browserReadyLock.notifyAll();
|
||||||
}
|
}
|
||||||
while( !Thread.interrupted() && (line = br.readLine()) != null ) {
|
while (!Thread.interrupted() && (line = br.readLine()) != null) {
|
||||||
LOG.debug("Browser output: {}", line);
|
log.debug("Browser output: {}", line);
|
||||||
if (line.startsWith("{")) {
|
if (line.startsWith("{")) {
|
||||||
JSONObject json = new JSONObject(line);
|
JSONObject json = new JSONObject(line);
|
||||||
if (json.has("msgid")) {
|
if (json.has(MSGID)) {
|
||||||
handleExecuteScriptResponse(json);
|
handleExecuteScriptResponse(json);
|
||||||
} else {
|
} else {
|
||||||
|
if (messageListener != null) {
|
||||||
messageListener.accept(line);
|
messageListener.accept(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
if(!stopped) {
|
if (!stopped) {
|
||||||
LOG.error("Couldn't read browser output", e);
|
log.error("Couldn't read browser output", e);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
stopped = true;
|
stopped = true;
|
||||||
|
@ -180,54 +172,57 @@ public class ExternalBrowser implements AutoCloseable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleExecuteScriptResponse(JSONObject json) {
|
private void handleExecuteScriptResponse(JSONObject json) {
|
||||||
var msgid = json.getString("msgid");
|
var msgid = json.getString(MSGID);
|
||||||
LOG.debug("Future {}", msgid);
|
log.debug("Future {}", msgid);
|
||||||
CompletableFuture<Object> future = responseFutures.get(msgid);
|
CompletableFuture<Object> future = responseFutures.get(msgid);
|
||||||
if (future != null) {
|
if (future != null) {
|
||||||
responseFutures.remove(msgid);
|
responseFutures.remove(msgid);
|
||||||
if (json.has("result")) {
|
final String RESULT = "result";
|
||||||
LOG.debug("Future {} done. Result: {}", msgid, json.get("result"));
|
if (json.has(RESULT)) {
|
||||||
future.complete(json.get("result"));
|
log.debug("Future {} done. Result: {}", msgid, json.get(RESULT));
|
||||||
|
future.complete(json.get(RESULT));
|
||||||
} else if (json.has("error")) {
|
} else if (json.has("error")) {
|
||||||
LOG.debug("Future {} failed", msgid);
|
log.debug("Future {} failed", msgid);
|
||||||
future.completeExceptionally(new Exception(json.getJSONObject("error").toString()));
|
future.completeExceptionally(new Exception(json.getJSONObject("error").toString()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG.warn("No future for previous request {}", msgid);
|
log.warn("No future for previous request {}", msgid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addProxyConfig(JSONObject jsonConfig) {
|
private void addProxyConfig(JSONObject jsonConfig) {
|
||||||
var proxyType = Config.getInstance().getSettings().proxyType;
|
var proxyType = Config.getInstance().getSettings().proxyType;
|
||||||
|
var proxy = new JSONObject();
|
||||||
|
final String KEY_ADDRESS = "address";
|
||||||
|
final String KEY_PROXY = "proxy";
|
||||||
switch (proxyType) {
|
switch (proxyType) {
|
||||||
case HTTP:
|
case HTTP:
|
||||||
var proxy = new JSONObject();
|
proxy.put(KEY_ADDRESS,
|
||||||
proxy.put("address",
|
|
||||||
"http=" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort
|
"http=" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort
|
||||||
+ ";https=" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
|
+ ";https=" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
|
||||||
if(Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) {
|
if (Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) {
|
||||||
String username = Config.getInstance().getSettings().proxyUser;
|
String username = Config.getInstance().getSettings().proxyUser;
|
||||||
String password = Config.getInstance().getSettings().proxyPassword;
|
String password = Config.getInstance().getSettings().proxyPassword;
|
||||||
proxy.put("user", username);
|
proxy.put("user", username);
|
||||||
proxy.put("password", password);
|
proxy.put("password", password);
|
||||||
}
|
}
|
||||||
jsonConfig.put("proxy", proxy);
|
jsonConfig.put(KEY_PROXY, proxy);
|
||||||
break;
|
break;
|
||||||
case SOCKS4:
|
case SOCKS4:
|
||||||
proxy = new JSONObject();
|
proxy = new JSONObject();
|
||||||
proxy.put("address", "socks4://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
|
proxy.put(KEY_ADDRESS, "socks4://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
|
||||||
jsonConfig.put("proxy", proxy);
|
jsonConfig.put(KEY_PROXY, proxy);
|
||||||
break;
|
break;
|
||||||
case SOCKS5:
|
case SOCKS5:
|
||||||
proxy = new JSONObject();
|
proxy = new JSONObject();
|
||||||
proxy.put("address", "socks5://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
|
proxy.put(KEY_ADDRESS, "socks5://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
|
||||||
if(Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) {
|
if (Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) {
|
||||||
String username = Config.getInstance().getSettings().proxyUser;
|
String username = Config.getInstance().getSettings().proxyUser;
|
||||||
String password = Config.getInstance().getSettings().proxyPassword;
|
String password = Config.getInstance().getSettings().proxyPassword;
|
||||||
proxy.put("user", username);
|
proxy.put("user", username);
|
||||||
proxy.put("password", password);
|
proxy.put("password", password);
|
||||||
}
|
}
|
||||||
jsonConfig.put("proxy", proxy);
|
jsonConfig.put(KEY_PROXY, proxy);
|
||||||
break;
|
break;
|
||||||
case DIRECT:
|
case DIRECT:
|
||||||
default:
|
default:
|
||||||
|
|
Loading…
Reference in New Issue