From be8b6017d504cdc791f9578ea196934e652b1af6 Mon Sep 17 00:00:00 2001 From: Samuel Holland Date: Sun, 7 Jan 2018 00:24:56 -0600 Subject: [PATCH] Make TunnelManager the point of asynchronicity Signed-off-by: Samuel Holland --- .../com/wireguard/android/Application.java | 10 +-- .../wireguard/android/backend/Backend.java | 42 ++++------ .../android/backend/WgQuickBackend.java | 60 +++++++------- .../android/configStore/ConfigStore.java | 29 +++---- .../android/configStore/FileConfigStore.java | 80 ++++++++----------- .../android/model/TunnelManager.java | 58 ++++++++------ 6 files changed, 125 insertions(+), 154 deletions(-) diff --git a/app/src/main/java/com/wireguard/android/Application.java b/app/src/main/java/com/wireguard/android/Application.java index c0fb046a..e8d0d7db 100644 --- a/app/src/main/java/com/wireguard/android/Application.java +++ b/app/src/main/java/com/wireguard/android/Application.java @@ -81,17 +81,15 @@ public class Application extends android.app.Application { @ApplicationScope @Provides - public static Backend getBackend(final AsyncWorker asyncWorker, - @ApplicationContext final Context context, + public static Backend getBackend(@ApplicationContext final Context context, final RootShell rootShell) { - return new WgQuickBackend(asyncWorker, context, rootShell); + return new WgQuickBackend(context, rootShell); } @ApplicationScope @Provides - public static ConfigStore getConfigStore(final AsyncWorker asyncWorker, - @ApplicationContext final Context context) { - return new FileConfigStore(asyncWorker, context); + public static ConfigStore getConfigStore(@ApplicationContext final Context context) { + return new FileConfigStore(context); } diff --git a/app/src/main/java/com/wireguard/android/backend/Backend.java b/app/src/main/java/com/wireguard/android/backend/Backend.java index fc174be1..fb71a3d3 100644 --- a/app/src/main/java/com/wireguard/android/backend/Backend.java +++ b/app/src/main/java/com/wireguard/android/backend/Backend.java @@ -7,61 +7,53 @@ import com.wireguard.config.Config; import java.util.Set; -import java9.util.concurrent.CompletionStage; - /** * Interface for implementations of the WireGuard secure network tunnel. */ public interface Backend { /** - * Update the volatile configuration of a running tunnel, asynchronously, and return the - * resulting configuration. If the tunnel is not up, return the configuration that would result - * (if known), or else simply return the given configuration. + * Update the volatile configuration of a running tunnel and return the resulting configuration. + * If the tunnel is not up, return the configuration that would result (if known), or else + * simply return the given configuration. * * @param tunnel The tunnel to apply the configuration to. * @param config The new configuration for this tunnel. - * @return A future completed when the configuration of the tunnel has been updated, and the new - * volatile configuration has been determined. This future will always be completed on the main - * thread. + * @return The updated configuration of the tunnel. */ - CompletionStage applyConfig(Tunnel tunnel, Config config); + Config applyConfig(Tunnel tunnel, Config config) throws Exception; /** * Enumerate the names of currently-running tunnels. * - * @return A future completed when the set of running tunnel names is available. This future - * will always be completed on the main thread. + * @return The set of running tunnel names. */ - CompletionStage> enumerate(); + Set enumerate() throws Exception; /** - * Get the actual state of a tunnel, asynchronously. + * Get the actual state of a tunnel. * * @param tunnel The tunnel to examine the state of. - * @return A future completed when the state of the tunnel has been determined. This future will - * always be completed on the main thread. + * @return The state of the tunnel. */ - CompletionStage getState(Tunnel tunnel); + State getState(Tunnel tunnel) throws Exception; /** - * Get statistics about traffic and errors on this tunnel, asynchronously. If the tunnel is not - * running, the statistics object will be filled with zero values. + * Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the + * statistics object will be filled with zero values. * * @param tunnel The tunnel to retrieve statistics for. - * @return A future completed when statistics for the tunnel are available. This future will - * always be completed on the main thread. + * @return The statistics for the tunnel. */ - CompletionStage getStatistics(Tunnel tunnel); + Statistics getStatistics(Tunnel tunnel) throws Exception; /** - * Set the state of a tunnel, asynchronously. + * Set the state of a tunnel. * * @param tunnel The tunnel to control the state of. * @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or * {@code TOGGLE}. - * @return A future completed when the state of the tunnel has changed, containing the new state - * of the tunnel. This future will always be completed on the main thread. + * @return The updated state of the tunnel. */ - CompletionStage setState(Tunnel tunnel, State state); + State setState(Tunnel tunnel, State state) throws Exception; } diff --git a/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java b/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java index 656cfa9c..6703f735 100644 --- a/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java +++ b/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java @@ -6,7 +6,6 @@ import android.util.Log; import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.model.Tunnel.Statistics; -import com.wireguard.android.util.AsyncWorker; import com.wireguard.android.util.RootShell; import com.wireguard.config.Config; @@ -17,25 +16,20 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; -import java9.util.concurrent.CompletableFuture; -import java9.util.concurrent.CompletionStage; import java9.util.stream.Collectors; import java9.util.stream.Stream; /** - * Created by samuel on 12/19/17. + * WireGuard backend that uses {@code wg-quick} to implement tunnel configuration. */ public final class WgQuickBackend implements Backend { private static final String TAG = WgQuickBackend.class.getSimpleName(); - private final AsyncWorker asyncWorker; private final Context context; private final RootShell rootShell; - public WgQuickBackend(final AsyncWorker asyncWorker, final Context context, - final RootShell rootShell) { - this.asyncWorker = asyncWorker; + public WgQuickBackend(final Context context, final RootShell rootShell) { this.context = context; this.rootShell = rootShell; } @@ -49,47 +43,47 @@ public final class WgQuickBackend implements Backend { } @Override - public CompletionStage applyConfig(final Tunnel tunnel, final Config config) { + public Config applyConfig(final Tunnel tunnel, final Config config) { if (tunnel.getState() == State.UP) - return CompletableFuture.failedFuture(new UnsupportedOperationException("stub")); - return CompletableFuture.completedFuture(config); + throw new UnsupportedOperationException("Not implemented"); + return config; } @Override - public CompletionStage> enumerate() { - return asyncWorker.supplyAsync(() -> { - final List output = new LinkedList<>(); - // Don't throw an exception here or nothing will show up in the UI. - if (rootShell.run(output, "wg show interfaces") != 0 || output.isEmpty()) - return Collections.emptySet(); - // wg puts all interface names on the same line. Split them into separate elements. - return Stream.of(output.get(0).split(" ")) - .collect(Collectors.toUnmodifiableSet()); - }); + public Set enumerate() { + final List output = new LinkedList<>(); + // Don't throw an exception here or nothing will show up in the UI. + if (rootShell.run(output, "wg show interfaces") != 0 || output.isEmpty()) + return Collections.emptySet(); + // wg puts all interface names on the same line. Split them into separate elements. + return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet()); } @Override - public CompletionStage getState(final Tunnel tunnel) { + public State getState(final Tunnel tunnel) { Log.v(TAG, "Requested state for tunnel " + tunnel.getName()); - return enumerate().thenApply(set -> set.contains(tunnel.getName()) ? State.UP : State.DOWN); + return enumerate().contains(tunnel.getName()) ? State.UP : State.DOWN; } @Override - public CompletionStage getStatistics(final Tunnel tunnel) { - return CompletableFuture.completedFuture(new Statistics()); + public Statistics getStatistics(final Tunnel tunnel) { + return new Statistics(); } @Override - public CompletionStage setState(final Tunnel tunnel, final State state) { + public State setState(final Tunnel tunnel, final State state) throws IOException { Log.v(TAG, "Requested state change to " + state + " for tunnel " + tunnel.getName()); - return tunnel.getStateAsync().thenCompose(currentState -> asyncWorker.supplyAsync(() -> { - final String stateName = resolveState(currentState, state).name().toLowerCase(); - final File file = new File(context.getFilesDir(), tunnel.getName() + ".conf"); - final String path = file.getAbsolutePath(); + final State originalState = getState(tunnel); + final State resolvedState = resolveState(originalState, state); + if (resolvedState == State.UP) { // FIXME: Assumes file layout from FileConfigStore. Use a temporary file. - if (rootShell.run(null, String.format("wg-quick %s '%s'", stateName, path)) != 0) + final File file = new File(context.getFilesDir(), tunnel.getName() + ".conf"); + if (rootShell.run(null, String.format("wg-quick up '%s'", file.getAbsolutePath())) != 0) throw new IOException("wg-quick failed"); - return tunnel; - })).thenCompose(this::getState); + } else { + if (rootShell.run(null, String.format("wg-quick down '%s'", tunnel.getName())) != 0) + throw new IOException("wg-quick failed"); + } + return getState(tunnel); } } diff --git a/app/src/main/java/com/wireguard/android/configStore/ConfigStore.java b/app/src/main/java/com/wireguard/android/configStore/ConfigStore.java index 19bb6bf5..c1f5b4e3 100644 --- a/app/src/main/java/com/wireguard/android/configStore/ConfigStore.java +++ b/app/src/main/java/com/wireguard/android/configStore/ConfigStore.java @@ -4,8 +4,6 @@ import com.wireguard.config.Config; import java.util.Set; -import java9.util.concurrent.CompletionStage; - /** * Interface for persistent storage providers for WireGuard configurations. */ @@ -17,39 +15,32 @@ public interface ConfigStore { * * @param name The name of the tunnel to create. * @param config Configuration for the new tunnel. - * @return A future completed when the tunnel and its configuration have been saved to - * persistent storage. This future encapsulates the configuration that was actually saved to - * persistent storage. This future will always be completed on the main thread. + * @return The configuration that was actually saved to persistent storage. */ - CompletionStage create(final String name, final Config config); + Config create(final String name, final Config config) throws Exception; /** * Delete a persistent tunnel. * * @param name The name of the tunnel to delete. - * @return A future completed when the tunnel and its configuration have been deleted. This - * future will always be completed on the main thread. */ - CompletionStage delete(final String name); + void delete(final String name) throws Exception; /** * Enumerate the names of tunnels present in persistent storage. * - * @return A future completed when the set of present tunnel names is available. This future - * will always be completed on the main thread. + * @return The set of present tunnel names. */ - CompletionStage> enumerate(); + Set enumerate() throws Exception; /** * Load the configuration for the tunnel given by {@code name}. * * @param name The identifier for the configuration in persistent storage (i.e. the name of the * tunnel). - * @return A future completed when an in-memory representation of the configuration is - * available. This future encapsulates the configuration loaded from persistent storage. This - * future will always be completed on the main thread. + * @return An in-memory representation of the configuration loaded from persistent storage. */ - CompletionStage load(final String name); + Config load(final String name) throws Exception; /** * Save the configuration for an existing tunnel given by {@code name}. @@ -57,9 +48,7 @@ public interface ConfigStore { * @param name The identifier for the configuration in persistent storage (i.e. the name of * the tunnel). * @param config An updated configuration object for the tunnel. - * @return A future completed when the configuration has been saved to persistent storage. This - * future encapsulates the configuration that was actually saved to persistent storage. This - * future will always be completed on the main thread. + * @return The configuration that was actually saved to persistent storage. */ - CompletionStage save(final String name, final Config config); + Config save(final String name, final Config config) throws Exception; } diff --git a/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java b/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java index 099bc0d3..30b2fba8 100644 --- a/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java +++ b/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java @@ -4,7 +4,6 @@ import android.content.Context; import android.util.Log; import com.wireguard.android.Application.ApplicationContext; -import com.wireguard.android.util.AsyncWorker; import com.wireguard.config.Config; import java.io.File; @@ -14,56 +13,48 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Set; -import java9.util.concurrent.CompletionStage; import java9.util.stream.Collectors; import java9.util.stream.Stream; /** - * Created by samuel on 12/28/17. + * Configuration store that uses a {@code wg-quick}-style file for each configured tunnel. */ public final class FileConfigStore implements ConfigStore { private static final String TAG = FileConfigStore.class.getSimpleName(); - private final AsyncWorker asyncWorker; private final Context context; - public FileConfigStore(final AsyncWorker asyncWorker, - @ApplicationContext final Context context) { - this.asyncWorker = asyncWorker; + public FileConfigStore(@ApplicationContext final Context context) { this.context = context; } @Override - public CompletionStage create(final String name, final Config config) { - return asyncWorker.supplyAsync(() -> { - final File file = fileFor(name); - if (!file.createNewFile()) { - final String message = "Configuration file " + file.getName() + " already exists"; - throw new IllegalStateException(message); - } - try (FileOutputStream stream = new FileOutputStream(file, false)) { - stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); - return config; - } - }); + public Config create(final String name, final Config config) throws IOException { + final File file = fileFor(name); + if (!file.createNewFile()) { + final String message = "Configuration file " + file.getName() + " already exists"; + throw new IllegalStateException(message); + } + try (FileOutputStream stream = new FileOutputStream(file, false)) { + stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); + return config; + } } @Override - public CompletionStage delete(final String name) { - return asyncWorker.runAsync(() -> { - final File file = fileFor(name); - if (!file.delete()) - throw new IOException("Cannot delete configuration file " + file.getName()); - }); + public void delete(final String name) throws IOException { + final File file = fileFor(name); + if (!file.delete()) + throw new IOException("Cannot delete configuration file " + file.getName()); } @Override - public CompletionStage> enumerate() { - return asyncWorker.supplyAsync(() -> Stream.of(context.fileList()) + public Set enumerate() { + return Stream.of(context.fileList()) .filter(name -> name.endsWith(".conf")) .map(name -> name.substring(0, name.length() - ".conf".length())) - .collect(Collectors.toUnmodifiableSet())); + .collect(Collectors.toUnmodifiableSet()); } private File fileFor(final String name) { @@ -71,28 +62,23 @@ public final class FileConfigStore implements ConfigStore { } @Override - public CompletionStage load(final String name) { - return asyncWorker.supplyAsync(() -> { - try (FileInputStream stream = new FileInputStream(fileFor(name))) { - return Config.from(stream); - } - }); + public Config load(final String name) throws IOException { + try (FileInputStream stream = new FileInputStream(fileFor(name))) { + return Config.from(stream); + } } @Override - public CompletionStage save(final String name, final Config config) { + public Config save(final String name, final Config config) throws IOException { Log.d(TAG, "Requested save config for tunnel " + name); - return asyncWorker.supplyAsync(() -> { - final File file = fileFor(name); - if (!file.isFile()) { - final String message = "Configuration file " + file.getName() + " not found"; - throw new IllegalStateException(message); - } - try (FileOutputStream stream = new FileOutputStream(file, false)) { - Log.d(TAG, "Writing out config for tunnel " + name); - stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); - return config; - } - }); + final File file = fileFor(name); + if (!file.isFile()) { + final String message = "Configuration file " + file.getName() + " not found"; + throw new IllegalStateException(message); + } + try (FileOutputStream stream = new FileOutputStream(file, false)) { + stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); + return config; + } } } diff --git a/app/src/main/java/com/wireguard/android/model/TunnelManager.java b/app/src/main/java/com/wireguard/android/model/TunnelManager.java index 22bf1190..e802c152 100644 --- a/app/src/main/java/com/wireguard/android/model/TunnelManager.java +++ b/app/src/main/java/com/wireguard/android/model/TunnelManager.java @@ -1,12 +1,14 @@ package com.wireguard.android.model; import android.content.SharedPreferences; +import android.support.annotation.NonNull; import com.wireguard.android.Application.ApplicationScope; import com.wireguard.android.backend.Backend; import com.wireguard.android.configStore.ConfigStore; import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.model.Tunnel.Statistics; +import com.wireguard.android.util.AsyncWorker; import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.util.ObservableKeyedList; import com.wireguard.android.util.ObservableSortedKeyedArrayList; @@ -38,6 +40,7 @@ public final class TunnelManager { private static final String KEY_RUNNING_TUNNELS = "enabled_configs"; private static final String TAG = TunnelManager.class.getSimpleName(); + private final AsyncWorker asyncWorker; private final Backend backend; private final ConfigStore configStore; private final SharedPreferences preferences; @@ -45,45 +48,55 @@ public final class TunnelManager { new ObservableSortedKeyedArrayList<>(COMPARATOR); @Inject - public TunnelManager(final Backend backend, final ConfigStore configStore, - final SharedPreferences preferences) { + public TunnelManager(final AsyncWorker asyncWorker, final Backend backend, + final ConfigStore configStore, final SharedPreferences preferences) { + this.asyncWorker = asyncWorker; this.backend = backend; this.configStore = configStore; this.preferences = preferences; } - private Tunnel add(final String name, final Config config, final State state) { + private Tunnel addToList(final String name, final Config config, final State state) { final Tunnel tunnel = new Tunnel(this, name, config, state); tunnels.add(tunnel); return tunnel; } - public CompletionStage create(final String name, final Config config) { + public CompletionStage create(@NonNull final String name, final Config config) { if (!Tunnel.isNameValid(name)) return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid name")); if (tunnels.containsKey(name)) { final String message = "Tunnel " + name + " already exists"; return CompletableFuture.failedFuture(new IllegalArgumentException(message)); } - return configStore.create(name, config).thenApply(cfg -> add(name, cfg, State.DOWN)); + return asyncWorker.supplyAsync(() -> configStore.create(name, config)) + .thenApply(savedConfig -> addToList(name, savedConfig, State.DOWN)); } CompletionStage delete(final Tunnel tunnel) { - return setTunnelState(tunnel, State.DOWN) - .thenCompose(x -> configStore.delete(tunnel.getName())) - .thenAccept(x -> remove(tunnel)); + return asyncWorker.runAsync(() -> { + backend.setState(tunnel, State.DOWN); + configStore.delete(tunnel.getName()); + }).thenAccept(x -> { + if (tunnel.getName().equals(preferences.getString(KEY_PRIMARY_TUNNEL, null))) + preferences.edit().remove(KEY_PRIMARY_TUNNEL).apply(); + tunnels.remove(tunnel); + }); } CompletionStage getTunnelConfig(final Tunnel tunnel) { - return configStore.load(tunnel.getName()).thenApply(tunnel::onConfigChanged); + return asyncWorker.supplyAsync(() -> configStore.load(tunnel.getName())) + .thenApply(tunnel::onConfigChanged); } CompletionStage getTunnelState(final Tunnel tunnel) { - return backend.getState(tunnel).thenApply(tunnel::onStateChanged); + return asyncWorker.supplyAsync(() -> backend.getState(tunnel)) + .thenApply(tunnel::onStateChanged); } CompletionStage getTunnelStatistics(final Tunnel tunnel) { - return backend.getStatistics(tunnel).thenApply(tunnel::onStatisticsChanged); + return asyncWorker.supplyAsync(() -> backend.getStatistics(tunnel)) + .thenApply(tunnel::onStatisticsChanged); } public ObservableKeyedList getTunnels() { @@ -91,16 +104,14 @@ public final class TunnelManager { } public void onCreate() { - configStore.enumerate().thenAcceptBoth(backend.enumerate(), (names, running) -> { - for (final String name : names) - add(name, null, running.contains(name) ? State.UP : State.DOWN); - }).whenComplete(ExceptionLoggers.E); + asyncWorker.supplyAsync(configStore::enumerate) + .thenAcceptBoth(asyncWorker.supplyAsync(backend::enumerate), this::onTunnelsLoaded) + .whenComplete(ExceptionLoggers.E); } - private void remove(final Tunnel tunnel) { - if (tunnel.getName().equals(preferences.getString(KEY_PRIMARY_TUNNEL, null))) - preferences.edit().remove(KEY_PRIMARY_TUNNEL).apply(); - tunnels.remove(tunnel); + private void onTunnelsLoaded(final Set present, final Set running) { + for (final String name : present) + addToList(name, null, running.contains(name) ? State.UP : State.DOWN); } public CompletionStage restoreState() { @@ -125,13 +136,14 @@ public final class TunnelManager { } CompletionStage setTunnelConfig(final Tunnel tunnel, final Config config) { - return backend.applyConfig(tunnel, config) - .thenCompose(cfg -> configStore.save(tunnel.getName(), cfg)) - .thenApply(tunnel::onConfigChanged); + return asyncWorker.supplyAsync(() -> { + final Config appliedConfig = backend.applyConfig(tunnel, config); + return configStore.save(tunnel.getName(), appliedConfig); + }).thenApply(tunnel::onConfigChanged); } CompletionStage setTunnelState(final Tunnel tunnel, final State state) { - return backend.setState(tunnel, state) + return asyncWorker.supplyAsync(() -> backend.setState(tunnel, state)) .thenApply(tunnel::onStateChanged); } }