/*
* Copyright (c) 2015 OpenSilk Productions LLC
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package syncthing.api;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import retrofit.HttpException;
import rx.Observable;
import rx.Observer;
import rx.Scheduler;
import rx.Subscription;
import rx.android.schedulers.HandlerScheduler;
import rx.observers.Subscribers;
import syncthing.api.model.event.Event;
import syncthing.api.model.event.EventType;
import timber.log.Timber;
/**
* Created by drew on 3/2/15.
*/
public class EventMonitor {
public interface EventListener {
void handleEvent(Event e);
void onError(Error e);
}
public enum Error {
UNAUTHORIZED,
STOPPING,
DISCONNECTED,
UNKNOWN,
}
final SyncthingApi restApi;
final EventListener listener;
final Observable<Event> eventsObservable;
final Observer<Event> eventsObserver;
final AtomicLong lastEvent = new AtomicLong(0);
int unhandledErrorCount = 0;
int connectExceptionCount = 0;
Subscription eventSubscription;
boolean running;
HandlerThread handlerThread;
Scheduler scheduler;
public EventMonitor(SyncthingApi restApi, EventListener listener) {
this.restApi = restApi;
this.listener = listener;
this.eventsObservable = Observable.timer(1000, TimeUnit.MILLISECONDS)
.flatMap(ii -> getRestCall())
.flatMap(events -> {
if (events == null || events.length == 0) {
lastEvent.set(0);
return Observable.empty();
} else {
lastEvent.set(events[events.length - 1].id);
return Observable.from(events);
}
}).filter(event -> {
// drop unknown events
if (event.type == EventType.UNKNOWN) {
Timber.w("Dropping unknown event %s", event.data);
}
return (event.type != null && event.type != EventType.UNKNOWN);
//drop selected duplicate events like PING
}).lift(OperatorEventsDistinctUntilChanged.INSTANCE);
this.eventsObserver = new Observer<Event>() {
@Override
public void onCompleted() {
start();
}
@Override
public void onError(Throwable t) {
unhandledErrorCount--;//network errors handle themselves
if (t instanceof HttpException) {
HttpException e = (HttpException) t;
Timber.w("HttpException code=%d msg=%s", e.code(), e.message());
if (e.code() == 401) {
listener.onError(Error.UNAUTHORIZED);
} else {
listener.onError(Error.STOPPING);
}
return;
} else if (t instanceof SocketTimeoutException) {
//just means no events for long time, can safely ignore
Timber.w("SocketTimeout: %s", t.getMessage());
} else if (t instanceof java.io.InterruptedIOException) {
//just means socket timeout / Syncthing startup stuttering
Timber.w("InterruptedIOException: %s", t.getMessage());
} else if (t instanceof ConnectException) {
//We could either be offline or the server could be
//offline, or the server could still be booting
//or other stuff idk, so we retry for a while
//before giving up.
Timber.w("ConnectException: %s", t.getMessage());
if (++connectExceptionCount > 50) {
connectExceptionCount = 0;
Timber.w("Too many ConnectExceptions... server likely offline");
listener.onError(Error.STOPPING);
} else {
listener.onError(Error.DISCONNECTED);
resetCounter();//someone else could have restarted it
start(1200);
}
return;
} else {
Timber.e(t, "Unforeseen Exception: %s %s", t.getClass().getSimpleName(), t.getMessage());
unhandledErrorCount++;//undo decrement above
}
connectExceptionCount = 0;//Incase we just came out of a connecting loop.
if (++unhandledErrorCount < 20) {
start(1200);
} else {
//At this point we have no fucking clue what is going on
Timber.w("Too many errors suspending longpoll");
unhandledErrorCount = 0;
listener.onError(Error.STOPPING);
}
}
@Override
public void onNext(Event event) {
unhandledErrorCount = 0;
connectExceptionCount = 0;
listener.handleEvent(event);
}
};
}
public void start() {
start(500);
}
public synchronized void start(long delay) {
running = true;
Timber.d("start(%d) lastEvent=%d", delay, lastEvent.get());
if (handlerThread == null) {
handlerThread = new HandlerThread("EventMonitor");
handlerThread.start();
scheduler = HandlerScheduler.from(new Handler(handlerThread.getLooper()));
}
//TODO check connectivity and fail fast
eventSubscription = eventsObservable
.subscribeOn(scheduler)
.subscribe(Subscribers.from(eventsObserver));
}
private Observable<Event[]> getRestCall() {
if (lastEvent.get() == 0) {
//only pull the last event
return restApi.events(0, 1);
} else {
return restApi.events(lastEvent.get());
}
}
public synchronized void stop() {
running = false;
resetCounter();
if (eventSubscription != null) {
eventSubscription.unsubscribe();
eventSubscription = null;
}
if (handlerThread != null) {
Looper l = handlerThread.getLooper();
if (l != null) l.quit();
handlerThread = null;
scheduler = null;
}
}
public synchronized boolean isRunning() {
return running;
}
public void resetCounter() {
lastEvent.set(0);
}
}