Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reload adb on the fly #331

Merged
merged 16 commits into from
Dec 25, 2023
Merged
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
1 change: 1 addition & 0 deletions HISTORY
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
version 0.22
[+] Support parsing long logcat format
[+] Show warnings when dumpstate cannot be imported properly, but try to still be useful
[+] Changing the ADB configuration no longer requires restart

[*] Offline and not-fully-connected devices can no longer be selected in the dialog
[*] Initializing the ADB no longer freezes the UI. This is especially noticeable on Windows.
Expand Down
1 change: 1 addition & 0 deletions base/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies {
implementation(libs.guava)
implementation(libs.log4j)

testFixturesApi(libs.test.assertj)
testFixturesApi(libs.test.hamcrest.hamcrest)
testFixturesImplementation(libs.guava)
testFixturesImplementation(libs.test.mockito.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,7 @@ public LineReader(Reader input, int bufferSize) {
* @return the next line of the input or {@code null} if the input is exhausted.
* @throws IOException if reading the input fails
*/
@Nullable
public CharSequence readLine() throws IOException {
public @Nullable CharSequence readLine() throws IOException {
// Initially, this method was written to include EOLN in the result. It turned out to be unnecessary, but I
// keep some comments down the line on how to return this behavior.
consumeLfTailIfNeeded();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ private LazyInstance() {
*/
public static <T> LazyInstance<T> lazy(Supplier<? extends T> supplier) {
return new LazyInstance<>() {
@Nullable
private volatile T instance;
private volatile @Nullable T instance;

@Override
public T get() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,8 @@ private static Stream<Path> splitPathVariable(String pathVar) {
var iterator = new AbstractIterator<String>() {
private int pos;

@Nullable
@Override
protected String computeNext() {
protected @Nullable String computeNext() {
var result = new StringBuilder();
boolean inQuotes = false;
while (pos < pathVar.length()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2023 the Andlogview authors
*
* 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 name.mlopatkin.andlogview.utils.events;

import com.google.common.util.concurrent.MoreExecutors;

import java.util.concurrent.Executor;

/**
* Thread-safe version of {@link Observable}. Observers can be added or removed on any thread. Observers may be notified
* on any thread, even concurrently, though this depends on the embedder.
*
* @param <T> the type of the observer
*/
public interface ThreadSafeObservable<T> extends Observable<T> {
/**
* Adds an observer to be notified on the provided executor. Adding the same observer multiple times, even with
* different executors is an error.
*
* @param observer the observer
* @param executor the executor to run its callbacks on
* @throws IllegalArgumentException if the observer is already added
*/
void addObserver(T observer, Executor executor);

/**
* Adds an observer to be notified on the provided executor and returns a {@link ScopedObserver} instance that can
* be used to remove the added observer. Adding the same observer multiple times, even with different executors is
* an error.
*
* @param observer the observer
* @param executor the executor to run its callbacks on
* @return the scoped observer that removes the added observer upon {@link ScopedObserver#close()} call
* @throws IllegalArgumentException if the observer is already added
*/
default ScopedObserver addScopedObserver(T observer, Executor executor) {
addObserver(observer, executor);
return () -> removeObserver(observer);
}

/**
* {@inheritDoc}
* <p>
* Adding the same observer multiple times is an error. The thread on which the observer is going to be notified is
* not defined.
*
* @throws IllegalArgumentException if the observer is already added
*/
@Override
default void addObserver(T observer) {
addObserver(observer, MoreExecutors.directExecutor());
}

/**
* {@inheritDoc}
* <p>
* Adding the same observer multiple times is an error. The thread on which the observer is going to be notified is
* not defined.
*
* @throws IllegalArgumentException if the observer is already added
*/
@Override
default ScopedObserver addScopedObserver(T observer) {
return Observable.super.addScopedObserver(observer);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright 2023 the Andlogview authors
*
* 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 name.mlopatkin.andlogview.utils.events;

import name.mlopatkin.andlogview.thirdparty.observerlist.ObserverList;

import com.google.common.base.Preconditions;
import com.google.errorprone.annotations.concurrent.GuardedBy;

import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
* A thread-safe version of {@link Subject}. Unlike the latter, only supports internal iteration to provide the proper
* locking protocol.
* <p>
* It is safe to add or remove observers from any thread while iteration is ongoing, and to run several iterations
* concurrently. Added observers will only be notified on the next iteration cycle. Removed observers won't be notified
* in this cycle if removed on the same thread that performs the iteration. Otherwise, it is best-effort.
* <p>
* Memory consistency notes: registering an observer happens-before its first notification.
*
* @param <T> the type of the observer
*/
public class ThreadSafeSubject<T> {
// ObserverList is not thread-safe, but we're serializing all structural modifications and
@GuardedBy("observers")
private final ObserverList<ObserverEntry<T>> observers = new ObserverList<>();
@GuardedBy("observers")
private final Map<T, ObserverEntry<T>> entries = new HashMap<>();

private final ThreadSafeObservable<T> observableView = new ThreadSafeObservable<>() {
@Override
public void addObserver(T observer, Executor executor) {
synchronized (observers) {
Preconditions.checkArgument(!entries.containsKey(observer), "The observer %s is already registered",
observer);
var entry = new ObserverEntry<>(observer, executor);
entries.put(observer, entry);
observers.addObserver(entry);
}
}

@Override
public void removeObserver(@Nullable T observer) {
synchronized (observers) {
var observerEntry = entries.remove(observer);
if (observerEntry != null) {
observers.removeObserver(observerEntry);
// Prevent pending callbacks in the queue of the executor from running.
observerEntry.isRegistered = false;
}
}
}
};

/**
* Observable that can be passed to clients for subscribing. Note that it is impossible to cast returned observable
* back to Subject.
*
* @return the observable
*/
public ThreadSafeObservable<T> asObservable() {
return observableView;
}

public boolean isEmpty() {
synchronized (observers) {
return observers.isEmpty();
}
}

/**
* Invokes {@code consumer} for all registered observers. The consumer might be invoked on other threads, or stored
* in the queue of some executor. Beware of memory leaks and data races.
*
* @param consumer the consumer to perform notifications
*/
public void forEach(Consumer<? super T> consumer) {
final Iterator<ObserverEntry<T>> observerIterator;
synchronized (observers) {
observerIterator = observers.iterator();
}
while (next(observerIterator, consumer)) {
// intentionally empty
}
}

private boolean next(Iterator<ObserverEntry<T>> observerIterator, Consumer<? super T> consumer) {
final ObserverEntry<T> nextObserver;
synchronized (observers) {
if (!observerIterator.hasNext()) {
return false;
}
nextObserver = observerIterator.next();
}
nextObserver.executor.execute(() -> {
// By the time the executor gets to executing the notification, the observer might be unregistered.
// This check prevents spurious notification from happening.
if (nextObserver.isRegistered) {
consumer.accept(nextObserver.observer);
}
});
return true;
}

private static class ObserverEntry<T> {
final T observer;
final Executor executor;
volatile boolean isRegistered = true;

public ObserverEntry(T observer, Executor executor) {
this.observer = observer;
this.executor = executor;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2023 the Andlogview authors
*
* 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 name.mlopatkin.andlogview.utils.events;

import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import name.mlopatkin.andlogview.base.concurrent.TestExecutor;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.function.Consumer;

@ExtendWith(MockitoExtension.class)
class ThreadSafeSubjectTest {
@Mock
Consumer<String> mockObserver;

@Test
void unregisteredObserversGetNoNotifications() {
var executor = new TestExecutor();
var subject = createSubject();

Mockito.doAnswer(invocation -> {
subject.asObservable().removeObserver(mockObserver);
return null;
}).when(mockObserver).accept("first");

subject.asObservable().addObserver(mockObserver, executor);
subject.forEach(obs -> obs.accept("first"));
subject.forEach(obs -> obs.accept("second"));

executor.flush();

verify(mockObserver).accept("first");
verify(mockObserver, never()).accept("second");
}

private ThreadSafeSubject<Consumer<String>> createSubject() {
return new ThreadSafeSubject<>();
}
}
Loading
Loading