2
0

Java improvements.

UI threading improvements.
Save user/site changes to file.
Ordering of user / site fixes.
Add questions to JSON output.
Bring JSON output format in line with C.
This commit is contained in:
Maarten Billemont
2018-07-09 01:13:25 -04:00
parent 529f1feace
commit 954c4f8d63
20 changed files with 446 additions and 151 deletions

View File

@@ -22,7 +22,6 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.Charsets;
import com.google.common.io.ByteSource;
import com.google.common.io.CharSource;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.TypeUtils;
import com.lyndir.masterpassword.gui.view.PasswordFrame;
@@ -51,6 +50,9 @@ public class GUI implements UnlockFrame.SignInCallback {
private PasswordFrame<?, ?> passwordFrame;
public static void main(final String... args) {
Thread.setDefaultUncaughtExceptionHandler(
(t, e) -> logger.err( e, "Uncaught: %s", e.getLocalizedMessage() ) );
if (Config.get().checkForUpdates())
checkUpdate();

View File

@@ -21,7 +21,6 @@ package com.lyndir.masterpassword.gui;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.Throwables;
import com.google.common.collect.Maps;
import com.google.common.io.Resources;
import com.google.common.util.concurrent.JdkFutureAdapters;
@@ -50,17 +49,19 @@ import org.jetbrains.annotations.NonNls;
@SuppressWarnings({ "HardcodedFileSeparator", "MethodReturnAlwaysConstant", "SpellCheckingInspection" })
public abstract class Res {
private static final int AVATAR_COUNT = 19;
private static final Map<Window, ScheduledExecutorService> executorByWindow = new WeakHashMap<>();
private static final Logger logger = Logger.get( Res.class );
private static final Colors colors = new Colors();
private static final int AVATAR_COUNT = 19;
private static final Map<Window, ScheduledExecutorService> jobExecutorByWindow = new WeakHashMap<>();
private static final Executor immediateUiExecutor = new SwingExecutorService( true );
private static final Executor laterUiExecutor = new SwingExecutorService( false );
private static final Logger logger = Logger.get( Res.class );
private static final Colors colors = new Colors();
public static Future<?> execute(final Window host, final Runnable job) {
return schedule( host, job, 0, TimeUnit.MILLISECONDS );
public static Future<?> job(final Window host, final Runnable job) {
return job( host, job, 0, TimeUnit.MILLISECONDS );
}
public static Future<?> schedule(final Window host, final Runnable job, final long delay, final TimeUnit timeUnit) {
return getExecutor( host ).schedule( () -> {
public static Future<?> job(final Window host, final Runnable job, final long delay, final TimeUnit timeUnit) {
return jobExecutor( host ).schedule( () -> {
try {
job.run();
}
@@ -70,33 +71,29 @@ public abstract class Res {
}, delay, timeUnit );
}
public static <V> ListenableFuture<V> execute(final Window host, final Callable<V> job) {
return schedule( host, job, 0, TimeUnit.MILLISECONDS );
public static <V> ListenableFuture<V> job(final Window host, final Callable<V> job) {
return job( host, job, 0, TimeUnit.MILLISECONDS );
}
public static <V> ListenableFuture<V> schedule(final Window host, final Callable<V> job, final long delay, final TimeUnit timeUnit) {
ScheduledExecutorService executor = getExecutor( host );
return JdkFutureAdapters.listenInPoolThread( executor.schedule( () -> {
try {
return job.call();
}
catch (final Throwable t) {
logger.err( t, "Unexpected: %s", t.getLocalizedMessage() );
throw Throwables.propagate( t );
}
}, delay, timeUnit ), executor );
public static <V> ListenableFuture<V> job(final Window host, final Callable<V> job, final long delay, final TimeUnit timeUnit) {
ScheduledExecutorService executor = jobExecutor( host );
return JdkFutureAdapters.listenInPoolThread( executor.schedule( job::call, delay, timeUnit ), executor );
}
private static ScheduledExecutorService getExecutor(final Window host) {
ScheduledExecutorService executor = executorByWindow.get( host );
public static Executor uiExecutor(final boolean immediate) {
return immediate? immediateUiExecutor: laterUiExecutor;
}
public static ScheduledExecutorService jobExecutor(final Window host) {
ScheduledExecutorService executor = jobExecutorByWindow.get( host );
if (executor == null) {
executorByWindow.put( host, executor = Executors.newSingleThreadScheduledExecutor() );
jobExecutorByWindow.put( host, executor = Executors.newSingleThreadScheduledExecutor() );
host.addWindowListener( new WindowAdapter() {
@Override
public void windowClosed(final WindowEvent e) {
ExecutorService executor = executorByWindow.remove( host );
ExecutorService executor = jobExecutorByWindow.remove( host );
if (executor != null)
executor.shutdownNow();
}
@@ -204,7 +201,7 @@ public abstract class Res {
font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( fontResourceName ).openStream() ) ) );
}
catch (final FontFormatException | IOException e) {
throw Throwables.propagate( e );
throw logger.bug( e );
}
return font;

View File

@@ -0,0 +1,91 @@
package com.lyndir.masterpassword.gui;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import com.google.common.collect.*;
import java.util.List;
import java.util.concurrent.*;
import javax.swing.*;
import org.jetbrains.annotations.NotNull;
/**
* @author lhunath, 2018-07-08
*/
public class SwingExecutorService extends AbstractExecutorService {
private final List<Runnable> pendingCommands = Lists.newLinkedList();
private final BlockingQueue<Boolean> terminated = Queues.newLinkedBlockingDeque( 1 );
private final boolean immediate;
private boolean shutdown;
/**
* @param immediate Allow immediate execution of the job in {@link #execute(Runnable)} if already on the right thread.
* If {@code false}, jobs are always posted for later execution on the event thread.
*/
public SwingExecutorService(final boolean immediate) {
this.immediate = immediate;
}
@Override
public void shutdown() {
shutdown = true;
synchronized (pendingCommands) {
if (pendingCommands.isEmpty())
terminated.offer( true );
}
}
@NotNull
@Override
public List<Runnable> shutdownNow() {
shutdown();
synchronized (pendingCommands) {
return ImmutableList.copyOf( pendingCommands );
}
}
@Override
public boolean isShutdown() {
return shutdown;
}
@Override
public boolean isTerminated() {
return ifNotNullElse( terminated.peek(), false );
}
@Override
public boolean awaitTermination(final long timeout, @NotNull final TimeUnit unit)
throws InterruptedException {
return ifNotNullElse( terminated.poll( timeout, unit ), false );
}
@Override
public void execute(@NotNull final Runnable command) {
if (shutdown)
throw new RejectedExecutionException( "Executor is shut down." );
synchronized (pendingCommands) {
pendingCommands.add( command );
}
if (immediate && SwingUtilities.isEventDispatchThread())
run( command );
else
SwingUtilities.invokeLater( () -> run( command ) );
}
private void run(final Runnable command) {
command.run();
synchronized (pendingCommands) {
pendingCommands.remove( command );
if (shutdown && pendingCommands.isEmpty())
terminated.offer( true );
}
}
}

View File

@@ -22,6 +22,7 @@ import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.MPAlgorithm;
import com.lyndir.masterpassword.MPResultType;
import com.lyndir.masterpassword.model.impl.MPBasicSite;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -44,6 +45,7 @@ public class MPIncognitoSite extends MPBasicSite<MPIncognitoQuestion> {
this.user = user;
}
@Nonnull
@Override
public MPIncognitoUser getUser() {
return user;

View File

@@ -0,0 +1,23 @@
package com.lyndir.masterpassword.gui.util;
import com.google.common.util.concurrent.FutureCallback;
import com.lyndir.lhunath.opal.system.logging.Logger;
/**
* @author lhunath, 2018-07-08
*/
public abstract class FailableCallback<T> implements FutureCallback<T> {
private final Logger logger;
protected FailableCallback(final Logger logger) {
this.logger = logger;
}
@Override
public void onFailure(final Throwable t) {
logger.err( t, "Future failed." );
onSuccess( null );
}
}

View File

@@ -23,11 +23,12 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.collect.Iterables;
import com.google.common.primitives.UnsignedInteger;
import com.google.common.util.concurrent.*;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.gui.Res;
import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.gui.util.UnsignedIntegerModel;
import com.lyndir.masterpassword.gui.util.*;
import com.lyndir.masterpassword.model.MPSite;
import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.impl.MPFileSite;
@@ -35,7 +36,7 @@ import com.lyndir.masterpassword.model.impl.MPFileUser;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.*;
import java.awt.event.WindowEvent;
import java.util.Collection;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
@@ -50,6 +51,8 @@ import javax.swing.event.DocumentListener;
*/
public abstract class PasswordFrame<U extends MPUser<S>, S extends MPSite<?>> extends JFrame implements DocumentListener {
private static final Logger logger = Logger.get( PasswordFrame.class );
@SuppressWarnings("FieldCanBeLocal")
private final Components.GradientPanel root;
private final JTextField siteNameField;
@@ -96,40 +99,33 @@ public abstract class PasswordFrame<U extends MPUser<S>, S extends MPSite<?>> ex
siteNameField = Components.textField(), Components.stud(),
siteActionButton = Components.button( "Add Site" ) );
siteNameField.getDocument().addDocumentListener( this );
siteNameField.addActionListener( new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
Futures.addCallback( updatePassword( true ), new FutureCallback<String>() {
siteNameField.addActionListener(
e -> Futures.addCallback( updatePassword( true ), new FailableCallback<String>( logger ) {
@Override
public void onSuccess(@Nullable final String sitePassword) {
if (sitePassword == null)
return;
if (currentSite instanceof MPFileSite)
((MPFileSite) currentSite).use();
Transferable clipboardContents = new StringSelection( sitePassword );
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
SwingUtilities.invokeLater( () -> {
passwordField.setText( null );
siteNameField.setText( null );
dispatchEvent( new WindowEvent( PasswordFrame.this, WindowEvent.WINDOW_CLOSING ) );
} );
dispatchEvent( new WindowEvent( PasswordFrame.this, WindowEvent.WINDOW_CLOSING ) );
}
}, Res.uiExecutor( false ) ) );
siteActionButton.addActionListener(
e -> {
if (currentSite == null)
return;
if (currentSite instanceof MPFileSite)
this.user.deleteSite( currentSite );
else
this.user.addSite( currentSite );
siteNameField.requestFocus();
@Override
public void onFailure(@Nonnull final Throwable t) {
}
updatePassword( true );
} );
}
} );
siteActionButton.addActionListener( e -> {
if (currentSite == null)
return;
if (currentSite instanceof MPFileSite)
this.user.deleteSite( currentSite );
else
this.user.addSite( currentSite );
siteNameField.requestFocus();
updatePassword( true );
} );
sitePanel.add( siteControls );
sitePanel.add( Components.stud() );
@@ -229,34 +225,36 @@ public abstract class PasswordFrame<U extends MPUser<S>, S extends MPSite<?>> ex
site.setCounter( siteCounter );
}
ListenableFuture<String> passwordFuture = Res.execute( this, () -> site.getResult( MPKeyPurpose.Authentication, null, null ) );
Futures.addCallback( passwordFuture, new FutureCallback<String>() {
@Override
public void onSuccess(@Nullable final String sitePassword) {
SwingUtilities.invokeLater( () -> {
updatingUI = true;
currentSite = site;
siteActionButton.setVisible( user instanceof MPFileUser );
if (currentSite instanceof MPFileSite)
siteActionButton.setText( "Delete Site" );
else
siteActionButton.setText( "Add Site" );
resultTypeField.setSelectedItem( currentSite.getResultType() );
siteVersionField.setSelectedItem( currentSite.getAlgorithm() );
siteCounterField.setValue( currentSite.getCounter() );
siteNameField.setText( currentSite.getName() );
if (siteNameField.getText().startsWith( siteNameQuery ))
siteNameField.select( siteNameQuery.length(), siteNameField.getText().length() );
ListenableFuture<String> passwordFuture = Res.job( this, () ->
site.getResult( MPKeyPurpose.Authentication, null, null ) );
SwingUtilities.invokeLater( () -> {
updatingUI = true;
currentSite = site;
siteActionButton.setVisible( user instanceof MPFileUser );
if (currentSite instanceof MPFileSite)
siteActionButton.setText( "Delete Site" );
else
siteActionButton.setText( "Add Site" );
resultTypeField.setSelectedItem( currentSite.getResultType() );
siteVersionField.setSelectedItem( currentSite.getAlgorithm().version() );
siteCounterField.setValue( currentSite.getCounter() );
siteNameField.setText( currentSite.getName() );
if (siteNameField.getText().startsWith( siteNameQuery ))
siteNameField.select( siteNameQuery.length(), siteNameField.getText().length() );
passwordField.setText( null );
tipLabel.setText( "Getting password..." );
Futures.addCallback( passwordFuture, new FailableCallback<String>( logger ) {
@Override
public void onSuccess(@Nullable final String sitePassword) {
if (sitePassword != null)
tipLabel.setText( "Press [Enter] to copy the password. Then paste it into the password field." );
passwordField.setText( sitePassword );
tipLabel.setText( "Press [Enter] to copy the password. Then paste it into the password field." );
updatingUI = false;
} );
}
@Override
public void onFailure(@Nonnull final Throwable t) {
}
}
}, Res.uiExecutor( true ) );
} );
return passwordFuture;

View File

@@ -153,7 +153,7 @@ public class UnlockFrame extends JFrame {
boolean checkSignIn() {
if (identiconFuture != null)
identiconFuture.cancel( false );
identiconFuture = Res.schedule( this, () -> SwingUtilities.invokeLater( () -> {
identiconFuture = Res.job( this, () -> SwingUtilities.invokeLater( () -> {
String fullName = (user == null)? "": user.getFullName();
char[] masterPassword = authenticationPanel.getMasterPassword();
@@ -186,7 +186,7 @@ public class UnlockFrame extends JFrame {
signInButton.setEnabled( false );
signInButton.setText( "Signing In..." );
Res.execute( this, () -> {
Res.job( this, () -> {
try {
user.authenticate( authenticationPanel.getMasterPassword() );