ctbrec-5.3.2-experimental/client/src/main/java/ctbrec/ui/ExternalBrowser.java

212 lines
7.8 KiB
Java

package ctbrec.ui;
import static java.nio.charset.StandardCharsets.*;
import java.io.BufferedReader;
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.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.OS;
import ctbrec.Settings.ProxyType;
import ctbrec.io.StreamRedirector;
public class ExternalBrowser implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(ExternalBrowser.class);
private static final ExternalBrowser INSTANCE = new ExternalBrowser();
private Lock lock = new ReentrantLock();
private Process p;
private Consumer<String> messageListener;
private InputStream in;
private OutputStream out;
private Socket socket;
private Thread reader;
private volatile boolean stopped = true;
private volatile boolean browserReady = false;
private Object browserReadyLock = new Object();
public static ExternalBrowser getInstance() {
return INSTANCE;
}
public void run(JSONObject jsonConfig, Consumer<String> messageListener) throws InterruptedException, IOException {
LOG.debug("Running browser with config {}", jsonConfig);
lock.lock();
try {
stopped = false;
this.messageListener = messageListener;
addProxyConfig(jsonConfig.getJSONObject("config"));
File configDir = new File(Config.getInstance().getConfigDir(), "ctbrec-minimal-browser");
String[] cmdline = OS.getBrowserCommand(configDir.getCanonicalPath());
p = new ProcessBuilder(cmdline).start();
if (LOG.isTraceEnabled()) {
new Thread(new StreamRedirector(p.getInputStream(), System.out)).start();
new Thread(new StreamRedirector(p.getErrorStream(), System.err)).start();
} else {
new Thread(new StreamRedirector(p.getInputStream(), OutputStream.nullOutputStream())).start();
new Thread(new StreamRedirector(p.getErrorStream(), OutputStream.nullOutputStream())).start();
}
LOG.debug("Browser started: {}", Arrays.toString(cmdline));
connectToRemoteControlSocket();
while (!browserReady) {
synchronized (browserReadyLock) {
browserReadyLock.wait(100);
}
}
if(LOG.isTraceEnabled()) {
LOG.debug("Connected to remote control server. Sending config {}", jsonConfig);
} else {
LOG.debug("Connected to remote control server. Sending config");
}
out.write(jsonConfig.toString().getBytes(UTF_8));
out.write('\n');
out.flush();
LOG.debug("Waiting for browser to terminate");
p.waitFor();
int exitValue = p.exitValue();
p = null;
reader = null;
in = null;
out = null;
this.messageListener = null;
LOG.debug("Browser Process terminated with {}", exitValue);
} finally {
lock.unlock();
}
}
private void connectToRemoteControlSocket() throws IOException {
for (int i = 0; i < 20; i++) {
try {
socket = new Socket("localhost", 3202);
in = socket.getInputStream();
out = socket.getOutputStream();
reader = new Thread(this::readBrowserOutput);
reader.start();
LOG.debug("Connected to control socket");
return;
} catch (IOException e) {
if(i == 19) {
LOG.error("Connection to remote control socket failed", e);
throw e;
}
}
// wait a bit, so that the remote server socket can be opened by electron
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public void executeJavaScript(String javaScript) throws IOException {
//LOG.debug("Executing JS {}", javaScript);
JSONObject script = new JSONObject();
script.put("execute", javaScript);
out.write(script.toString().getBytes(UTF_8));
out.write('\n');
out.flush();
if(javaScript.equals("quit")) {
stopped = true;
}
}
@Override
public void close() throws IOException {
if(stopped) {
return;
}
stopped = true;
executeJavaScript("quit");
}
private void readBrowserOutput() {
LOG.debug("Browser output reader started");
try {
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line;
synchronized (browserReadyLock) {
browserReady = true;
browserReadyLock.notifyAll();
}
while( !Thread.interrupted() && (line = br.readLine()) != null ) {
LOG.debug("Browser output: {}", line);
if(!line.startsWith("{")) {
} else {
if(messageListener != null) {
messageListener.accept(line);
}
}
}
} catch (IOException e) {
if(!stopped) {
LOG.error("Couldn't read browser output", e);
}
} finally {
stopped = true;
synchronized (browserReadyLock) {
browserReady = true;
browserReadyLock.notifyAll();
}
}
}
private void addProxyConfig(JSONObject jsonConfig) {
ProxyType proxyType = Config.getInstance().getSettings().proxyType;
switch (proxyType) {
case HTTP:
JSONObject proxy = new JSONObject();
proxy.put("address",
"http=" + 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()) {
String username = Config.getInstance().getSettings().proxyUser;
String password = Config.getInstance().getSettings().proxyPassword;
proxy.put("user", username);
proxy.put("password", password);
}
jsonConfig.put("proxy", proxy);
break;
case SOCKS4:
proxy = new JSONObject();
proxy.put("address", "socks4://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
jsonConfig.put("proxy", proxy);
break;
case SOCKS5:
proxy = new JSONObject();
proxy.put("address", "socks5://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
if(Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) {
String username = Config.getInstance().getSettings().proxyUser;
String password = Config.getInstance().getSettings().proxyPassword;
proxy.put("user", username);
proxy.put("password", password);
}
jsonConfig.put("proxy", proxy);
break;
case DIRECT:
default:
// nothing to do here
break;
}
}
}