mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 13:16:44 +00:00
use unix sockets in sparrow home for instance checks and message passing, with system symlink to find existing instances for files and uris
This commit is contained in:
parent
08ec158d19
commit
d1a353ae53
5 changed files with 238 additions and 612 deletions
|
@ -16,7 +16,7 @@ import java.io.File;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public class SparrowWallet {
|
public class SparrowWallet {
|
||||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
public static final String APP_ID = "sparrow";
|
||||||
public static final String APP_NAME = "Sparrow";
|
public static final String APP_NAME = "Sparrow";
|
||||||
public static final String APP_VERSION = "1.8.5";
|
public static final String APP_VERSION = "1.8.5";
|
||||||
public static final String APP_VERSION_SUFFIX = "";
|
public static final String APP_VERSION_SUFFIX = "";
|
||||||
|
@ -79,7 +79,7 @@ public class SparrowWallet {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
instance = new Instance(fileUriArguments);
|
instance = new Instance(fileUriArguments);
|
||||||
instance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
instance.acquireLock(!fileUriArguments.isEmpty()); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
||||||
} catch(InstanceException e) {
|
} catch(InstanceException e) {
|
||||||
getLogger().error("Could not access application lock", e);
|
getLogger().error("Could not access application lock", e);
|
||||||
}
|
}
|
||||||
|
@ -130,7 +130,7 @@ public class SparrowWallet {
|
||||||
private final List<String> fileUriArguments;
|
private final List<String> fileUriArguments;
|
||||||
|
|
||||||
public Instance(List<String> fileUriArguments) {
|
public Instance(List<String> fileUriArguments) {
|
||||||
super(SparrowWallet.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
|
super(SparrowWallet.APP_ID + "." + Network.get(), true);
|
||||||
this.fileUriArguments = fileUriArguments;
|
this.fileUriArguments = fileUriArguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,535 +1,232 @@
|
||||||
/**
|
|
||||||
* Copyright 2019 Pratanu Mandal
|
|
||||||
*
|
|
||||||
* Licensed 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 com.sparrowwallet.sparrow.instance;
|
package com.sparrowwallet.sparrow.instance;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.BufferedWriter;
|
|
||||||
import java.io.DataInputStream;
|
|
||||||
import java.io.DataOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.ServerSocket;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.StandardProtocolFamily;
|
||||||
import java.nio.channels.FileChannel;
|
import java.net.UnixDomainSocketAddress;
|
||||||
import java.nio.channels.FileLock;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.SelectionKey;
|
||||||
|
import java.nio.channels.Selector;
|
||||||
|
import java.nio.channels.ServerSocketChannel;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
|
||||||
* The <code>Instance</code> class is the primary logical entry point to the library.<br>
|
|
||||||
* It allows to create an application lock or free it and send and receive messages between first and subsequent instances.<br><br>
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* // unique application ID
|
|
||||||
* String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";
|
|
||||||
*
|
|
||||||
* // create Instance instance
|
|
||||||
* Instance unique = new Instance(APP_ID) {
|
|
||||||
* @Override
|
|
||||||
* protected void receiveMessage(String message) {
|
|
||||||
* // print received message (timestamp)
|
|
||||||
* System.out.println(message);
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @Override
|
|
||||||
* protected String sendMessage() {
|
|
||||||
* // send timestamp as message
|
|
||||||
* Timestamp ts = new Timestamp(new Date().getTime());
|
|
||||||
* return "Another instance launch attempted: " + ts.toString();
|
|
||||||
* }
|
|
||||||
* };
|
|
||||||
*
|
|
||||||
* // try to obtain lock
|
|
||||||
* try {
|
|
||||||
* unique.acquireLock();
|
|
||||||
* } catch (InstanceException e) {
|
|
||||||
* e.printStackTrace();
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* ...
|
|
||||||
*
|
|
||||||
* // try to free the lock before exiting program
|
|
||||||
* try {
|
|
||||||
* unique.freeLock();
|
|
||||||
* } catch (InstanceException e) {
|
|
||||||
* e.printStackTrace();
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @author Pratanu Mandal
|
|
||||||
* @since 1.3
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public abstract class Instance {
|
public abstract class Instance {
|
||||||
private static final Logger log = LoggerFactory.getLogger(Instance.class);
|
private static final Logger log = LoggerFactory.getLogger(Instance.class);
|
||||||
|
|
||||||
// starting position of port check
|
|
||||||
private static final int PORT_START = 7221;
|
|
||||||
|
|
||||||
// system temporary directory path
|
|
||||||
private static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unique string representing the application ID.<br><br>
|
|
||||||
*
|
|
||||||
* The APP_ID must be as unique as possible.
|
|
||||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
|
||||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
|
||||||
*/
|
|
||||||
public final String APP_ID;
|
|
||||||
|
|
||||||
// auto exit from application or not
|
|
||||||
private final boolean AUTO_EXIT;
|
|
||||||
|
|
||||||
// lock server port
|
|
||||||
private int port;
|
|
||||||
|
|
||||||
// lock server socket
|
|
||||||
private ServerSocket server;
|
|
||||||
|
|
||||||
// lock file RAF object
|
|
||||||
private RandomAccessFile lockRAF;
|
|
||||||
|
|
||||||
// file lock for the lock file RAF object
|
|
||||||
private FileLock fileLock;
|
|
||||||
|
|
||||||
/**
|
public final String applicationId;
|
||||||
* Parameterized constructor.<br>
|
private final boolean autoExit;
|
||||||
* This constructor configures to automatically exit the application for subsequent instances.<br><br>
|
|
||||||
*
|
private Selector selector;
|
||||||
* The APP_ID must be as unique as possible.
|
private ServerSocketChannel serverChannel;
|
||||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
|
||||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
public Instance(final String applicationId) {
|
||||||
*
|
this(applicationId, true);
|
||||||
* @param APP_ID Unique string representing the application ID
|
}
|
||||||
*/
|
|
||||||
public Instance(final String APP_ID) {
|
public Instance(final String applicationId, final boolean autoExit) {
|
||||||
this(APP_ID, true);
|
this.applicationId = applicationId;
|
||||||
}
|
this.autoExit = autoExit;
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Parameterized constructor.<br>
|
/**
|
||||||
* This constructor allows to explicitly specify the exit strategy for subsequent instances.<br><br>
|
* Try to obtain lock. If not possible, send data to first instance.
|
||||||
*
|
*
|
||||||
* The APP_ID must be as unique as possible.
|
* @throws InstanceException throws InstanceException if it is unable to start a server or connect to server
|
||||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
*/
|
||||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
public void acquireLock(boolean findExisting) throws InstanceException {
|
||||||
*
|
Path lockFile = getLockFile(findExisting);
|
||||||
* @since 1.2
|
|
||||||
*
|
if(!Files.exists(lockFile)) {
|
||||||
* @param APP_ID Unique string representing the application ID
|
startServer(lockFile);
|
||||||
* @param AUTO_EXIT If true, automatically exit the application for subsequent instances
|
createSymlink(lockFile);
|
||||||
*/
|
} else {
|
||||||
public Instance(final String APP_ID, final boolean AUTO_EXIT) {
|
doClient(lockFile);
|
||||||
this.APP_ID = APP_ID;
|
}
|
||||||
this.AUTO_EXIT = AUTO_EXIT;
|
}
|
||||||
}
|
|
||||||
|
private void startServer(Path lockFile) throws InstanceException {
|
||||||
/**
|
try {
|
||||||
* Try to obtain lock. If not possible, send data to first instance.
|
selector = Selector.open();
|
||||||
*
|
UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(lockFile);
|
||||||
* @deprecated Use <code>acquireLock()</code> instead.
|
serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
|
||||||
* @throws InstanceException throws InstanceException if it is unable to start a server or connect to server
|
serverChannel.bind(socketAddress);
|
||||||
*/
|
serverChannel.configureBlocking(false);
|
||||||
@Deprecated
|
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
|
||||||
public void lock() throws InstanceException {
|
lockFile.toFile().deleteOnExit();
|
||||||
acquireLock();
|
} catch(Exception e) {
|
||||||
}
|
throw new InstanceException("Could not open UNIX socket at " + lockFile.toAbsolutePath(), e);
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Try to obtain lock. If not possible, send data to first instance.
|
Thread thread = new Thread(() -> {
|
||||||
*
|
while(true) {
|
||||||
* @since 1.2
|
try {
|
||||||
*
|
selector.select();
|
||||||
* @return true if able to acquire lock, false otherwise
|
Set<SelectionKey> selectedKeys = selector.selectedKeys();
|
||||||
* @throws InstanceException throws InstanceException if it is unable to start a server or connect to server
|
Iterator<SelectionKey> iter = selectedKeys.iterator();
|
||||||
*/
|
while(iter.hasNext()) {
|
||||||
public boolean acquireLock() throws InstanceException {
|
SelectionKey key = iter.next();
|
||||||
// try to obtain port number from lock file
|
if(key.isAcceptable()) {
|
||||||
port = lockFile();
|
SocketChannel client = serverChannel.accept();
|
||||||
|
client.configureBlocking(false);
|
||||||
if (port == -1) {
|
client.register(selector, SelectionKey.OP_READ);
|
||||||
// failed to fetch port number
|
}
|
||||||
// try to start server
|
if(key.isReadable()) {
|
||||||
startServer();
|
try(SocketChannel clientChannel = (SocketChannel)key.channel()) {
|
||||||
}
|
String message = readMessage(clientChannel);
|
||||||
else {
|
clientChannel.write(ByteBuffer.wrap(applicationId.getBytes(StandardCharsets.UTF_8)));
|
||||||
// port number fetched from lock file
|
receiveMessage(message);
|
||||||
// try to start client
|
}
|
||||||
doClient();
|
}
|
||||||
}
|
iter.remove();
|
||||||
|
}
|
||||||
return (server != null);
|
} catch(SocketException e) {
|
||||||
}
|
if(serverChannel.isOpen()) {
|
||||||
|
handleException(new InstanceException(e));
|
||||||
// start the server
|
}
|
||||||
private void startServer() throws InstanceException {
|
} catch(Exception e) {
|
||||||
// try to create server
|
handleException(new InstanceException(e));
|
||||||
port = PORT_START;
|
}
|
||||||
while (true) {
|
}
|
||||||
try {
|
});
|
||||||
server = new ServerSocket(port, 50, InetAddress.getByName(null));
|
|
||||||
break;
|
thread.setDaemon(true);
|
||||||
} catch (IOException e) {
|
thread.setName("SparrowInstanceListener");
|
||||||
port++;
|
thread.start();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private void doClient(Path lockFile) throws InstanceException {
|
||||||
// try to lock file
|
try(SocketChannel client = SocketChannel.open(UnixDomainSocketAddress.of(lockFile))) {
|
||||||
lockFile(port);
|
String message = sendMessage();
|
||||||
|
client.write(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8)));
|
||||||
// server created successfully; this is the first instance
|
client.shutdownOutput();
|
||||||
// keep listening for data from other instances
|
|
||||||
Thread thread = new Thread() {
|
String response = readMessage(client);
|
||||||
@Override
|
if(response.equals(applicationId) && autoExit) {
|
||||||
public void run() {
|
beforeExit();
|
||||||
while (!server.isClosed()) {
|
System.exit(0);
|
||||||
try {
|
}
|
||||||
// establish connection
|
} catch(Exception e) {
|
||||||
final Socket socket = server.accept();
|
throw new InstanceException("Could not open client connection to existing instance", e);
|
||||||
|
}
|
||||||
// handle socket on a different thread to allow parallel connections
|
}
|
||||||
Thread thread = new Thread() {
|
|
||||||
@Override
|
private static String readMessage(SocketChannel clientChannel) throws IOException {
|
||||||
public void run() {
|
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||||
try {
|
StringBuilder messageBuilder = new StringBuilder();
|
||||||
// open writer
|
while(clientChannel.read(buffer) != -1) {
|
||||||
OutputStream os = socket.getOutputStream();
|
buffer.flip();
|
||||||
DataOutputStream dos = new DataOutputStream(os);
|
messageBuilder.append(new String(buffer.array(), 0, buffer.limit()));
|
||||||
|
buffer.clear();
|
||||||
// open reader
|
}
|
||||||
InputStream is = socket.getInputStream();
|
|
||||||
DataInputStream dis = new DataInputStream(is);
|
return messageBuilder.toString();
|
||||||
|
}
|
||||||
// read message length from client
|
|
||||||
int length = dis.readInt();
|
private Path getLockFile(boolean findExisting) {
|
||||||
|
if(findExisting) {
|
||||||
// read message string from client
|
Path symlink = getSystemSymlinkPath();
|
||||||
String message = null;
|
try {
|
||||||
if (length > -1) {
|
if(Files.exists(symlink)) {
|
||||||
byte[] messageBytes = new byte[length];
|
return Files.readSymbolicLink(symlink);
|
||||||
int bytesRead = dis.read(messageBytes, 0, length);
|
}
|
||||||
message = new String(messageBytes, 0, bytesRead, "UTF-8");
|
} catch(IOException e) {
|
||||||
}
|
log.warn("Could not follow symbolic link at " + symlink.toAbsolutePath());
|
||||||
|
} catch(Exception e) {
|
||||||
// write response to client
|
//ignore
|
||||||
if (APP_ID == null) {
|
}
|
||||||
dos.writeInt(-1);
|
}
|
||||||
}
|
|
||||||
else {
|
return Storage.getSparrowDir().toPath().resolve(applicationId + ".lock");
|
||||||
byte[] appId = APP_ID.getBytes("UTF-8");
|
}
|
||||||
|
|
||||||
dos.writeInt(appId.length);
|
private void createSymlink(Path lockFile) {
|
||||||
dos.write(appId);
|
Path symlink = getSystemSymlinkPath();
|
||||||
}
|
try {
|
||||||
dos.flush();
|
if(!Files.exists(symlink, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
Files.createSymbolicLink(symlink, lockFile);
|
||||||
// close writer and reader
|
log.warn("Created symlink at " + symlink.toAbsolutePath());
|
||||||
dos.close();
|
symlink.toFile().deleteOnExit();
|
||||||
dis.close();
|
}
|
||||||
|
} catch(IOException e) {
|
||||||
// perform user action on message
|
log.warn("Could not create symlink " + symlink.toAbsolutePath() + " to lockFile at " + lockFile.toAbsolutePath());
|
||||||
receiveMessage(message);
|
} catch(Exception e) {
|
||||||
|
//ignore
|
||||||
// close socket
|
}
|
||||||
socket.close();
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
handleException(new InstanceException(e));
|
private Path getSystemSymlinkPath() {
|
||||||
}
|
return Path.of(System.getProperty("java.io.tmpdir")).resolve(applicationId + ".link");
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
/**
|
||||||
// start socket thread
|
* Free the lock if possible. This is only required to be called from the first instance.
|
||||||
thread.start();
|
*
|
||||||
} catch (SocketException e) {
|
* @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock
|
||||||
if (!server.isClosed()) {
|
*/
|
||||||
handleException(new InstanceException(e));
|
public void freeLock() throws InstanceException {
|
||||||
}
|
try {
|
||||||
} catch (IOException e) {
|
if(serverChannel != null && serverChannel.isOpen()) {
|
||||||
handleException(new InstanceException(e));
|
serverChannel.close();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
Files.deleteIfExists(getSystemSymlinkPath());
|
||||||
};
|
Files.deleteIfExists(getLockFile(false));
|
||||||
|
} catch(Exception e) {
|
||||||
thread.start();
|
throw new InstanceException(e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// do client tasks
|
|
||||||
private void doClient() throws InstanceException {
|
/**
|
||||||
// get localhost address
|
* Method used in first instance to receive messages from subsequent instances.<br><br>
|
||||||
InetAddress address = null;
|
*
|
||||||
try {
|
* This method is not synchronized.
|
||||||
address = InetAddress.getByName(null);
|
*
|
||||||
} catch (UnknownHostException e) {
|
* @param message message received by first instance from subsequent instances
|
||||||
throw new InstanceException(e);
|
*/
|
||||||
}
|
protected abstract void receiveMessage(String message);
|
||||||
|
|
||||||
// try to establish connection to server
|
/**
|
||||||
Socket socket = null;
|
* Method used in subsequent instances to send message to first instance.<br><br>
|
||||||
try {
|
*
|
||||||
socket = new Socket(address, port);
|
* It is not recommended to perform blocking (long running) tasks here. Use <code>beforeExit()</code> method instead.<br>
|
||||||
} catch (IOException e) {
|
* One exception to this rule is if you intend to perform some user interaction before sending the message.<br><br>
|
||||||
// connection failed try to start server
|
*
|
||||||
startServer();
|
* This method is not synchronized.
|
||||||
}
|
*
|
||||||
|
* @return message sent from subsequent instances
|
||||||
// connection successful try to connect to server
|
*/
|
||||||
if (socket != null) {
|
protected abstract String sendMessage();
|
||||||
try {
|
|
||||||
// get message to be sent to first instance
|
/**
|
||||||
String message = sendMessage();
|
* Method to receive and handle exceptions occurring while first instance is listening for subsequent instances.<br><br>
|
||||||
|
*
|
||||||
// open writer
|
* By default prints stack trace of all exceptions. Override this method to handle exceptions explicitly.<br><br>
|
||||||
OutputStream os = socket.getOutputStream();
|
*
|
||||||
DataOutputStream dos = new DataOutputStream(os);
|
* This method is not synchronized.
|
||||||
|
*
|
||||||
// open reader
|
* @param exception exception occurring while first instance is listening for subsequent instances
|
||||||
InputStream is = socket.getInputStream();
|
*/
|
||||||
DataInputStream dis = new DataInputStream(is);
|
protected void handleException(Exception exception) {
|
||||||
|
|
||||||
// write message to server
|
|
||||||
if (message == null) {
|
|
||||||
dos.writeInt(-1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
byte[] messageBytes = message.getBytes("UTF-8");
|
|
||||||
|
|
||||||
dos.writeInt(messageBytes.length);
|
|
||||||
dos.write(messageBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
dos.flush();
|
|
||||||
|
|
||||||
// read response length from server
|
|
||||||
int length = dis.readInt();
|
|
||||||
|
|
||||||
// read response string from server
|
|
||||||
String response = null;
|
|
||||||
if (length > -1) {
|
|
||||||
byte[] responseBytes = new byte[length];
|
|
||||||
int bytesRead = dis.read(responseBytes, 0, length);
|
|
||||||
response = new String(responseBytes, 0, bytesRead, "UTF-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
// close writer and reader
|
|
||||||
dos.close();
|
|
||||||
dis.close();
|
|
||||||
|
|
||||||
if (response.equals(APP_ID)) {
|
|
||||||
// validation successful
|
|
||||||
if (AUTO_EXIT) {
|
|
||||||
// perform pre-exit tasks
|
|
||||||
beforeExit();
|
|
||||||
// exit this instance
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// validation failed, this is the first instance
|
|
||||||
startServer();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InstanceException(e);
|
|
||||||
} finally {
|
|
||||||
// close socket
|
|
||||||
try {
|
|
||||||
if (socket != null) socket.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InstanceException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to get port from lock file
|
|
||||||
private int lockFile() throws InstanceException {
|
|
||||||
// lock file path
|
|
||||||
String filePath = TEMP_DIR + File.separator + APP_ID + ".lock";
|
|
||||||
File file = new File(filePath);
|
|
||||||
|
|
||||||
// try to get port from lock file
|
|
||||||
if (file.exists()) {
|
|
||||||
BufferedReader br = null;
|
|
||||||
try {
|
|
||||||
br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
|
|
||||||
return Integer.parseInt(br.readLine());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InstanceException(e);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
// do nothing
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (br != null) br.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InstanceException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to write port to lock file
|
|
||||||
private void lockFile(int port) throws InstanceException {
|
|
||||||
// lock file path
|
|
||||||
String filePath = TEMP_DIR + File.separator + APP_ID + ".lock";
|
|
||||||
File file = new File(filePath);
|
|
||||||
|
|
||||||
// try to write port to lock file
|
|
||||||
BufferedWriter bw = null;
|
|
||||||
try {
|
|
||||||
bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)));
|
|
||||||
bw.write(String.valueOf(port));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InstanceException(e);
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (bw != null) bw.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InstanceException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to obtain file lock
|
|
||||||
try {
|
|
||||||
lockRAF = new RandomAccessFile(file, "rw");
|
|
||||||
FileChannel fc = lockRAF.getChannel();
|
|
||||||
fileLock = fc.tryLock(0, Long.MAX_VALUE, true);
|
|
||||||
if (fileLock == null) {
|
|
||||||
throw new InstanceException("Failed to obtain file lock");
|
|
||||||
}
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
throw new InstanceException(e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InstanceException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Free the lock if possible. This is only required to be called from the first instance.
|
|
||||||
*
|
|
||||||
* @deprecated Use <code>freeLock()</code> instead.
|
|
||||||
* @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public void free() throws InstanceException {
|
|
||||||
freeLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Free the lock if possible. This is only required to be called from the first instance.
|
|
||||||
*
|
|
||||||
* @since 1.2
|
|
||||||
*
|
|
||||||
* @return true if able to release lock, false otherwise
|
|
||||||
* @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock
|
|
||||||
*/
|
|
||||||
public boolean freeLock() throws InstanceException {
|
|
||||||
try {
|
|
||||||
// close server socket
|
|
||||||
if (server != null) {
|
|
||||||
server.close();
|
|
||||||
|
|
||||||
// lock file path
|
|
||||||
String filePath = TEMP_DIR + File.separator + APP_ID + ".lock";
|
|
||||||
File file = new File(filePath);
|
|
||||||
|
|
||||||
// try to release file lock
|
|
||||||
if (fileLock != null) {
|
|
||||||
fileLock.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to close lock file RAF object
|
|
||||||
if (lockRAF != null) {
|
|
||||||
lockRAF.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to delete lock file
|
|
||||||
if (file.exists()) {
|
|
||||||
file.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InstanceException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method used in first instance to receive messages from subsequent instances.<br><br>
|
|
||||||
*
|
|
||||||
* This method is not synchronized.
|
|
||||||
*
|
|
||||||
* @param message message received by first instance from subsequent instances
|
|
||||||
*/
|
|
||||||
protected abstract void receiveMessage(String message);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method used in subsequent instances to send message to first instance.<br><br>
|
|
||||||
*
|
|
||||||
* It is not recommended to perform blocking (long running) tasks here. Use <code>beforeExit()</code> method instead.<br>
|
|
||||||
* One exception to this rule is if you intend to perform some user interaction before sending the message.<br><br>
|
|
||||||
*
|
|
||||||
* This method is not synchronized.
|
|
||||||
*
|
|
||||||
* @return message sent from subsequent instances
|
|
||||||
*/
|
|
||||||
protected abstract String sendMessage();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to receive and handle exceptions occurring while first instance is listening for subsequent instances.<br><br>
|
|
||||||
*
|
|
||||||
* By default prints stack trace of all exceptions. Override this method to handle exceptions explicitly.<br><br>
|
|
||||||
*
|
|
||||||
* This method is not synchronized.
|
|
||||||
*
|
|
||||||
* @param exception exception occurring while first instance is listening for subsequent instances
|
|
||||||
*/
|
|
||||||
protected void handleException(Exception exception) {
|
|
||||||
log.error("Error listening for instances", exception);
|
log.error("Error listening for instances", exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called before exiting from subsequent instances.<br><br>
|
* This method is called before exiting from subsequent instances.<br><br>
|
||||||
*
|
*
|
||||||
* Override this method to perform blocking tasks before exiting from subsequent instances.<br>
|
* Override this method to perform blocking tasks before exiting from subsequent instances.<br>
|
||||||
* This method is not invoked if auto exit is turned off.<br><br>
|
* This method is not invoked if auto exit is turned off.<br><br>
|
||||||
*
|
*
|
||||||
* This method is not synchronized.
|
* This method is not synchronized.
|
||||||
*
|
*/
|
||||||
* @since 1.2
|
protected void beforeExit() {}
|
||||||
*/
|
|
||||||
protected void beforeExit() {}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,88 +7,14 @@ import com.google.gson.JsonArray;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
/**
|
|
||||||
* The <code>InstanceList</code> class is a logical entry point to the library which extends the functionality of the <code>Instance</code> class.<br>
|
|
||||||
* It allows to create an application lock or free it and send and receive messages between first and subsequent instances.<br><br>
|
|
||||||
*
|
|
||||||
* This class is intended for passing a list of strings instead of a single string from the subsequent instance to the first instance.<br><br>
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* // unique application ID
|
|
||||||
* String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";
|
|
||||||
*
|
|
||||||
* // create Instance instance
|
|
||||||
* Instance unique = new InstanceList(APP_ID) {
|
|
||||||
* @Override
|
|
||||||
* protected List<String> sendMessageList() {
|
|
||||||
* List<String> messageList = new ArrayList<String>();
|
|
||||||
*
|
|
||||||
* messageList.add("Message 1");
|
|
||||||
* messageList.add("Message 2");
|
|
||||||
* messageList.add("Message 3");
|
|
||||||
* messageList.add("Message 4");
|
|
||||||
*
|
|
||||||
* return messageList;
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @Override
|
|
||||||
* protected void receiveMessageList(List<String> messageList) {
|
|
||||||
* for (String message : messageList) {
|
|
||||||
* System.out.println(message);
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* };
|
|
||||||
*
|
|
||||||
* // try to obtain lock
|
|
||||||
* try {
|
|
||||||
* unique.acquireLock();
|
|
||||||
* } catch (InstanceException e) {
|
|
||||||
* e.printStackTrace();
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* ...
|
|
||||||
*
|
|
||||||
* // try to free the lock before exiting program
|
|
||||||
* try {
|
|
||||||
* unique.freeLock();
|
|
||||||
* } catch (InstanceException e) {
|
|
||||||
* e.printStackTrace();
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @author Pratanu Mandal
|
|
||||||
* @since 1.3
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public abstract class InstanceList extends Instance {
|
public abstract class InstanceList extends Instance {
|
||||||
|
|
||||||
/**
|
public InstanceList(String applicationId) {
|
||||||
* Parameterized constructor.<br>
|
super(applicationId);
|
||||||
* This constructor configures to automatically exit the application for subsequent instances.<br><br>
|
|
||||||
*
|
|
||||||
* The APP_ID must be as unique as possible.
|
|
||||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
|
||||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
|
||||||
*
|
|
||||||
* @param APP_ID Unique string representing the application ID
|
|
||||||
*/
|
|
||||||
public InstanceList(String APP_ID) {
|
|
||||||
super(APP_ID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public InstanceList(String applicationId, boolean autoExit) {
|
||||||
* Parameterized constructor.<br>
|
super(applicationId, autoExit);
|
||||||
* This constructor allows to explicitly specify the exit strategy for subsequent instances.<br><br>
|
|
||||||
*
|
|
||||||
* The APP_ID must be as unique as possible.
|
|
||||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
|
||||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
|
||||||
*
|
|
||||||
* @param APP_ID Unique string representing the application ID
|
|
||||||
* @param AUTO_EXIT If true, automatically exit the application for subsequent instances
|
|
||||||
*/
|
|
||||||
public InstanceList(String APP_ID, boolean AUTO_EXIT) {
|
|
||||||
super(APP_ID, AUTO_EXIT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,16 +27,15 @@ public abstract class InstanceList extends Instance {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected final void receiveMessage(String message) {
|
protected final void receiveMessage(String message) {
|
||||||
if (message == null) {
|
if(message == null) {
|
||||||
receiveMessageList(null);
|
receiveMessageList(null);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// parse the JSON array string into an array of string arguments
|
// parse the JSON array string into an array of string arguments
|
||||||
JsonArray jsonArgs = JsonParser.parseString(message).getAsJsonArray();
|
JsonArray jsonArgs = JsonParser.parseString(message).getAsJsonArray();
|
||||||
|
|
||||||
List<String> stringArgs = new ArrayList<String>(jsonArgs.size());
|
List<String> stringArgs = new ArrayList<String>(jsonArgs.size());
|
||||||
|
|
||||||
for (int i = 0; i < jsonArgs.size(); i++) {
|
for(int i = 0; i < jsonArgs.size(); i++) {
|
||||||
JsonElement element = jsonArgs.get(i);
|
JsonElement element = jsonArgs.get(i);
|
||||||
stringArgs.add(element.getAsString());
|
stringArgs.add(element.getAsString());
|
||||||
}
|
}
|
||||||
|
@ -137,10 +62,11 @@ public abstract class InstanceList extends Instance {
|
||||||
JsonArray jsonArgs = new JsonArray();
|
JsonArray jsonArgs = new JsonArray();
|
||||||
|
|
||||||
List<String> stringArgs = sendMessageList();
|
List<String> stringArgs = sendMessageList();
|
||||||
|
if(stringArgs == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (stringArgs == null) return null;
|
for(String arg : stringArgs) {
|
||||||
|
|
||||||
for (String arg : stringArgs) {
|
|
||||||
jsonArgs.add(arg);
|
jsonArgs.add(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,5 +94,4 @@ public abstract class InstanceList extends Instance {
|
||||||
* @return list of messages sent from subsequent instances
|
* @return list of messages sent from subsequent instances
|
||||||
*/
|
*/
|
||||||
protected abstract List<String> sendMessageList();
|
protected abstract List<String> sendMessageList();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -535,7 +535,7 @@ public class Storage {
|
||||||
return certsDir;
|
return certsDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
static File getSparrowDir() {
|
public static File getSparrowDir() {
|
||||||
File sparrowDir;
|
File sparrowDir;
|
||||||
if(Network.get() != Network.MAINNET) {
|
if(Network.get() != Network.MAINNET) {
|
||||||
sparrowDir = new File(getSparrowHome(), Network.get().getName());
|
sparrowDir = new File(getSparrowHome(), Network.get().getName());
|
||||||
|
|
|
@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
@ -28,11 +29,14 @@ public class TorUtils {
|
||||||
} else {
|
} else {
|
||||||
HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1);
|
HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1);
|
||||||
try(Socket socket = new Socket(control.getHost(), control.getPort())) {
|
try(Socket socket = new Socket(control.getHost(), control.getPort())) {
|
||||||
|
socket.setSoTimeout(1500);
|
||||||
if(authenticate(socket)) {
|
if(authenticate(socket)) {
|
||||||
writeNewNym(socket);
|
writeNewNym(socket);
|
||||||
}
|
}
|
||||||
} catch(TorAuthenticationException e) {
|
} catch(TorAuthenticationException e) {
|
||||||
log.warn("Error authenticating to Tor at " + control + ", server returned " + e.getMessage());
|
log.warn("Error authenticating to Tor at " + control + ", server returned " + e.getMessage());
|
||||||
|
} catch(SocketTimeoutException e) {
|
||||||
|
log.warn("Timeout reading from " + control + ", is this a Tor ControlPort?");
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
log.warn("Error connecting to " + control + ", no Tor ControlPort configured?");
|
log.warn("Error connecting to " + control + ", no Tor ControlPort configured?");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue