Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions kse/src/main/java/org/kse/InstanceManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/*
* Copyright 2004 - 2013 Wayne Grant
* 2013 - 2026 Kai Kramer
*
* This file is part of KeyStore Explorer.
*
* KeyStore Explorer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeyStore Explorer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeyStore Explorer. If not, see <http://www.gnu.org/licenses/>.
*/

package org.kse;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.StandardProtocolFamily;
import java.net.UnixDomainSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.SwingUtilities;

import org.kse.gui.KseFrame;
import org.kse.gui.dnd.DroppedFileHandler;
import org.kse.gui.error.DError;

/**
* A Singleton for managing the existing instance infrastructure. The existing
* instance is identified using unix domain sockets/named pipes. The first instance
* opens the socket/named pipe. Other instances of KSE will detect the existence
* of the socket/pipe and notify the existing instance of the files to be opened.
*/
public enum InstanceManager {

/**
* Singleton instance
*/
INSTANCE;

private static final String SOCKET_FILENAME = "kse-ipc.sock";

private boolean isBound;
private ServerSocketChannel serverChannel;
private Thread listenerThread;

/**
* Attempt to become the primary instance of KSE if the feature is enabled.
*/
public void tryBecomePrimary() {
Path socketPath = getSocketPath();

try {
serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
UnixDomainSocketAddress addr = UnixDomainSocketAddress.of(socketPath);
serverChannel.bind(addr);
isBound = true;
registerShutdownHooks();
return; // is primary instance
} catch (IOException e) {
// Bind failed - another instance is running or file is stale
}

if (canConnectToPrimary(socketPath)) {
return; // primary instance exists
}

// Clean up stale socket file if needed
try {
Files.deleteIfExists(socketPath);
} catch (IOException ignored) {
}

try {
serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
UnixDomainSocketAddress addr = UnixDomainSocketAddress.of(socketPath);
serverChannel.bind(addr);
isBound = true;
registerShutdownHooks();
return; // is primary after stale file cleanup
} catch (IOException e) {
// unknown state - cannot connect to primary instance and cannot bind to socket
displayError(e);
}
}

/**
* Registers the main KseFrame with the single instance manager. Needed for opening files.
*
* @param kseFrame The KseFrame.
*/
public void register(KseFrame kseFrame) {
// Only start the listener thread if it is bound to the socket.
if (!isBound) {
return;
}

listenerThread = new Thread(() -> listenLoop(kseFrame), "kse-ipc-listener");
listenerThread.setDaemon(true);
listenerThread.start();
}

/**
* Sends the files to open to the primary instance.
*
* @param parameterFiles The list of files to send.
* @throws IOException If an error occurred when sending the list of files.
*/
public void sendToPrimary(List<File> parameterFiles) throws IOException {
UnixDomainSocketAddress addr = UnixDomainSocketAddress.of(getSocketPath());

try (SocketChannel ch = SocketChannel.open(StandardProtocolFamily.UNIX)) {
ch.connect(addr);
writeParameterFiles(ch, parameterFiles);
}
}

private boolean canConnectToPrimary(Path socketPath) {
try (SocketChannel ch = SocketChannel.open(StandardProtocolFamily.UNIX)) {
UnixDomainSocketAddress addr = UnixDomainSocketAddress.of(socketPath);
ch.connect(addr);
return true;
} catch (IOException e) {
return false;
}
}

private void listenLoop(KseFrame kseFrame) {
try {
while (true) {
SocketChannel client = serverChannel.accept();
handleRequest(client, kseFrame);
}
} catch (ClosedChannelException e) {
// Ignore. The socket is being shutdown.
} catch (IOException e) {
// Being unable to read from a domain socket should be a rare and
// truly exceptional condition.
displayError(e);
}
}

private void handleRequest(SocketChannel client, KseFrame kseFrame) {
try (client) {
List<File> parameterFiles = readParameterFiles(client);
if (!parameterFiles.isEmpty()) {
SwingUtilities.invokeLater(() -> DroppedFileHandler.openFiles(kseFrame, parameterFiles));
}
} catch (IOException e) {
// Ignore. There is nothing the user can do about this.
}
}

private void writeParameterFiles(WritableByteChannel ch, List<File> files) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);

oos.writeObject(files.toArray(File[]::new));
oos.flush();

ByteBuffer buf = ByteBuffer.wrap(baos.toByteArray());
while (buf.hasRemaining()) {
ch.write(buf);
}
}

private List<File> readParameterFiles(ReadableByteChannel ch) throws IOException {
try (ObjectInputStream ois = new ObjectInputStream(Channels.newInputStream(ch))) {
Object incomingObject = ois.readObject();
if (incomingObject instanceof File[]) {
return Arrays.asList((File[]) incomingObject);
}
} catch (ClassNotFoundException e) {
// Ignore. File[] is always be available.
}
return Collections.EMPTY_LIST;
}

private Path getSocketPath() {
return Path.of(System.getProperty("java.io.tmpdir"), SOCKET_FILENAME);
}

private void displayError(Exception e) {
SwingUtilities.invokeLater(() -> DError.displayError((JFrame) null, e));
}

/**
* Closes the socket and cleans up.
*/
public void shutdown() {
try {
if (serverChannel != null && serverChannel.isOpen()) {
// This kills the listener thread with an AsynchronousCloseException
serverChannel.close();
}
isBound = false;
} catch (IOException ignored) {
}

try {
Files.deleteIfExists(getSocketPath());
} catch (IOException ignored) {
}
}

private void registerShutdownHooks() {
Runtime.getRuntime().addShutdownHook(new Thread(INSTANCE::shutdown, "kse-ipc-shutdown"));
}
}
14 changes: 14 additions & 0 deletions kse/src/main/java/org/kse/KSE.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package org.kse;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Paths;
Expand Down Expand Up @@ -134,6 +135,19 @@ public static void main(String[] args) {
}
}

if (!parameterFiles.isEmpty() && preferences.isOpenWithExistingInstance()) {
// Attempt to notify existing instance of files
try {
InstanceManager.INSTANCE.sendToPrimary(parameterFiles);
return;
} catch (IOException e) {
// Ignore. An existing instance does not exist so continue to load.
}
}

// Attempt to become the primary instance
InstanceManager.INSTANCE.tryBecomePrimary();

SwingUtilities.invokeLater(new CreateApplicationGui(preferences, parameterFiles));
} catch (Throwable t) {
DError dError = new DError(new JFrame(), t);
Expand Down
3 changes: 3 additions & 0 deletions kse/src/main/java/org/kse/gui/CreateApplicationGui.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

import org.kse.AuthorityCertificates;
import org.kse.KSE;
import org.kse.InstanceManager;
import org.kse.gui.actions.CheckUpdateAction;
import org.kse.gui.dnd.DroppedFileHandler;
import org.kse.gui.error.DError;
Expand Down Expand Up @@ -91,6 +92,8 @@ public void run() {
// check if stored location of cacerts file still exists
checkCaCerts(kseFrame);

InstanceManager.INSTANCE.register(kseFrame);

// open file list passed via command line params (basically same as if files were dropped on application)
DroppedFileHandler.openFiles(kseFrame, parameterFiles);

Expand Down
3 changes: 3 additions & 0 deletions kse/src/main/java/org/kse/gui/actions/ExitAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import javax.swing.ImageIcon;
import javax.swing.KeyStroke;

import org.kse.InstanceManager;
import org.kse.gui.CurrentDirectory;
import org.kse.gui.KseFrame;
import org.kse.gui.KseRestart;
Expand Down Expand Up @@ -97,6 +98,8 @@ public void exitApplication(boolean restart) {
KeyStoreExplorerAction.savePasswordManagerWithProgress(kseFrame.getUnderlyingFrame());
}

InstanceManager.INSTANCE.shutdown();

if (restart) {
KseRestart.restart();
}
Expand Down
2 changes: 2 additions & 0 deletions kse/src/main/java/org/kse/gui/actions/PreferencesAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import javax.swing.UIManager;

import org.kse.AuthorityCertificates;
import org.kse.InstanceManager;
import org.kse.crypto.csr.pkcs12.Pkcs12Util;
import org.kse.gui.KseFrame;
import org.kse.gui.preferences.DPreferences;
Expand Down Expand Up @@ -109,6 +110,7 @@ public void showPreferences() {
preferences.setSerialNumberLengthInBytes(dPreferences.getSerialNumberLengthInBytes());
preferences.setAutomaticallyReload(dPreferences.isAutomaticReloadEnabled());
preferences.setSilentlyReload(dPreferences.isSilentReloadEnabled());
preferences.setOpenWithExistingInstance(dPreferences.isSingleInstanceEnabled());

preferences.setPkcs12EncryptionSetting(dPreferences.getPkcs12EncryptionSetting());
Pkcs12Util.setEncryptionStrength(preferences.getPkcs12EncryptionSetting());
Expand Down
4 changes: 4 additions & 0 deletions kse/src/main/java/org/kse/gui/preferences/DPreferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,10 @@ public boolean isSilentReloadEnabled() {
return panelUserInterface.getJcbSilentlyReload().isSelected();
}

public boolean isSingleInstanceEnabled() {
return panelUserInterface.getJcbSingleInstance().isSelected();
}

/**
* Read the new default DN (RDNs can be empty here)
*
Expand Down
13 changes: 11 additions & 2 deletions kse/src/main/java/org/kse/gui/preferences/PanelUserInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class PanelUserInterface {
private JSpinner jspSnRandomBytes;
private JCheckBox jcbEnableAutomaticReload;
private JCheckBox jcbEnableSilentReload;
private JCheckBox jcbEnableOpenWithExistingInstance;

private JCheckBox jcbEnableAutoUpdateChecks;
private JSpinner jspAutoUpdateCheckInterval;
Expand Down Expand Up @@ -167,7 +168,10 @@ JPanel initUserInterfaceCard() {
jspSnRandomBytes.setToolTipText(res.getString("DPreferences.jlSnRandomBytes.tooltip"));
JLabel jlSnRandomBytesPostfix = new JLabel(res.getString("DPreferences.jlSnRandomBytesPostfix.text"));

JLabel jlAutoReload = new JLabel(res.getString("DPreferences.jlAutoReload.text"));
JLabel jlKeyStores = new JLabel(res.getString("DPreferences.jlKeyStores.text"));
jcbEnableOpenWithExistingInstance = new JCheckBox(res.getString("DPreferences.jcbEnableOpenWithExistingInstance.text"));
jcbEnableOpenWithExistingInstance.setToolTipText(res.getString("DPreferences.jcbEnableOpenWithExistingInstance.tooltip"));
jcbEnableOpenWithExistingInstance.setSelected(preferences.isOpenWithExistingInstance());
jcbEnableAutomaticReload = new JCheckBox(res.getString("DPreferences.jcbEnableAutomaticReload.text"));
jcbEnableAutomaticReload.setToolTipText(res.getString("DPreferences.jcbEnableAutomaticReload.tooltip"));
jcbEnableSilentReload = new JCheckBox(res.getString("DPreferences.jcbEnableSilentReload.text"));
Expand Down Expand Up @@ -203,7 +207,8 @@ JPanel initUserInterfaceCard() {
MiGUtil.addSeparator(jpUI, jlSnRandomBytes.getText());
jpUI.add(jspSnRandomBytes, "gapx indent, split 2");
jpUI.add(jlSnRandomBytesPostfix, "wrap");
MiGUtil.addSeparator(jpUI, jlAutoReload.getText());
MiGUtil.addSeparator(jpUI, jlKeyStores.getText());
jpUI.add(jcbEnableOpenWithExistingInstance, "gapx indent, wrap");
jpUI.add(jcbEnableAutomaticReload, "gapx indent, wrap");
jpUI.add(jcbEnableSilentReload, "gapx indent, wrap");

Expand Down Expand Up @@ -336,4 +341,8 @@ public JCheckBox getJcbAutomaticReload() {
public JCheckBox getJcbSilentlyReload() {
return jcbEnableSilentReload;
}

public JCheckBox getJcbSingleInstance() {
return jcbEnableOpenWithExistingInstance;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public class KsePreferences {
private Map<String, SignatureType> signatureTypes = new HashMap<>();
private boolean automaticallyReload = false;
private boolean silentlyReload = false;
private boolean openWithExistingInstance = true;

// auto-generated getters/setters

Expand Down Expand Up @@ -402,4 +403,12 @@ public void setSilentlyReload(boolean silentlyReload) {
public boolean isSilentlyReload() {
return silentlyReload;
}

public boolean isOpenWithExistingInstance() {
return openWithExistingInstance;
}

public void setOpenWithExistingInstance(boolean openWithExistingInstance) {
this.openWithExistingInstance = openWithExistingInstance;
}
}
Loading