Plugin disable only after no ongoing v8 calls to reduce crashes, errors of placeholder loaders now visible, cancel retry on home now removes loader

This commit is contained in:
Kelvin 2023-10-16 22:04:19 +02:00
parent 9ffdf39f13
commit 374d9950be
17 changed files with 225 additions and 51 deletions

View file

@ -58,7 +58,7 @@ fun findNonRuntimeException(ex: Throwable?): Throwable? {
fun warnIfMainThread(context: String) {
if(BuildConfig.DEBUG && Looper.myLooper() == Looper.getMainLooper())
Logger.w(V8Plugin.TAG, "JAVASCRIPT ON MAIN THREAD\nAt: ${context}\n" + Thread.currentThread().stackTrace);
Logger.w(V8Plugin.TAG, "JAVASCRIPT ON MAIN THREAD\nAt: ${context}\n" + Thread.currentThread().stackTrace.joinToString { it.toString() });
}
fun ensureNotMainThread() {

View file

@ -4,7 +4,7 @@ import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import java.time.OffsetDateTime
class PlatformContentPlaceholder(pluginId: String): IPlatformContent {
class PlatformContentPlaceholder(pluginId: String, exception: Throwable? = null): IPlatformContent {
override val contentType: ContentType = ContentType.PLACEHOLDER;
override val id: PlatformID = PlatformID("", null, pluginId);
override val name: String = "";
@ -12,4 +12,5 @@ class PlatformContentPlaceholder(pluginId: String): IPlatformContent {
override val shareUrl: String = "";
override val datetime: OffsetDateTime? = null;
override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("", pluginId), "", "", null, null);
val error: Throwable? = exception
}

View file

@ -26,6 +26,8 @@ import com.futo.platformplayer.api.media.platforms.js.models.*
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.PluginEngineException
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
import com.futo.platformplayer.logging.Logger
@ -289,7 +291,8 @@ open class JSClient : IPlatformClient {
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isChannelUrl", ex);
if(ex !is PluginEngineException)
announcePluginUnhandledException("isChannelUrl", ex);
return false;
}
}
@ -565,7 +568,7 @@ open class JSClient : IPlatformClient {
StateAnnouncement.instance.registerAnnouncement("PluginUnhandled_${config.id}_${method}",
"Plugin ${config.name} encountered an error in [${method}]",
"${ex.message}\nPlease contact the plugin developer",
AnnouncementType.RECURRING,
AnnouncementType.SESSION_RECURRING,
OffsetDateTime.now());
}
catch(_: Throwable) {}

View file

@ -1,5 +1,6 @@
package com.futo.platformplayer.api.media.structures
import com.futo.platformplayer.api.media.models.contents.PlatformContentPlaceholder
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
@ -37,8 +38,12 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
synchronized(_pending) {
_pending.remove(pendingPager);
}
if(error != null)
if(error != null) {
onPagerError.emit(error);
val replacing = _placeHolderPagersPaired[pendingPager];
if(replacing != null)
updatePager(null, replacing, error);
}
else
updatePager(pendingPager.getCompleted());
}
@ -60,9 +65,17 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
override fun nextPage() = synchronized(_pagersReusable){ _currentPager.nextPage() };
override fun getResults(): List<T> = synchronized(_pagersReusable){ _currentPager.getResults() };
private fun updatePager(pagerToAdd: IPager<T>?) {
if(pagerToAdd == null)
private fun updatePager(pagerToAdd: IPager<T>?, toReplacePager: IPager<T>? = null, error: Throwable? = null) {
if(pagerToAdd == null) {
if(toReplacePager != null && toReplacePager is PlaceholderPager && error != null) {
val pluginId = toReplacePager.placeholderFactory.invoke().id?.pluginId ?: "";
_currentPager = PlaceholderPager(5) {
return@PlaceholderPager PlatformContentPlaceholder(pluginId, error)
} as IPager<T>;
onPagerChanged.emit(_currentPager);
}
return;
}
synchronized(_pagersReusable) {
Logger.i("RefreshMultiDistributionContentPager", "Received new pager for RefreshPager")
_pagersReusable.add(pagerToAdd.asReusable());

View file

@ -6,11 +6,11 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
* A placeholder pager simply generates PlatformContent by some creator function.
*/
class PlaceholderPager : IPager<IPlatformContent> {
private val _creator: ()->IPlatformContent;
val placeholderFactory: ()->IPlatformContent;
private val _pageSize: Int;
constructor(pageSize: Int, placeholderCreator: ()->IPlatformContent) {
_creator = placeholderCreator;
placeholderFactory = placeholderCreator;
_pageSize = pageSize;
}
@ -18,7 +18,7 @@ class PlaceholderPager : IPager<IPlatformContent> {
override fun getResults(): List<IPlatformContent> {
val pages = ArrayList<IPlatformContent>();
for(item in 1.._pageSize)
pages.add(_creator());
pages.add(placeholderFactory());
return pages;
}
override fun hasMorePages(): Boolean = true;

View file

@ -8,6 +8,10 @@ abstract class EventBase<Handler, ConditionalHandler>: IEvent {
protected val _conditionalListeners = mutableListOf<TaggedHandler<ConditionalHandler>>();
protected val _listeners = mutableListOf<TaggedHandler<Handler>>();
fun hasListeners(): Boolean =
synchronized(_listeners){_listeners.isNotEmpty()} ||
synchronized(_conditionalListeners){_conditionalListeners.isNotEmpty()};
fun subscribeConditional(listener: ConditionalHandler) {
synchronized(_conditionalListeners) {
_conditionalListeners.add(TaggedHandler(listener));
@ -65,10 +69,7 @@ abstract class EventBase<Handler, ConditionalHandler>: IEvent {
class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
fun emit() : Boolean {
var handled: Boolean;
synchronized(_listeners) {
handled = _listeners.isNotEmpty();
}
var handled = false;
synchronized(_conditionalListeners) {
for (conditional in _conditionalListeners)
@ -76,6 +77,7 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
}
synchronized(_listeners) {
handled = handled || _listeners.isNotEmpty();
for (handler in _listeners)
handler.handler.invoke();
}
@ -85,17 +87,14 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
}
class Event1<T1>() : EventBase<((T1)->Unit), ((T1)->Boolean)>() {
fun emit(value : T1): Boolean {
var handled: Boolean;
synchronized(_listeners) {
handled = _listeners.isNotEmpty();
}
var handled = false;
synchronized(_conditionalListeners) {
for (conditional in _conditionalListeners)
handled = handled || conditional.handler.invoke(value);
}
synchronized(_listeners) {
handled = handled || _listeners.isNotEmpty();
for (handler in _listeners)
handler.handler.invoke(value);
}
@ -105,10 +104,7 @@ class Event1<T1>() : EventBase<((T1)->Unit), ((T1)->Boolean)>() {
}
class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
fun emit(value1 : T1, value2 : T2): Boolean {
var handled: Boolean;
synchronized(_listeners) {
handled = _listeners.isNotEmpty();
}
var handled = false;
synchronized(_conditionalListeners) {
for (conditional in _conditionalListeners)
@ -116,6 +112,7 @@ class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
}
synchronized(_listeners) {
handled = handled || _listeners.isNotEmpty();
for (handler in _listeners)
handler.handler.invoke(value1, value2);
}
@ -126,10 +123,7 @@ class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
class Event3<T1, T2, T3>() : EventBase<((T1, T2, T3)->Unit), ((T1, T2, T3)->Boolean)>() {
fun emit(value1 : T1, value2 : T2, value3 : T3): Boolean {
var handled: Boolean;
synchronized(_listeners) {
handled = _listeners.isNotEmpty();
}
var handled = false;
synchronized(_conditionalListeners) {
for (conditional in _conditionalListeners)
@ -137,6 +131,7 @@ class Event3<T1, T2, T3>() : EventBase<((T1, T2, T3)->Unit), ((T1, T2, T3)->Bool
}
synchronized(_listeners) {
handled = handled || _listeners.isNotEmpty();
for (handler in _listeners)
handler.handler.invoke(value1, value2, value3);
}

View file

@ -57,6 +57,7 @@ class TaskHandler<TParameter, TResult> {
fun run(parameter: TParameter) {
val id = ++_idGenerator;
var handled = false;
_scope().launch(_dispatcher) {
if (id != _idGenerator)
return@launch;
@ -67,24 +68,31 @@ class TaskHandler<TParameter, TResult> {
return@launch;
withContext(Dispatchers.Main) {
if (id != _idGenerator)
if (id != _idGenerator) {
handled = true;
return@withContext;
}
try {
onSuccess.emit(result);
handled = true;
}
catch (e: Throwable) {
Logger.w(TAG, "Handled exception in TaskHandler onSuccess.", e);
onError.emit(e, parameter);
handled = true;
}
}
}
catch (e: Throwable) {
Log.i("TaskHandler", "TaskHandler.run in exception: " + e.message);
if (id != _idGenerator)
if (id != _idGenerator) {
handled = true;
return@launch;
}
withContext(Dispatchers.Main) {
handled = true;
if (id != _idGenerator)
return@withContext;
@ -95,7 +103,18 @@ class TaskHandler<TParameter, TResult> {
}
}
}
}
}/*.invokeOnCompletion { //Commented for now, because it doesn't fix the bug it was intended to fix, but might want it later anyway
if(!handled) {
if(it is CancellationException) {
Logger.w(TAG, "Detected unhandled TaskHandler due to cancellation, forwarding cancellation");
onError.emit(it, parameter);
}
else {
//TODO: Forward exception?
Logger.w(TAG, "Detected unhandled TaskHandler due to [${it}]", it);
}
}
}*/
}
@Synchronized

View file

@ -1,7 +1,6 @@
package com.futo.platformplayer.engine
import android.content.Context
import android.os.Looper
import com.caoccao.javet.exceptions.JavetCompilationException
import com.caoccao.javet.exceptions.JavetExecutionException
import com.caoccao.javet.interop.V8Host
@ -18,9 +17,7 @@ import com.futo.platformplayer.engine.exceptions.*
import com.futo.platformplayer.engine.internal.V8Converter
import com.futo.platformplayer.engine.packages.*
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateAssets
import kotlinx.coroutines.*
class V8Plugin {
val config: IV8PluginConfig;
@ -31,14 +28,29 @@ class V8Plugin {
val httpClient: ManagedHttpClient get() = _client;
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
private val _runtimeLock = Object();
var _runtime : V8Runtime? = null;
private val _deps : LinkedHashMap<String, String> = LinkedHashMap();
private val _depsPackages : MutableList<V8Package> = mutableListOf();
private var _script : String? = null;
var isStopped = true;
val onStopped = Event1<V8Plugin>();
//TODO: Implement a more universal isBusy system for plugins + JSClient + pooling? TBD if propagation would be beneficial
private val _busyCounterLock = Object();
private var _busyCounter = 0;
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
/**
* Called before a busy counter is about to be removed.
* Is primarily used to prevent additional calls to dead runtimes.
*
* Parameter is the busy count after this execution
*/
val afterBusy = Event1<Int>();
constructor(context: Context, config: IV8PluginConfig, script: String? = null, client: ManagedHttpClient = ManagedHttpClient(), clientAuth: ManagedHttpClient = ManagedHttpClient()) {
this._client = client;
this._clientAuth = clientAuth;
@ -81,7 +93,7 @@ class V8Plugin {
fun start() {
val script = _script ?: throw IllegalStateException("Attempted to start V8 without script");
synchronized(this) {
synchronized(_runtimeLock) {
if (_runtime != null)
return;
@ -121,19 +133,25 @@ class V8Plugin {
catchScriptErrors("Plugin[${config.name}]") {
it.getExecutor(script).executeVoid()
};
isStopped = false;
}
}
}
fun stop(){
Logger.i(TAG, "Stopping plugin [${config.name}]");
synchronized(this) {
_runtime?.let {
_runtime = null;
if(!it.isClosed && !it.isDead)
it.close();
};
isStopped = true;
whenNotBusy {
synchronized(_runtimeLock) {
isStopped = true;
_runtime?.let {
_runtime = null;
if(!it.isClosed && !it.isDead)
it.close();
Logger.i(TAG, "Stopped plugin [${config.name}]");
};
}
onStopped.emit(this);
}
onStopped.emit(this);
}
fun execute(js: String) : V8Value {
@ -141,14 +159,53 @@ class V8Plugin {
}
fun <T : V8Value> executeTyped(js: String) : T {
warnIfMainThread("V8Plugin.executeTyped");
if(isStopped)
throw PluginEngineStoppedException(config, "Instance is stopped", js);
synchronized(_busyCounterLock) {
_busyCounter++;
}
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
return catchScriptErrors("Plugin[${config.name}]", js) { runtime.getExecutor(js).execute() };
try {
return catchScriptErrors("Plugin[${config.name}]", js) {
runtime.getExecutor(js).execute()
};
}
finally {
synchronized(_busyCounterLock) {
//Free busy *after* afterBusy calls are done to prevent calls on dead runtimes
try {
afterBusy.emit(_busyCounter - 1);
}
catch(ex: Throwable) {
Logger.e(TAG, "Unhandled V8Plugin.afterBusy", ex);
}
_busyCounter--;
}
}
}
fun executeBoolean(js: String) : Boolean? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value };
fun executeString(js: String) : String? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value };
fun executeInteger(js: String) : Int? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value };
fun whenNotBusy(handler: (V8Plugin)->Unit) {
synchronized(_busyCounterLock) {
if(_busyCounter == 0)
handler(this);
else {
val tag = Object();
afterBusy.subscribe(tag) {
if(it == 0) {
Logger.w(TAG, "V8Plugin afterBusy handled");
afterBusy.remove(tag);
handler(this);
}
}
}
}
}
private fun getPackage(context: Context, packageName: String): V8Package {
//TODO: Auto get all package types?
return when(packageName) {

View file

@ -0,0 +1,11 @@
package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.getOrThrow
import java.lang.Exception
open class PluginEngineException(config: IV8PluginConfig, error: String, code: String? = null) : PluginException(config, error, null, code) {
}

View file

@ -0,0 +1,11 @@
package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.getOrThrow
import java.lang.Exception
class PluginEngineStoppedException(config: IV8PluginConfig, error: String, code: String? = null) : PluginEngineException(config, error, code) {
}

View file

@ -110,7 +110,10 @@ class HomeFragment : MainFragment() {
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
UIDialogs.showGeneralRetryErrorDialog(context, "Failed to get Home", it, {
loadResults()
});
}) {
finishRefreshLayoutLoader();
setLoading(false);
};
};
}

View file

@ -28,6 +28,7 @@ import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.subscriptions.SubscriptionBar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
@ -168,7 +169,12 @@ class SubscriptionsFeedFragment : MainFragment() {
.success { loadedResult(it); }
.exception<Throwable> {
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
if(it !is CancellationException)
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
else {
finishRefreshLayoutLoader();
setLoading(false);
}
};
private fun initializeToolbarContent() {
@ -251,7 +257,11 @@ class SubscriptionsFeedFragment : MainFragment() {
} catch (e: Throwable) {
Logger.e(TAG, "Failed to finish loading", e)
}
}
}/*.invokeOnCompletion { //Commented for now, because it doesn't fix the bug it was intended to fix, but might want it later anyway
if(it is CancellationException) {
setLoading(false);
}
}*/
}
private fun handleExceptions(exs: List<Throwable>) {

View file

@ -399,13 +399,15 @@ class StatePlatform {
return@async searchResult;
} catch(ex: Throwable) {
Logger.e(TAG, "getHomeRefresh", ex);
return@async null;
throw ex;
//return@async null;
}
});
}.toList();
val finishedPager = deferred.map { it.second }.awaitFirstNotNullDeferred() ?: return EmptyPager();
val toAwait = deferred.filter { it.second != finishedPager.first };
return RefreshDistributionContentPager(
listOf(finishedPager.second),
toAwait.map { it.second },

View file

@ -3,8 +3,10 @@ package com.futo.platformplayer.views.adapters
import android.content.Context
import android.graphics.drawable.Animatable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
@ -18,6 +20,7 @@ class PreviewPlaceholderViewHolder : ContentPreviewViewHolder {
private val _loader: ImageView;
private val _platformIndicator: PlatformIndicator;
private val _error: TextView;
val context: Context;
@ -30,15 +33,28 @@ class PreviewPlaceholderViewHolder : ContentPreviewViewHolder {
context = itemView.context;
_loader = itemView.findViewById(R.id.loader);
_platformIndicator = itemView.findViewById(R.id.thumbnail_platform);
_error = itemView.findViewById(R.id.text_error);
(_loader.drawable as Animatable?)?.start(); //TODO: stop?
(_loader.drawable as Animatable?)?.start();
}
override fun bind(content: IPlatformContent) {
if(content is PlatformContentPlaceholder)
if(content is PlatformContentPlaceholder) {
_platformIndicator.setPlatformFromClientID(content.id.pluginId);
else
_error.text = content.error?.message ?: "";
if(content.error != null) {
_loader.visibility = View.GONE;
(_loader.drawable as Animatable?)?.stop();
}
else {
_loader.visibility = View.VISIBLE;
(_loader.drawable as Animatable?)?.start();
}
}
else {
_platformIndicator.clearPlatform();
(_loader.drawable as Animatable?)?.stop();
}
}
override fun preview(video: IPlatformContentDetails?, paused: Boolean) { }

View file

@ -100,7 +100,8 @@ class AnnouncementView : LinearLayout {
}
private fun setAnnouncement(announcement: Announcement?, count: Int) {
Logger.i(TAG, "setAnnouncement announcement=$announcement count=$count");
if(count == 0 && announcement == null)
Logger.i(TAG, "setAnnouncement announcement=$announcement count=$count");
_currentAnnouncement = announcement;

View file

@ -33,6 +33,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -49,6 +50,21 @@
app:layout_constraintRight_toRightOf="parent"
android:layout_gravity="center" />
</LinearLayout>
<TextView
android:id="@+id/text_error"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="100dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:textSize="9dp"
android:textColor="@color/pastel_red" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -16,6 +16,7 @@
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:gravity="center">
<ImageView
@ -31,7 +32,7 @@
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:paddingTop="27dp"
app:layout_constraintBottom_toBottomOf="parent"
android:gravity="center">
<com.futo.platformplayer.views.platform.PlatformIndicator
@ -43,7 +44,22 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_gravity="center" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/text_error"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:textSize="9dp"
android:textColor="@color/pastel_red" />
</LinearLayout>