Merge branch 'possiblebusyfix' into 'master'

Possible busy fix

See merge request videostreaming/grayjay!173
This commit is contained in:
Koen
2026-04-27 10:14:01 +00:00
27 changed files with 662 additions and 340 deletions
@@ -118,14 +118,13 @@ inline fun <reified T> V8ValueArray.expectV8Variants(config: IV8PluginConfig, co
inline fun V8Plugin.ensureIsBusy() { inline fun V8Plugin.ensureIsBusy() {
this.let { this.let {
if (!it.isThreadAlreadyBusy()) { if (!it.isThreadAlreadyBusy()) {
//throw IllegalStateException("Tried to access V8Plugin without busy");
val stacktrace = Thread.currentThread().stackTrace; val stacktrace = Thread.currentThread().stackTrace;
Logger.w("Extensions_V8", val message = "V8 USE OUTSIDE BUSY: " + stacktrace.drop(3)?.firstOrNull().toString() +
"V8 USE OUTSIDE BUSY: " + stacktrace.drop(3)?.firstOrNull().toString() +
", " + stacktrace.drop(4)?.firstOrNull().toString() + ", " + stacktrace.drop(4)?.firstOrNull().toString() +
", " + stacktrace.drop(5)?.firstOrNull()?.toString() + ", " + stacktrace.drop(5)?.firstOrNull()?.toString() +
", " + stacktrace.drop(6)?.firstOrNull()?.toString() ", " + stacktrace.drop(6)?.firstOrNull()?.toString();
); Logger.w("Extensions_V8", message);
throw IllegalStateException(message);
} }
} }
} }
@@ -136,7 +135,6 @@ inline fun V8Value.ensureIsBusy() {
} }
inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T { inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T {
if(false)
ensureIsBusy(); ensureIsBusy();
return when(T::class) { return when(T::class) {
String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T; String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T;
@@ -186,10 +184,14 @@ inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextN
else -> throw NotImplementedError("Type ${T::class.simpleName} not implemented conversion"); else -> throw NotImplementedError("Type ${T::class.simpleName} not implemented conversion");
} }
} }
fun V8ArrayToStringList(obj: V8ValueArray): List<String> = obj.keys.map { obj.getString(it) }; fun V8ArrayToStringList(obj: V8ValueArray): List<String> {
obj.ensureIsBusy();
return obj.keys.map { obj.getString(it) };
}
fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> { fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
if(obj == null) if(obj == null)
return hashMapOf(); return hashMapOf();
obj.ensureIsBusy();
val map = hashMapOf<String, String>(); val map = hashMapOf<String, String>();
for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() }) for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() })
map.put(prop, obj.getString(prop)); map.put(prop, obj.getString(prop));
@@ -203,6 +205,7 @@ fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
plugin.busy { plugin.busy {
this.register(object: IV8ValuePromise.IListener { this.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) { override fun onFulfilled(p0: V8Value?) {
plugin.busy {
if(p0 is V8ValueError) if(p0 is V8ValueError)
promiseException = ScriptExecutionException(plugin.config, p0.message); promiseException = ScriptExecutionException(plugin.config, p0.message);
else { else {
@@ -210,14 +213,19 @@ fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
p0.setWeak(); p0.setWeak();
promiseResult = p0 as T; promiseResult = p0 as T;
} }
}
latch.countDown(); latch.countDown();
} }
override fun onRejected(p0: V8Value?) { override fun onRejected(p0: V8Value?) {
plugin.busy {
promiseException = p0?.toException(plugin.config); promiseException = p0?.toException(plugin.config);
}
latch.countDown(); latch.countDown();
} }
override fun onCatch(p0: V8Value?) { override fun onCatch(p0: V8Value?) {
plugin.busy {
promiseException = p0?.toException(plugin.config); promiseException = p0?.toException(plugin.config);
}
latch.countDown(); latch.countDown();
} }
}); });
@@ -229,8 +237,11 @@ fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
} }
//Logger.i("V8", "V8ValueBlocking started (Busy) [" + blockCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString()); //Logger.i("V8", "V8ValueBlocking started (Busy) [" + blockCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString());
val isPending = plugin.busy {
if(!promise.isPending) { promise.isPending
};
if(!isPending) {
plugin.busy {
try { try {
Logger.i("V8", "V8Promise resolved synchronously"); Logger.i("V8", "V8Promise resolved synchronously");
if(promise.isFulfilled) if(promise.isFulfilled)
@@ -242,7 +253,7 @@ fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
promiseException = ex; promiseException = ex;
} }
} }
else { } else {
plugin.unbusy { plugin.unbusy {
latch.await(); latch.await();
} }
@@ -266,26 +277,32 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
plugin.busy { plugin.busy {
this.register(object: IV8ValuePromise.IListener { this.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) { override fun onFulfilled(p0: V8Value?) {
plugin.busy {
plugin.resolvePromise(promise); plugin.resolvePromise(promise);
underlyingDef.complete(p0 as T); underlyingDef.complete(p0 as T);
} }
}
override fun onRejected(p0: V8Value?) { override fun onRejected(p0: V8Value?) {
try { try {
plugin.busy {
plugin.resolvePromise(promise); plugin.resolvePromise(promise);
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented.."); val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented..");
Logger.i("V8", "Promise rejected, setting exception"); Logger.i("V8", "Promise rejected, setting exception");
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound)); underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
} }
}
catch(ex: Throwable) { catch(ex: Throwable) {
Logger.e("V8", "Rejection handling failed?" , ex); Logger.e("V8", "Rejection handling failed?" , ex);
} }
} }
override fun onCatch(p0: V8Value?) { override fun onCatch(p0: V8Value?) {
try { try {
plugin.busy {
plugin.resolvePromise(promise); plugin.resolvePromise(promise);
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented.."); val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented..");
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound)); underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
} }
}
catch(ex: Throwable) { catch(ex: Throwable) {
Logger.e("V8", "Catching handling failed?" , ex); Logger.e("V8", "Catching handling failed?" , ex);
} }
@@ -300,6 +317,7 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
} }
fun V8Value.toException(config: IV8PluginConfig): Throwable { fun V8Value.toException(config: IV8PluginConfig): Throwable {
ensureIsBusy();
val p0 = this; val p0 = this;
if(p0 is V8ValueObject) { if(p0 is V8ValueObject) {
return V8Plugin.getExceptionFromPlugin(config, p0, null, null, null, "P:"); return V8Plugin.getExceptionFromPlugin(config, p0, null, null, null, "P:");
@@ -349,6 +367,7 @@ class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Defer
fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any?): T { fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any?): T {
ensureIsBusy();
var result = this.invoke<V8Value>(method, *obj); var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) { if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!); return result.toV8ValueBlocking(this.getSourcePlugin()!!);
@@ -356,6 +375,7 @@ fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any?): T {
return result as T; return result as T;
} }
fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any?): V8Deferred<T> { fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any?): V8Deferred<T> {
ensureIsBusy();
var result = this.invoke<V8Value>(method, *obj); var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) { if(result is V8ValuePromise) {
return result.toV8ValueAsync(this.getSourcePlugin()!!); return result.toV8ValueAsync(this.getSourcePlugin()!!);
@@ -363,6 +383,7 @@ fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any?):
return V8Deferred(CompletableDeferred(result as T)); return V8Deferred(CompletableDeferred(result as T));
} }
fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value { fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value {
ensureIsBusy();
var result = this.invoke<V8Value>(method, *obj); var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) { if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!); return result.toV8ValueBlocking(this.getSourcePlugin()!!);
@@ -370,6 +391,7 @@ fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value {
return result; return result;
} }
fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferred<V8Value> { fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferred<V8Value> {
ensureIsBusy();
var result = this.invoke<V8Value>(method, *obj); var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) { if(result is V8ValuePromise) {
val result = result.toV8ValueAsync<V8Value>(this.getSourcePlugin()!!); val result = result.toV8ValueAsync<V8Value>(this.getSourcePlugin()!!);
@@ -488,13 +488,14 @@ open class JSClient : IPlatformClient {
if (_peekChannelTypes != null) { if (_peekChannelTypes != null) {
return _peekChannelTypes!!; return _peekChannelTypes!!;
} }
return busy {
val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()"); val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()");
_peekChannelTypes = arr.keys.mapNotNull { _peekChannelTypes = arr.keys.mapNotNull {
val str = arr.get<V8ValueString>(it); val str = arr.get<V8ValueString>(it);
return@mapNotNull str.value; return@mapNotNull str.value;
}; };
return _peekChannelTypes ?: listOf(); return@busy _peekChannelTypes ?: listOf();
}
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("getPeekChannelTypes", ex); announcePluginUnhandledException("getPeekChannelTypes", ex);
@@ -523,10 +524,12 @@ open class JSClient : IPlatformClient {
if(!capabilities.hasGetChannelUrlByClaim) if(!capabilities.hasGetChannelUrlByClaim)
throw IllegalStateException("This plugin does not support channel url by claim"); throw IllegalStateException("This plugin does not support channel url by claim");
return busy {
val value = plugin.executeTyped<V8Value>("source.getChannelUrlByClaim(${claimType}, ${Json.encodeToString(claimValues)})"); val value = plugin.executeTyped<V8Value>("source.getChannelUrlByClaim(${claimType}, ${Json.encodeToString(claimValues)})");
if(value !is V8ValueString) if(value !is V8ValueString)
return null; return@busy null;
return value.value; return@busy value.value;
}
} }
@JSOptional @JSOptional
@JSDocs(12, "source.getChannelTemplateByClaimMap()", "Get a map for every supported claimtype mapping field to urls") @JSDocs(12, "source.getChannelTemplateByClaimMap()", "Get a map for every supported claimtype mapping field to urls")
@@ -536,9 +539,10 @@ open class JSClient : IPlatformClient {
if(!capabilities.hasGetChannelTemplateByClaimMap) if(!capabilities.hasGetChannelTemplateByClaimMap)
throw IllegalStateException("This plugin does not support channel template by claim map"); throw IllegalStateException("This plugin does not support channel template by claim map");
return busy {
val value = plugin.executeTyped<V8Value>("source.getChannelTemplateByClaimMap()"); val value = plugin.executeTyped<V8Value>("source.getChannelTemplateByClaimMap()");
if(value !is V8ValueObject) if(value !is V8ValueObject)
return mapOf(); return@busy mapOf();
val claimTypes = mutableMapOf<Int, Map<Int, String>>(); val claimTypes = mutableMapOf<Int, Map<Int, String>>();
@@ -557,7 +561,8 @@ open class JSClient : IPlatformClient {
} }
} }
channelClaimTemplates = claimTypes.toMap(); channelClaimTemplates = claimTypes.toMap();
return claimTypes; return@busy claimTypes;
}
} }
@@ -698,27 +703,33 @@ open class JSClient : IPlatformClient {
@JSDocs(22, "source.getUserPlaylists()", "Gets the playlist of the current user") @JSDocs(22, "source.getUserPlaylists()", "Gets the playlist of the current user")
override fun getUserPlaylists(): Array<String> { override fun getUserPlaylists(): Array<String> {
ensureEnabled(); ensureEnabled();
return plugin.executeTyped<V8ValueArray>("source.getUserPlaylists()") return busy {
return@busy plugin.executeTyped<V8ValueArray>("source.getUserPlaylists()")
.toArray() .toArray()
.map { (it as V8ValueString).value } .map { (it as V8ValueString).value }
.toTypedArray(); .toTypedArray();
} }
}
@JSOptional @JSOptional
@JSDocs(23, "source.getUserSubscriptions()", "Gets the subscriptions of the current user") @JSDocs(23, "source.getUserSubscriptions()", "Gets the subscriptions of the current user")
override fun getUserSubscriptions(): Array<String> { override fun getUserSubscriptions(): Array<String> {
ensureEnabled(); ensureEnabled();
return plugin.executeTyped<V8ValueArray>("source.getUserSubscriptions()") return busy {
return@busy plugin.executeTyped<V8ValueArray>("source.getUserSubscriptions()")
.toArray() .toArray()
.map { (it as V8ValueString).value } .map { (it as V8ValueString).value }
.toTypedArray(); .toTypedArray();
} }
}
@JSOptional @JSOptional
@JSDocs(23, "source.getUserHistory()", "Gets the history of the current user") @JSDocs(23, "source.getUserHistory()", "Gets the history of the current user")
override fun getUserHistory(): IPager<IPlatformContent> { override fun getUserHistory(): IPager<IPlatformContent> {
ensureEnabled(); ensureEnabled();
return JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()")); return isBusyWith("getUserHistory") {
return@isBusyWith JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()"));
}
} }
fun validate() { fun validate() {
@@ -21,6 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.getSourcePlugin
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
open class JSArticle( open class JSArticle(
@@ -34,7 +35,7 @@ open class JSArticle(
obj.getOrDefault<String>(config, "summary", "PlatformArticle", "") ?: "" obj.getOrDefault<String>(config, "summary", "PlatformArticle", "") ?: ""
override val thumbnails: Thumbnails? = override val thumbnails: Thumbnails? =
if (obj.has("thumbnails")) if (obj.getSourcePlugin()?.busy { obj.has("thumbnails") } ?: obj.has("thumbnails"))
Thumbnails.fromV8( Thumbnails.fromV8(
config, config,
obj.getOrThrow<V8ValueObject>(config, "thumbnails", "PlatformArticle") obj.getOrThrow<V8ValueObject>(config, "thumbnails", "PlatformArticle")
@@ -31,18 +31,20 @@ open class JSArticleDetails(
final override val contentType: ContentType = ContentType.ARTICLE final override val contentType: ContentType = ContentType.ARTICLE
private val _hasGetComments: Boolean = _content.has("getComments") private val _hasGetComments: Boolean = client.busy { _content.has("getComments") }
private val _hasGetContentRecommendations: Boolean = _content.has("getContentRecommendations") private val _hasGetContentRecommendations: Boolean = client.busy { _content.has("getContentRecommendations") }
override val rating: IRating = override val rating: IRating = client.busy {
obj.getOrDefault<V8ValueObject>(client.config, "rating", "PlatformArticle", null) obj.getOrDefault<V8ValueObject>(client.config, "rating", "PlatformArticle", null)
?.let { IRating.fromV8(client.config, it, "PlatformArticle") } ?.let { IRating.fromV8(client.config, it, "PlatformArticle") }
?: RatingLikes(0) ?: RatingLikes(0)
}
override val summary: String = override val summary: String = client.busy {
_content.getOrThrow(client.config, "summary", "PlatformArticle") _content.getOrThrow(client.config, "summary", "PlatformArticle")
}
override val thumbnails: Thumbnails? = override val thumbnails: Thumbnails? = client.busy {
if (_content.has("thumbnails")) if (_content.has("thumbnails"))
Thumbnails.fromV8( Thumbnails.fromV8(
client.config, client.config,
@@ -50,14 +52,19 @@ open class JSArticleDetails(
) )
else else
null null
}
override val segments: List<IJSArticleSegment> = override val segments: List<IJSArticleSegment> = client.busy {
obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", "PlatformArticle") obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", "PlatformArticle")
?.mapNotNull { fromV8Segment(client, it) } ?.mapNotNull { fromV8Segment(client, it) }
?: emptyList() ?: emptyList()
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? { override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
if(!_hasGetComments || _content.isClosed) val canGetComments = this.client.busy {
_hasGetComments && !_content.isClosed
}
if(!canGetComments)
return null; return null;
if(client is DevJSClient) if(client is DevJSClient)
@@ -73,7 +80,10 @@ open class JSArticleDetails(
override fun getPlaybackTracker(): IPlaybackTracker? = null; override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? { override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
if(!_hasGetContentRecommendations || _content.isClosed) val canGetContentRecommendations = this.client.busy {
_hasGetContentRecommendations && !_content.isClosed
}
if(!canGetContentRecommendations)
return null; return null;
if(client is DevJSClient) if(client is DevJSClient)
@@ -87,20 +97,25 @@ open class JSArticleDetails(
} }
private fun getContentRecommendationsJS(client: JSClient): JSContentPager { private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
return client.busy {
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>()); val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager); return@busy JSContentPager(_pluginConfig, client, contentPager);
}
} }
private fun getCommentsJS(client: JSClient): JSCommentPager { private fun getCommentsJS(client: JSClient): JSCommentPager {
return client.busy {
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>()); val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager); return@busy JSCommentPager(_pluginConfig, client, commentPager);
}
} }
companion object { companion object {
fun fromV8Segment(client: JSClient, obj: V8ValueObject): IJSArticleSegment? { fun fromV8Segment(client: JSClient, obj: V8ValueObject): IJSArticleSegment? {
return client.busy {
if(!obj.has("type")) if(!obj.has("type"))
throw IllegalArgumentException("Object missing type field"); throw IllegalArgumentException("Object missing type field");
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) { return@busy when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
SegmentType.TEXT -> JSTextSegment(client, obj); SegmentType.TEXT -> JSTextSegment(client, obj);
SegmentType.IMAGES -> JSImagesSegment(client, obj); SegmentType.IMAGES -> JSImagesSegment(client, obj);
SegmentType.HEADER -> JSHeaderSegment(client, obj); SegmentType.HEADER -> JSHeaderSegment(client, obj);
@@ -109,6 +124,7 @@ open class JSArticleDetails(
} }
} }
} }
}
} }
enum class SegmentType(val value: Int) { enum class SegmentType(val value: Int) {
@@ -46,23 +46,45 @@ class JSComment : IPlatformComment {
_comment = obj; _comment = obj;
_plugin = plugin; _plugin = plugin;
var parsedContextUrl: String? = null;
var parsedAuthor: PlatformAuthorLink? = null;
var parsedMessage: String? = null;
var parsedRating: IRating? = null;
var parsedDate: OffsetDateTime? = null;
var parsedReplyCount: Int? = null;
var parsedContext: Map<String, String>? = null;
var parsedHasGetReplies = false;
plugin.busy {
val contextName = "Comment"; val contextName = "Comment";
contextUrl = _comment!!.getOrThrow(config, "contextUrl", contextName); parsedContextUrl = _comment!!.getOrThrow(config, "contextUrl", contextName);
author = PlatformAuthorLink.fromV8(_config!!, _comment!!.getOrThrow(config, "author", contextName)); parsedAuthor = PlatformAuthorLink.fromV8(_config!!, _comment!!.getOrThrow(config, "author", contextName));
message = _comment!!.getOrThrow(config, "message", contextName); parsedMessage = _comment!!.getOrThrow(config, "message", contextName);
rating = IRating.fromV8(config, _comment!!.getOrThrow(config, "rating", contextName)); parsedRating = IRating.fromV8(config, _comment!!.getOrThrow(config, "rating", contextName));
date = _comment!!.getOrThrowNullable<Int>(config, "date", contextName)?.let { OffsetDateTime.of(LocalDateTime.ofEpochSecond(it.toLong(), 0, ZoneOffset.UTC), ZoneOffset.UTC) } parsedDate = _comment!!.getOrThrowNullable<Int>(config, "date", contextName)?.let { OffsetDateTime.of(LocalDateTime.ofEpochSecond(it.toLong(), 0, ZoneOffset.UTC), ZoneOffset.UTC) };
replyCount = _comment!!.getOrThrowNullable(config, "replyCount", contextName); parsedReplyCount = _comment!!.getOrThrowNullable(config, "replyCount", contextName);
context = _comment!!.getOrDefault(config, "context", contextName, hashMapOf()) ?: hashMapOf(); parsedContext = _comment!!.getOrDefault(config, "context", contextName, hashMapOf()) ?: hashMapOf();
_hasGetReplies = _comment!!.has("getReplies"); parsedHasGetReplies = _comment!!.has("getReplies");
}
contextUrl = parsedContextUrl ?: "";
author = parsedAuthor ?: PlatformAuthorLink.UNKNOWN;
message = parsedMessage ?: "";
rating = parsedRating ?: throw IllegalStateException("Missing comment rating");
date = parsedDate;
replyCount = parsedReplyCount;
context = parsedContext ?: hashMapOf();
_hasGetReplies = parsedHasGetReplies;
} }
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment>? { override fun getReplies(client: IPlatformClient): IPager<IPlatformComment>? {
if(!_hasGetReplies) if(!_hasGetReplies)
return null; return null;
val obj = _comment!!.invokeV8<V8ValueObject>("getReplies", arrayOf<Any>());
val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient"); val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
return JSCommentPager(_config!!, plugin, obj); return plugin.busy {
val obj = _comment!!.invokeV8<V8ValueObject>("getReplies", arrayOf<Any>());
return@busy JSCommentPager(_config!!, plugin, obj);
}
} }
} }
@@ -11,6 +11,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.decodeUnicode import com.futo.platformplayer.decodeUnicode
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getSourcePlugin
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.OffsetDateTime import java.time.OffsetDateTime
@@ -23,7 +24,8 @@ open class JSContent(
override val contentType: ContentType = ContentType.UNKNOWN override val contentType: ContentType = ContentType.UNKNOWN
protected val _hasGetDetails: Boolean = _content.has("getDetails") protected val _hasGetDetails: Boolean =
_content.getSourcePlugin()?.busy { _content.has("getDetails") } ?: false
override val id: PlatformID = override val id: PlatformID =
PlatformID.fromV8(_pluginConfig, _content.getOrThrow(_pluginConfig, "id", CTX)) PlatformID.fromV8(_pluginConfig, _content.getOrThrow(_pluginConfig, "id", CTX))
@@ -41,7 +41,9 @@ abstract class JSPager<T> : IPager<T> {
} }
override fun hasMorePages(): Boolean { override fun hasMorePages(): Boolean {
return _hasMorePages && !pager.isClosed; return plugin.getUnderlyingPlugin().busy {
_hasMorePages && !pager.isClosed;
}
} }
override fun nextPage() { override fun nextPage() {
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getSourcePlugin
import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
@@ -30,52 +31,79 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) { constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
var parsedRating: IRating? = null;
var parsedTextType: TextType? = null;
var parsedContent: String? = null;
var parsedHasGetComments = false;
var parsedHasGetContentRecommendations = false;
val parse = {
val contextName = "PlatformPostDetails"; val contextName = "PlatformPostDetails";
rating = obj.getOrDefault<V8ValueObject>(config, "rating", contextName, null)?.let { IRating.fromV8(config, it, contextName) } ?: RatingLikes(0); parsedRating = obj.getOrDefault<V8ValueObject>(config, "rating", contextName, null)?.let { IRating.fromV8(config, it, contextName) } ?: RatingLikes(0);
textType = TextType.fromInt((obj.getOrDefault<Int>(config, "textType", contextName, null) ?: 0)); parsedTextType = TextType.fromInt((obj.getOrDefault<Int>(config, "textType", contextName, null) ?: 0));
content = obj.getOrDefault(config, "content", contextName, "") ?: ""; parsedContent = obj.getOrDefault(config, "content", contextName, "") ?: "";
_hasGetComments = _content.has("getComments"); parsedHasGetComments = _content.has("getComments");
_hasGetContentRecommendations = _content.has("getContentRecommendations"); parsedHasGetContentRecommendations = _content.has("getContentRecommendations");
};
obj.getSourcePlugin()?.busy {
parse();
} ?: parse()
rating = parsedRating ?: RatingLikes(0);
textType = parsedTextType ?: TextType.RAW;
content = parsedContent ?: "";
_hasGetComments = parsedHasGetComments;
_hasGetContentRecommendations = parsedHasGetContentRecommendations;
} }
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? { override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
if(!_hasGetComments || _content.isClosed) val jsClient = client as? JSClient;
if(jsClient == null)
return null;
val canGetComments = jsClient.busy {
_hasGetComments && !_content.isClosed
}
if(!canGetComments)
return null; return null;
if(client is DevJSClient) if(client is DevJSClient)
return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.getComments()") { return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.getComments()") {
return@handleDevCall getCommentsJS(client); return@handleDevCall getCommentsJS(client);
} }
else if(client is JSClient) return getCommentsJS(jsClient);
return getCommentsJS(client);
return null;
} }
override fun getPlaybackTracker(): IPlaybackTracker? = null; override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? { override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
if(!_hasGetContentRecommendations || _content.isClosed) val jsClient = client as? JSClient;
if(jsClient == null)
return null;
val canGetContentRecommendations = jsClient.busy {
_hasGetContentRecommendations && !_content.isClosed
}
if(!canGetContentRecommendations)
return null; return null;
if(client is DevJSClient) if(client is DevJSClient)
return StateDeveloper.instance.handleDevCall(client.devID, "postDetail.getContentRecommendations()") { return StateDeveloper.instance.handleDevCall(client.devID, "postDetail.getContentRecommendations()") {
return@handleDevCall getContentRecommendationsJS(client); return@handleDevCall getContentRecommendationsJS(client);
} }
else if(client is JSClient) return getContentRecommendationsJS(jsClient);
return getContentRecommendationsJS(client);
return null;
} }
private fun getContentRecommendationsJS(client: JSClient): JSContentPager { private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
return client.busy {
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>()); val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager); return@busy JSContentPager(_pluginConfig, client, contentPager);
}
} }
private fun getCommentsJS(client: JSClient): JSCommentPager { private fun getCommentsJS(client: JSClient): JSCommentPager {
return client.busy {
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>()); val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager); return@busy JSCommentPager(_pluginConfig, client, commentPager);
}
} }
} }
@@ -41,21 +41,27 @@ class JSRequestExecutor: AutoCloseable {
this._config = plugin.config; this._config = plugin.config;
val config = plugin.config; val config = plugin.config;
urlPrefix = executor.getOrDefault(config, "urlPrefix", "RequestExecutor", null); var parsedUrlPrefix: String? = null;
var parsedHasCleanup = false;
plugin.busy {
parsedUrlPrefix = executor.getOrDefault(config, "urlPrefix", "RequestExecutor", null);
if(!executor.has("executeRequest")) if(!executor.has("executeRequest"))
throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null); throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null);
hasCleanup = executor.has("cleanup"); parsedHasCleanup = executor.has("cleanup");
}
urlPrefix = parsedUrlPrefix;
hasCleanup = parsedHasCleanup;
} }
//TODO: Executor properties? //TODO: Executor properties?
@Throws(ScriptException::class) @Throws(ScriptException::class)
open fun executeRequest(method: String, url: String, body: ByteArray?, headers: Map<String, String>): ByteArray { open fun executeRequest(method: String, url: String, body: ByteArray?, headers: Map<String, String>): ByteArray {
return _plugin.getUnderlyingPlugin().busy {
if (_executor.isClosed) if (_executor.isClosed)
throw IllegalStateException("Executor object is closed"); throw IllegalStateException("Executor object is closed");
return _plugin.getUnderlyingPlugin().busy {
val result = if(_plugin is DevJSClient) val result = if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>( V8Plugin.catchScriptErrors<Any>(
@@ -108,11 +114,13 @@ class JSRequestExecutor: AutoCloseable {
open fun cleanup() { open fun cleanup() {
_plugin.busy {
synchronized(_cleanLock) { synchronized(_cleanLock) {
if (!hasCleanup || _executor.isClosed || _cleaned) if (!hasCleanup || _executor.isClosed || _cleaned)
return; return@busy;
_cleaned = true; _cleaned = true;
} }
}
Logger.i("JSRequestExecutor", "JSRequestExecutor cleanup requested"); Logger.i("JSRequestExecutor", "JSRequestExecutor cleanup requested");
_plugin.busy { _plugin.busy {
if(_plugin is DevJSClient) if(_plugin is DevJSClient)
@@ -36,11 +36,11 @@ class JSRequestModifier: IRequestModifier {
} }
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest { override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
return _plugin.busy {
if (_modifier.isClosed) { if (_modifier.isClosed) {
return Request(url, headers); return@busy Request(url, headers);
} }
return _plugin.busy {
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
_modifier.invokeV8("modifyRequest", url, headers); _modifier.invokeV8("modifyRequest", url, headers);
} as V8ValueObject; } as V8ValueObject;
@@ -29,12 +29,28 @@ class JSSubtitleSource : ISubtitleSource {
constructor(config: SourcePluginConfig, v8Value: V8ValueObject) { constructor(config: SourcePluginConfig, v8Value: V8ValueObject) {
_obj = v8Value; _obj = v8Value;
var parsedName: String? = null;
var parsedLanguage: String? = null;
var parsedUrl: String? = null;
var parsedFormat: String? = null;
var parsedHasFetch = false;
val parse = {
val context = "JSSubtitles"; val context = "JSSubtitles";
name = v8Value.getOrThrow(config, "name", context, false); parsedName = v8Value.getOrThrow(config, "name", context, false);
language = v8Value.getOrDefault(config, "language", context, null); parsedLanguage = v8Value.getOrDefault(config, "language", context, null);
url = v8Value.getOrThrow(config, "url", context, true); parsedUrl = v8Value.getOrThrow(config, "url", context, true);
format = v8Value.getOrThrow(config, "format", context, true); parsedFormat = v8Value.getOrThrow(config, "format", context, true);
hasFetch = v8Value.has("getSubtitles"); parsedHasFetch = v8Value.has("getSubtitles");
};
v8Value.getSourcePlugin()?.busy {
parse();
} ?: parse()
name = parsedName ?: "";
language = parsedLanguage;
url = parsedUrl;
format = parsedFormat;
hasFetch = parsedHasFetch;
} }
override fun getSubtitles(): String { override fun getSubtitles(): String {
@@ -52,34 +52,63 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
override val subtitles: List<ISubtitleSource>; override val subtitles: List<ISubtitleSource>;
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) {
val contextName = "VideoDetails";
_plugin = plugin; _plugin = plugin;
var parsedDescription: String? = null;
var parsedVideo: IVideoSourceDescriptor? = null;
var parsedDash: IDashManifestSource? = null;
var parsedHls: IHLSManifestSource? = null;
var parsedLive: IVideoSource? = null;
var parsedRating: IRating? = null;
var parsedSubtitles: List<ISubtitleSource>? = null;
var parsedHasGetComments = false;
var parsedHasGetPlaybackTracker = false;
var parsedHasGetContentRecommendations = false;
var parsedHasGetVODEvents = false;
plugin.busy {
val contextName = "VideoDetails";
val config = plugin.config; val config = plugin.config;
description = _content.getOrThrow(config, "description", contextName); parsedDescription = _content.getOrThrow(config, "description", contextName);
video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName)); parsedVideo = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName));
dash = JSSource.fromV8DashNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName)); parsedDash = JSSource.fromV8DashNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName));
hls = JSSource.fromV8HLSNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName)); parsedHls = JSSource.fromV8HLSNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName));
live = JSSource.fromV8VideoNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName)); parsedLive = JSSource.fromV8VideoNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName));
rating = IRating.fromV8OrDefault(config, _content.getOrDefault<V8ValueObject>(config, "rating", contextName, null), RatingLikes(0)); parsedRating = IRating.fromV8OrDefault(config, _content.getOrDefault<V8ValueObject>(config, "rating", contextName, null), RatingLikes(0));
if(!_content.has("subtitles")) if(!_content.has("subtitles"))
subtitles = listOf(); parsedSubtitles = listOf();
else { else {
val subArrs = _content.getOrThrowNullable<V8ValueArray>(config, "subtitles", contextName); val subArrs = _content.getOrThrowNullable<V8ValueArray>(config, "subtitles", contextName);
if(subArrs != null) if(subArrs != null)
subtitles = subArrs.keys.map { JSSubtitleSource.fromV8(config, subArrs.get(it)) }; parsedSubtitles = subArrs.keys.map { JSSubtitleSource.fromV8(config, subArrs.get(it)) };
else else
subtitles = listOf(); parsedSubtitles = listOf();
} }
_hasGetComments = _content.has("getComments"); parsedHasGetComments = _content.has("getComments");
_hasGetPlaybackTracker = _content.has("getPlaybackTracker"); parsedHasGetPlaybackTracker = _content.has("getPlaybackTracker");
_hasGetContentRecommendations = _content.has("getContentRecommendations"); parsedHasGetContentRecommendations = _content.has("getContentRecommendations");
_hasGetVODEvents = _content.has("getVODEvents"); parsedHasGetVODEvents = _content.has("getVODEvents");
}
description = parsedDescription ?: "";
video = parsedVideo ?: throw IllegalStateException("Missing video source descriptor");
dash = parsedDash;
hls = parsedHls;
live = parsedLive;
rating = parsedRating ?: RatingLikes(0);
subtitles = parsedSubtitles ?: listOf();
_hasGetComments = parsedHasGetComments;
_hasGetPlaybackTracker = parsedHasGetPlaybackTracker;
_hasGetContentRecommendations = parsedHasGetContentRecommendations;
_hasGetVODEvents = parsedHasGetVODEvents;
} }
override fun getPlaybackTracker(): IPlaybackTracker? { override fun getPlaybackTracker(): IPlaybackTracker? {
if(!_hasGetPlaybackTracker || _content.isClosed) val canGetPlaybackTracker = _plugin.busy {
_hasGetPlaybackTracker && !_content.isClosed
}
if(!canGetPlaybackTracker)
return null; return null;
if(_pluginConfig.id == StateDeveloper.DEV_ID) if(_pluginConfig.id == StateDeveloper.DEV_ID)
return StateDeveloper.instance.handleDevCall(_pluginConfig.id, "videoDetail.getComments()") { return StateDeveloper.instance.handleDevCall(_pluginConfig.id, "videoDetail.getComments()") {
@@ -102,7 +131,10 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
} }
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? { override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
if(!_hasGetContentRecommendations || _content.isClosed) val canGetContentRecommendations = _plugin.busy {
_hasGetContentRecommendations && !_content.isClosed
}
if(!canGetContentRecommendations)
return null; return null;
if(client is DevJSClient) if(client is DevJSClient)
@@ -122,7 +154,12 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
} }
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? { override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
if(client !is JSClient || !_hasGetComments || _content.isClosed) if(client !is JSClient)
return null;
val canGetComments = _plugin.busy {
_hasGetComments && !_content.isClosed
}
if(!canGetComments)
return null; return null;
if(client is DevJSClient) if(client is DevJSClient)
@@ -39,10 +39,10 @@ open class JSAudioUrlSource(
?: "$container $bitrate" ?: "$container $bitrate"
override var priority: Boolean = override var priority: Boolean =
if (_obj.has("priority")) _obj.getOrThrow<Boolean>(cfg, "priority", ctx) else false plugin.busy { if (_obj.has("priority")) _obj.getOrThrow<Boolean>(cfg, "priority", ctx) else false }
override var original: Boolean = override var original: Boolean =
if (_obj.has("original")) _obj.getOrThrow<Boolean>(cfg, "original", ctx) else false plugin.busy { if (_obj.has("original")) _obj.getOrThrow<Boolean>(cfg, "original", ctx) else false }
override fun getAudioUrl(): String = url override fun getAudioUrl(): String = url
@@ -19,21 +19,23 @@ class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
val config = plugin.config val config = plugin.config
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName) licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor") hasLicenseRequestExecutor = plugin.busy { obj.has("getLicenseRequestExecutor") }
} }
override fun getLicenseRequestExecutor(): JSRequestExecutor? { override fun getLicenseRequestExecutor(): JSRequestExecutor? {
return _plugin.busy {
if (!hasLicenseRequestExecutor || _obj.isClosed) if (!hasLicenseRequestExecutor || _obj.isClosed)
return null return@busy null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>()) _obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
} }
if (result !is V8ValueObject) if (result !is V8ValueObject)
return null return@busy null
return JSRequestExecutor(_plugin, result) return@busy JSRequestExecutor(_plugin, result)
}
} }
override fun toString(): String { override fun toString(): String {
@@ -55,7 +55,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false; priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN; language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
original = obj.getOrNull(config, "original", contextName) ?: false; original = obj.getOrNull(config, "original", contextName) ?: false;
hasGenerate = _obj.has("generate"); hasGenerate = plugin.busy { _obj.has("generate") };
} }
private var _pregenerate: V8Deferred<String?>? = null; private var _pregenerate: V8Deferred<String?>? = null;
@@ -67,7 +67,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> { override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate) if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest)); return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed) if(_plugin.busy { _obj.isClosed })
throw IllegalStateException("Source object already closed"); throw IllegalStateException("Source object already closed");
val pregenerated = _pregenerate; val pregenerated = _pregenerate;
@@ -111,7 +111,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
override fun generate(): String? { override fun generate(): String? {
if(!hasGenerate) if(!hasGenerate)
return manifest; return manifest;
if(_obj.isClosed) if(_plugin.busy { _obj.isClosed })
throw IllegalStateException("Source object already closed"); throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin(); val plugin = _plugin.getUnderlyingPlugin();
@@ -73,7 +73,7 @@ open class JSDashManifestRawSource(
override var manifest: String? = override var manifest: String? =
_obj.getOrDefault<String>(cfg, "manifest", ctx, null) _obj.getOrDefault<String>(cfg, "manifest", ctx, null)
override val hasGenerate: Boolean = _obj.has("generate") override val hasGenerate: Boolean = plugin.busy { _obj.has("generate") }
val canMerge: Boolean = val canMerge: Boolean =
_obj.getOrDefault<Boolean>(cfg, "canMerge", ctx, false) ?: false _obj.getOrDefault<Boolean>(cfg, "canMerge", ctx, false) ?: false
@@ -89,7 +89,7 @@ open class JSDashManifestRawSource(
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> { override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate) if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest)); return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed) if(_plugin.busy { _obj.isClosed })
throw IllegalStateException("Source object already closed"); throw IllegalStateException("Source object already closed");
val pregenerated = _pregenerate; val pregenerated = _pregenerate;
if(pregenerated != null) { if(pregenerated != null) {
@@ -133,7 +133,7 @@ open class JSDashManifestRawSource(
override open fun generate(): String? { override open fun generate(): String? {
if(!hasGenerate) if(!hasGenerate)
return manifest; return manifest;
if(_obj.isClosed) if(_plugin.busy { _obj.isClosed })
throw IllegalStateException("Source object already closed"); throw IllegalStateException("Source object already closed");
var result: String? = null; var result: String? = null;
@@ -42,24 +42,26 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
priority = obj.getOrNull(config, "priority", contextName) ?: false priority = obj.getOrNull(config, "priority", contextName) ?: false
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName) licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor") hasLicenseRequestExecutor = plugin.busy { obj.has("getLicenseRequestExecutor") }
language = _obj.getOrNull(config, "language", contextName); language = _obj.getOrNull(config, "language", contextName);
original = _obj.getOrNull(config, "original", contextName); original = _obj.getOrNull(config, "original", contextName);
} }
override fun getLicenseRequestExecutor(): JSRequestExecutor? { override fun getLicenseRequestExecutor(): JSRequestExecutor? {
return _plugin.busy {
if (!hasLicenseRequestExecutor || _obj.isClosed) if (!hasLicenseRequestExecutor || _obj.isClosed)
return null return@busy null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>()) _obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
} }
if (result !is V8ValueObject) if (result !is V8ValueObject)
return null return@busy null
return JSRequestExecutor(_plugin, result) return@busy JSRequestExecutor(_plugin, result)
}
} }
override fun getVideoUrl(): String { override fun getVideoUrl(): String {
@@ -44,15 +44,26 @@ abstract class JSSource {
this._obj = obj; this._obj = obj;
this.type = type; this.type = type;
_requestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let { var parsedRequestModifier: JSRequest? = null;
var parsedHasRequestModifier = false;
var parsedRequestExecutor: JSRequest? = null;
var parsedHasRequestExecutor = false;
plugin.busy {
parsedRequestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let {
JSRequest(plugin, it, null, null, true); JSRequest(plugin, it, null, null, true);
} };
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier"); parsedHasRequestModifier = parsedRequestModifier != null || obj.has("getRequestModifier");
_requestExecutor = obj.getOrDefault<V8ValueObject>(_config, "requestExecutor", "JSSource.requestExecutor", null)?.let { parsedRequestExecutor = obj.getOrDefault<V8ValueObject>(_config, "requestExecutor", "JSSource.requestExecutor", null)?.let {
JSRequest(plugin, it, null, null, true); JSRequest(plugin, it, null, null, true);
};
parsedHasRequestExecutor = parsedRequestExecutor != null || obj.has("getRequestExecutor");
} }
hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor");
_requestModifier = parsedRequestModifier;
hasRequestModifier = parsedHasRequestModifier;
_requestExecutor = parsedRequestExecutor;
hasRequestExecutor = parsedHasRequestExecutor;
} }
fun getRequestModifier(): IRequestModifier? = _plugin.isBusyWith("getRequestModifier") { fun getRequestModifier(): IRequestModifier? = _plugin.isBusyWith("getRequestModifier") {
@@ -18,21 +18,23 @@ class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
val config = plugin.config val config = plugin.config
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName) licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor") hasLicenseRequestExecutor = plugin.busy { obj.has("getLicenseRequestExecutor") }
} }
override fun getLicenseRequestExecutor(): JSRequestExecutor? { override fun getLicenseRequestExecutor(): JSRequestExecutor? {
return _plugin.busy {
if (!hasLicenseRequestExecutor || _obj.isClosed) if (!hasLicenseRequestExecutor || _obj.isClosed)
return null return@busy null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>()) _obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
} }
if (result !is V8ValueObject) if (result !is V8ValueObject)
return null return@busy null
return JSRequestExecutor(_plugin, result) return@busy JSRequestExecutor(_plugin, result)
}
} }
override fun toString(): String { override fun toString(): String {
@@ -139,13 +139,17 @@ class VideoDownload {
@Contextual @Contextual
@kotlinx.serialization.Transient @kotlinx.serialization.Transient
var videoSourceLive: JSSource? = null; var videoSourceLive: JSSource? = null;
val isLiveVideoSourceValid get() = videoSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false; val isLiveVideoSourceValid get() = videoSourceLive?.getUnderlyingPlugin()?.busy {
videoSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
} ?: false;
var requiresLiveAudioSource: Boolean = false; var requiresLiveAudioSource: Boolean = false;
@Contextual @Contextual
@kotlinx.serialization.Transient @kotlinx.serialization.Transient
var audioSourceLive: JSSource? = null; var audioSourceLive: JSSource? = null;
val isLiveAudioSourceValid get() = audioSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false; val isLiveAudioSourceValid get() = audioSourceLive?.getUnderlyingPlugin()?.busy {
audioSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
} ?: false;
var hasVideoRequestExecutor: Boolean = false; var hasVideoRequestExecutor: Boolean = false;
var hasAudioRequestExecutor: Boolean = false; var hasAudioRequestExecutor: Boolean = false;
@@ -58,6 +58,7 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
@@ -95,6 +96,9 @@ class V8Plugin {
private val _busyLock = ReentrantLock() private val _busyLock = ReentrantLock()
val isBusy get() = _busyLock.isLocked; val isBusy get() = _busyLock.isLocked;
@Volatile
private var _busyHolder: Thread? = null;
var allowDevSubmit: Boolean = false var allowDevSubmit: Boolean = false
private set(value) { private set(value) {
field = value; field = value;
@@ -161,9 +165,10 @@ class V8Plugin {
fun start() { fun start() {
val script = _script ?: throw IllegalStateException("Attempted to start V8 without script"); val script = _script ?: throw IllegalStateException("Attempted to start V8 without script");
tryBusy(BUSY_STARTUP_MS) {
synchronized(_runtimeLock) { synchronized(_runtimeLock) {
if (_runtime != null) if (_runtime != null)
return; return@tryBusy;
runtimeId = runtimeId + 1; runtimeId = runtimeId + 1;
//V8RuntimeOptions.V8_FLAGS.setUseStrict(true); //V8RuntimeOptions.V8_FLAGS.setUseStrict(true);
val host = V8Host.getV8Instance(); val host = V8Host.getV8Instance();
@@ -209,6 +214,7 @@ class V8Plugin {
} }
} }
} }
}
fun stop(){ fun stop(){
Logger.i(TAG, "Stopping plugin [${config.name}]"); Logger.i(TAG, "Stopping plugin [${config.name}]");
busy { busy {
@@ -254,27 +260,30 @@ class V8Plugin {
fun isThreadAlreadyBusy(): Boolean { fun isThreadAlreadyBusy(): Boolean {
return _busyLock.isHeldByCurrentThread; return _busyLock.isHeldByCurrentThread;
} }
fun <T> busy(handle: ()->T): T { fun <T> busy(handle: ()->T): T = busyInternal(BUSY_FATAL_MS, true, "busy(enter)", handle)
_busyLock.lock();
//Logger.i(TAG, "Busy Enter [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString()) fun <T> tryBusy(maxWaitMs: Long, handle: ()->T): T = busyInternal(maxWaitMs, false, "tryBusy(enter)", handle)
private fun <T> busyInternal(maxWaitMs: Long, allowUnwedge: Boolean, context: String, handle: ()->T): T {
acquireBusyOrThrow(context, maxWaitMs, allowUnwedge);
_busyHolder = Thread.currentThread();
try { try {
return handle(); return handle();
} }
finally { finally {
//Logger.i(TAG, "Busy Leave [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString()) if (_busyLock.isHeldByCurrentThread) {
if (_busyLock.holdCount == 1)
_busyHolder = null;
_busyLock.unlock(); _busyLock.unlock();
} }
/* }
_busyLock.withLock {
//Logger.i(TAG, "Entered busy: " + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString());
return handle();
}*/
} }
fun <T> unbusy(handle: ()->T): T { fun <T> unbusy(handle: ()->T): T {
val wasLocked = isThreadAlreadyBusy(); val wasLocked = isThreadAlreadyBusy();
if(!wasLocked) if(!wasLocked)
return handle(); return handle();
val lockCount = _busyLock.holdCount; val lockCount = _busyLock.holdCount;
_busyHolder = null;
for(i in 1..lockCount) for(i in 1..lockCount)
_busyLock.unlock(); _busyLock.unlock();
try { try {
@@ -283,9 +292,90 @@ class V8Plugin {
} }
finally { finally {
Logger.w(TAG, "Relocking V8 thread for [${config.name}] for a blocking resolve of a promise") Logger.w(TAG, "Relocking V8 thread for [${config.name}] for a blocking resolve of a promise")
var acquired = 0;
try {
for (i in 1..lockCount) {
acquireBusyOrThrow("unbusy(relock)");
acquired++;
}
_busyHolder = Thread.currentThread();
}
catch (timeout: Throwable) {
for (j in 1..acquired)
_busyLock.unlock();
throw timeout;
}
}
}
for(i in 1..lockCount) private fun acquireBusyOrThrow(context: String, maxWaitMs: Long = BUSY_FATAL_MS, allowUnwedge: Boolean = true) {
_busyLock.lock(); val warnAt = Math.min(BUSY_WARN_MS, maxWaitMs);
if (_busyLock.tryLock(warnAt, TimeUnit.MILLISECONDS))
return;
logBusyContention(context);
val remaining = maxWaitMs - warnAt;
if (remaining > 0 && _busyLock.tryLock(remaining, TimeUnit.MILLISECONDS))
return;
if (!allowUnwedge)
throw IllegalStateException("V8 busy lock for [${config.name}] timed out after ${maxWaitMs}ms in $context (fast-fail)");
unwedgeBusyHolder(context);
if (_busyLock.tryLock(BUSY_RECOVERY_MS, TimeUnit.MILLISECONDS))
return;
throw IllegalStateException("V8 busy lock for [${config.name}] timed out after ${maxWaitMs + BUSY_RECOVERY_MS}ms in $context; holder did not release after recovery");
}
private fun unwedgeBusyHolder(context: String) {
val holder = _busyHolder;
Logger.w(TAG, "V8 busy lock for [${config.name}] still held in $context after ${BUSY_FATAL_MS}ms; attempting to unwedge holder ${holder?.name ?: "unknown"}");
try {
val rt = _runtime;
if (rt != null && !rt.isClosed && !rt.isDead) {
Logger.w(TAG, "Calling terminateExecution() on [${config.name}] runtime");
rt.terminateExecution();
}
}
catch (ex: Throwable) {
Logger.e(TAG, "terminateExecution() failed for [${config.name}]", ex);
}
try {
holder?.interrupt();
}
catch (ex: Throwable) {
Logger.e(TAG, "Interrupting holder thread for [${config.name}] failed", ex);
}
}
private fun logBusyContention(context: String) {
try {
val holder = _busyHolder;
val sb = StringBuilder();
sb.append("V8 BUSY CONTENTION [${config.name}] in $context: queueLength=${_busyLock.queueLength}, holdCount=${_busyLock.holdCount}, waited>${BUSY_WARN_MS}ms\n");
if (holder != null) {
sb.append("Lock holder: ${holder.name} (id=${holder.id}, state=${holder.state})\n");
for (frame in holder.stackTrace.take(40))
sb.append(" at ").append(frame.toString()).append("\n");
} else {
sb.append("Lock holder unknown (likely already released or never set)\n");
}
sb.append("Suspect waiting/blocked threads:\n");
val cur = Thread.currentThread();
for ((thread, stack) in Thread.getAllStackTraces()) {
if (thread == cur || thread == holder) continue;
if (thread.state != Thread.State.WAITING && thread.state != Thread.State.BLOCKED && thread.state != Thread.State.TIMED_WAITING) continue;
if (stack.none {
val cn = it.className;
cn.contains("V8Plugin") || cn.contains("JSClient") || cn.contains("Extensions_V8")
|| cn.contains("Subscription") || cn.contains("PackageHttp") || cn.contains("JSPager")
|| cn.contains("JSContent")
}) continue;
sb.append(" ${thread.name} (state=${thread.state}):\n");
for (frame in stack.take(20))
sb.append(" at ").append(frame.toString()).append("\n");
}
Logger.w(TAG, sb.toString());
}
catch (ex: Throwable) {
Logger.e(TAG, "Failed to log busy contention", ex);
} }
} }
fun execute(js: String) : V8Value { fun execute(js: String) : V8Value {
@@ -430,6 +520,11 @@ class V8Plugin {
val TAG = "V8Plugin"; val TAG = "V8Plugin";
private const val BUSY_WARN_MS = 10_000L;
private const val BUSY_FATAL_MS = 60_000L;
private const val BUSY_RECOVERY_MS = 5_000L;
const val BUSY_STARTUP_MS = 5_000L;
fun getPluginFromRuntime(runtime: V8Runtime): V8Plugin? { fun getPluginFromRuntime(runtime: V8Runtime): V8Plugin? {
return _runtimeMap.getOrDefault(runtime, null); return _runtimeMap.getOrDefault(runtime, null);
} }
@@ -128,8 +128,10 @@ class PackageBridge : V8Package {
@V8Function @V8Function
fun dispose(value: V8Value) { fun dispose(value: V8Value) {
Logger.e(TAG, "Manual dispose: " + value.javaClass.name); Logger.e(TAG, "Manual dispose: " + value.javaClass.name);
_plugin.busy {
value.close(); value.close();
} }
}
var timeoutCounter = 0; var timeoutCounter = 0;
var timeoutMap = ConcurrentHashMap<Int, Any?>(); var timeoutMap = ConcurrentHashMap<Int, Any?>();
@@ -651,14 +651,17 @@ class PackageHttp: V8Package {
@V8Function @V8Function
fun connect(socketObj: V8ValueObject) { fun connect(socketObj: V8ValueObject) {
val hasOpen = socketObj.has("open"); val (hasOpen, hasMessage, hasClosing, hasClosed, hasFailure) = _package._plugin.busy {
val hasMessage = socketObj.has("message"); val open = socketObj.has("open");
val hasClosing = socketObj.has("closing"); val message = socketObj.has("message");
val hasClosed = socketObj.has("closed"); val closing = socketObj.has("closing");
val hasFailure = socketObj.has("failure"); val closed = socketObj.has("closed");
val failure = socketObj.has("failure");
socketObj.setWeak(); //We have to manage this lifecycle socketObj.setWeak(); //We have to manage this lifecycle
_listeners = socketObj; _listeners = socketObj;
Quintuple(open, message, closing, closed, failure);
};
_socket = _packageClient.logExceptions { _socket = _packageClient.logExceptions {
val client = _client; val client = _client;
@@ -666,52 +669,51 @@ class PackageHttp: V8Package {
override fun open() { override fun open() {
Logger.i(TAG, "Websocket opened: " + _url); Logger.i(TAG, "Websocket opened: " + _url);
_isOpen = true; _isOpen = true;
if(hasOpen && _listeners?.isClosed != true) {
try { try {
_package._plugin.busy { _package._plugin.busy {
if(hasOpen && _listeners?.isClosed != true) {
_listeners?.invokeV8Void("open", arrayOf<Any>()); _listeners?.invokeV8Void("open", arrayOf<Any>());
} }
} }
}
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex);
} }
} }
}
override fun message(msg: String) { override fun message(msg: String) {
if(hasMessage && _listeners?.isClosed != true) {
try { try {
_package._plugin.busy { _package._plugin.busy {
if(hasMessage && _listeners?.isClosed != true) {
_listeners?.invokeV8Void("message", msg); _listeners?.invokeV8Void("message", msg);
} }
} }
}
catch(ex: Throwable) {} catch(ex: Throwable) {}
} }
}
override fun closing(code: Int, reason: String) { override fun closing(code: Int, reason: String) {
if(hasClosing && _listeners?.isClosed != true)
{
try { try {
_package._plugin.busy { _package._plugin.busy {
if(hasClosing && _listeners?.isClosed != true) {
_listeners?.invokeV8Void("closing", code, reason); _listeners?.invokeV8Void("closing", code, reason);
} }
} }
}
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex);
} }
} }
}
override fun closed(code: Int, reason: String) { override fun closed(code: Int, reason: String) {
_isOpen = false; _isOpen = false;
if(hasClosed && _listeners?.isClosed != true) {
try { try {
_package._plugin.busy { _package._plugin.busy {
if(hasClosed && _listeners?.isClosed != true) {
_listeners?.invokeV8Void("closed", code, reason); _listeners?.invokeV8Void("closed", code, reason);
} }
} }
}
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
} }
}
Logger.w(TAG, "PackageHttp Socket removed"); Logger.w(TAG, "PackageHttp Socket removed");
synchronized(_package.aliveSockets) { synchronized(_package.aliveSockets) {
_package.aliveSockets.remove(this@SocketResult); _package.aliveSockets.remove(this@SocketResult);
@@ -720,17 +722,17 @@ class PackageHttp: V8Package {
override fun failure(exception: Throwable) { override fun failure(exception: Throwable) {
_isOpen = false; _isOpen = false;
Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception); Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception);
if(hasFailure && _listeners?.isClosed != true) {
try { try {
_package._plugin.busy { _package._plugin.busy {
if(hasFailure && _listeners?.isClosed != true) {
_listeners?.invokeV8Void("failure", exception.message); _listeners?.invokeV8Void("failure", exception.message);
} }
} }
}
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
} }
} }
}
}); });
}; };
} }
@@ -747,9 +749,19 @@ class PackageHttp: V8Package {
@V8Function @V8Function
fun close(code: Int?, reason: String?) { fun close(code: Int?, reason: String?) {
_socket?.close(code ?: 1000, reason ?: ""); _socket?.close(code ?: 1000, reason ?: "");
_package._plugin.busy {
_listeners?.close() _listeners?.close()
} }
} }
}
private data class Quintuple<A, B, C, D, E>(
val first: A,
val second: B,
val third: C,
val fourth: D,
val fifth: E
)
data class RequestDescriptor( data class RequestDescriptor(
val method: String, val method: String,
@@ -28,6 +28,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.DevJSClient import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
import com.futo.platformplayer.api.media.platforms.local.LocalClient import com.futo.platformplayer.api.media.platforms.local.LocalClient
import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
@@ -181,11 +182,14 @@ class StatePlatform {
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
var toDisables = mutableListOf<IPlatformClient>(); val toDispose = mutableListOf<IPlatformClient>();
var enabled: Array<String>; var enabled: Array<String>;
synchronized(_clientsLock) { synchronized(_clientsLock) {
for(e in _enabledClients) { val previousAvailable = _availableClients.toList();
toDisables.add(e); val reusableByDescriptor = HashMap<SourcePluginDescriptor, JSClient>();
for (prev in previousAvailable) {
if (prev is JSClient)
reusableByDescriptor[prev.descriptor] = prev;
} }
_enabledClients.clear(); _enabledClients.clear();
@@ -200,16 +204,26 @@ class StatePlatform {
for (plugin in StatePlugins.instance.getPlugins()) { for (plugin in StatePlugins.instance.getPlugins()) {
try { try {
val client = JSClient(context, plugin); val reused = reusableByDescriptor[plugin];
client.onCaptchaException.subscribe { c, ex -> val isReused = reused != null && reused.descriptor === plugin;
val client: JSClient = if (isReused) {
reused!!;
} else {
JSClient(context, plugin).also { fresh ->
fresh.onCaptchaException.subscribe { c, ex ->
StateApp.instance.handleCaptchaException(c, ex); StateApp.instance.handleCaptchaException(c, ex);
} }
}
}
_icons[plugin.config.id] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?: _icons[plugin.config.id] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?:
ImageVariable(plugin.config.absoluteIconUrl, null); ImageVariable(plugin.config.absoluteIconUrl, null);
_iconsByName[plugin.config.name.lowercase()] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?: _iconsByName[plugin.config.name.lowercase()] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?:
ImageVariable(plugin.config.absoluteIconUrl, null); ImageVariable(plugin.config.absoluteIconUrl, null);
_availableClients.add(client); _availableClients.add(client);
if (isReused)
reusableByDescriptor.remove(plugin);
} }
catch(ex: Throwable) { catch(ex: Throwable) {
Logger.e(TAG, "Failed to initialize plugin [${plugin.config.name}]", ex); Logger.e(TAG, "Failed to initialize plugin [${plugin.config.name}]", ex);
@@ -219,6 +233,8 @@ class StatePlatform {
} }
} }
toDispose.addAll(reusableByDescriptor.values);
if(_availableClients.distinctBy { it.id }.count() < _availableClients.size) { if(_availableClients.distinctBy { it.id }.count() < _availableClients.size) {
val dups = _availableClients.filter { x-> _availableClients.count { it.id == x.id } > 1 }; val dups = _availableClients.filter { x-> _availableClients.count { it.id == x.id } > 1 };
val overrideClients = _availableClients.distinctBy { it.id } val overrideClients = _availableClients.distinctBy { it.id }
@@ -244,7 +260,7 @@ class StatePlatform {
} }
selectClients(*enabled); selectClients(*enabled);
for(toDisable in toDisables) { for(toDisable in toDispose) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
try { try {
toDisable.disable(); toDisable.disable();
@@ -44,6 +44,7 @@ import kotlin.streams.asSequence
* Used to maintain subscriptions * Used to maintain subscriptions
*/ */
class StateSubscriptions { class StateSubscriptions {
private val _subscriptions = FragmentedStorage.storeJson<Subscription>("subscriptions") private val _subscriptions = FragmentedStorage.storeJson<Subscription>("subscriptions")
.withUnique { it.channel.url } .withUnique { it.channel.url }
.withRestore(object: ReconstructStore<Subscription>(){ .withRestore(object: ReconstructStore<Subscription>(){
@@ -40,6 +40,8 @@ import java.time.OffsetDateTime
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask import java.util.concurrent.ForkJoinTask
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
abstract class SubscriptionsTaskFetchAlgorithm( abstract class SubscriptionsTaskFetchAlgorithm(
@@ -125,7 +127,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
val timeTotal = measureTimeMillis { val timeTotal = measureTimeMillis {
for(task in forkTasks) { for(task in forkTasks) {
try { try {
val result = task.get(); val result = task.get(TASK_TIMEOUT_S, TimeUnit.SECONDS);
if(result != null) { if(result != null) {
if(result.pager != null) { if(result.pager != null) {
taskResults.add(result); taskResults.add(result);
@@ -148,6 +150,10 @@ abstract class SubscriptionsTaskFetchAlgorithm(
} else { } else {
throw ex.cause ?: ex; throw ex.cause ?: ex;
} }
} catch (ex: TimeoutException) {
Logger.w(TAG, "Subscription task timed out after ${TASK_TIMEOUT_S}s, abandoning");
task.cancel(true);
exs.add(ex);
}; };
} }
@@ -382,4 +388,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
val pager: IPager<IPlatformContent>?, val pager: IPager<IPlatformContent>?,
val exception: Throwable? val exception: Throwable?
) )
companion object {
private const val TASK_TIMEOUT_S = 90L;
}
} }