Skip to content

Commit bc1d1ab

Browse files
committed
fix(logging): demote 'no releases found' from W to D; add RpcException.code field; descriptive messages for no-result RPC cases
1 parent be1745c commit bc1d1ab

3 files changed

Lines changed: 142 additions & 95 deletions

File tree

  • core-getter
  • core-websdk/src/main/java/net/xzos/upgradeall/core/websdk/api/client_proxy

core-getter/rpc/src/main/java/net/xzos/upgradeall/getter/rpc/RpcClient.kt

Lines changed: 126 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package net.xzos.upgradeall.getter.rpc
22

33
import com.google.gson.Gson
4-
import com.google.gson.JsonElement
54
import com.google.gson.JsonObject
65
import com.google.gson.reflect.TypeToken
76
import io.ktor.client.*
@@ -18,74 +17,78 @@ import kotlin.time.Duration.Companion.seconds
1817

1918
/**
2019
* WebSocket-based JSON-RPC 2.0 client using Ktor.
21-
*
20+
*
2221
* This client maintains a persistent WebSocket connection and handles concurrent
2322
* JSON-RPC requests by matching request IDs with responses.
2423
*/
25-
class RpcClient(private val url: String) {
24+
class RpcClient(
25+
private val url: String,
26+
) {
2627
private val gson = Gson()
2728
private val requestId = AtomicLong(1)
28-
29-
private val client = HttpClient(CIO) {
30-
install(WebSockets) {
31-
pingInterval = 30.seconds
29+
30+
private val client =
31+
HttpClient(CIO) {
32+
install(WebSockets) {
33+
pingInterval = 30.seconds
34+
}
3235
}
33-
}
34-
36+
3537
private var sessionJob: Job? = null
3638
private var session: DefaultClientWebSocketSession? = null
3739
private val pendingRequests = ConcurrentHashMap<Long, CompletableDeferred<JsonObject>>()
3840
private val sessionMutex = Mutex()
3941
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
40-
42+
4143
@Volatile
4244
private var isConnected = false
43-
45+
4446
/**
4547
* Ensure WebSocket connection is established
4648
*/
4749
private suspend fun ensureConnected() {
4850
sessionMutex.withLock {
4951
if (isConnected && session != null) return@withLock
50-
52+
5153
// Close old session if exists
5254
session?.close()
5355
sessionJob?.cancel()
54-
56+
5557
// Parse URL (format: "ws://host:port" or "http://host:port")
5658
val wsUrl = url.replace("http://", "ws://").replace("https://", "wss://")
57-
59+
5860
try {
59-
sessionJob = scope.launch {
60-
client.webSocket(wsUrl) {
61-
session = this
62-
isConnected = true
63-
64-
// Start message receiver loop
65-
for (frame in incoming) {
66-
if (frame is Frame.Text) {
67-
val text = frame.readText()
68-
handleResponse(text)
61+
sessionJob =
62+
scope.launch {
63+
client.webSocket(wsUrl) {
64+
session = this
65+
isConnected = true
66+
67+
// Start message receiver loop
68+
for (frame in incoming) {
69+
if (frame is Frame.Text) {
70+
val text = frame.readText()
71+
handleResponse(text)
72+
}
6973
}
7074
}
75+
// Connection closed
76+
isConnected = false
77+
session = null
78+
79+
// Fail all pending requests
80+
val exception = RpcException("WebSocket connection closed")
81+
pendingRequests.values.forEach { it.completeExceptionally(exception) }
82+
pendingRequests.clear()
7183
}
72-
// Connection closed
73-
isConnected = false
74-
session = null
75-
76-
// Fail all pending requests
77-
val exception = RpcException("WebSocket connection closed")
78-
pendingRequests.values.forEach { it.completeExceptionally(exception) }
79-
pendingRequests.clear()
80-
}
81-
84+
8285
// Wait for connection to be established
8386
var attempts = 0
8487
while (!isConnected && attempts < 50) {
8588
delay(100)
8689
attempts++
8790
}
88-
91+
8992
if (!isConnected) {
9093
throw RpcException("Failed to connect to WebSocket server")
9194
}
@@ -96,24 +99,25 @@ class RpcClient(private val url: String) {
9699
}
97100
}
98101
}
99-
102+
100103
/**
101104
* Handle incoming JSON-RPC response
102105
*/
103106
private fun handleResponse(text: String) {
104107
try {
105108
val response = gson.fromJson(text, JsonObject::class.java)
106-
109+
107110
if (response.has("id") && !response.get("id").isJsonNull) {
108111
val id = response.get("id").asLong
109112
val deferred = pendingRequests.remove(id)
110-
113+
111114
if (deferred != null) {
112115
if (response.has("error") && !response.get("error").isJsonNull) {
113116
val error = response.getAsJsonObject("error")
114117
val message = error.get("message")?.asString ?: "Unknown error"
115118
val data = error.get("data")?.asString
116-
deferred.completeExceptionally(RpcException(message, data))
119+
val code = error.get("code")?.asInt ?: 0
120+
deferred.completeExceptionally(RpcException(message, data, code))
117121
} else {
118122
deferred.complete(response)
119123
}
@@ -123,34 +127,41 @@ class RpcClient(private val url: String) {
123127
// Ignore malformed responses
124128
}
125129
}
126-
130+
127131
/**
128132
* Invoke a JSON-RPC method with named parameters
129133
*/
130-
suspend fun <T> invoke(method: String, params: Map<String, Any?>, resultType: Type, timeoutMillis: Long = 60_000): T {
134+
suspend fun <T> invoke(
135+
method: String,
136+
params: Map<String, Any?>,
137+
resultType: Type,
138+
timeoutMillis: Long = 60_000,
139+
): T {
131140
ensureConnected()
132-
141+
133142
val id = requestId.getAndIncrement()
134-
val request = JsonObject().apply {
135-
addProperty("jsonrpc", "2.0")
136-
addProperty("method", method)
137-
addProperty("id", id)
138-
add("params", gson.toJsonTree(params))
139-
}
140-
143+
val request =
144+
JsonObject().apply {
145+
addProperty("jsonrpc", "2.0")
146+
addProperty("method", method)
147+
addProperty("id", id)
148+
add("params", gson.toJsonTree(params))
149+
}
150+
141151
val deferred = CompletableDeferred<JsonObject>()
142152
pendingRequests[id] = deferred
143-
153+
144154
return try {
145155
// Send request
146156
session?.send(Frame.Text(request.toString()))
147157
?: throw RpcException("WebSocket session is null")
148-
158+
149159
// Wait for response with timeout
150-
val response = withTimeout(timeoutMillis) {
151-
deferred.await()
152-
}
153-
160+
val response =
161+
withTimeout(timeoutMillis) {
162+
deferred.await()
163+
}
164+
154165
// Parse result
155166
val result = response.get("result")
156167
@Suppress("UNCHECKED_CAST")
@@ -165,33 +176,39 @@ class RpcClient(private val url: String) {
165176
throw RpcException("Request failed: ${e.message}", e.toString())
166177
}
167178
}
168-
179+
169180
/**
170181
* Invoke a JSON-RPC method without parameters
171182
*/
172-
suspend fun <T> invoke(method: String, resultType: Type, timeoutMillis: Long = 60_000): T {
183+
suspend fun <T> invoke(
184+
method: String,
185+
resultType: Type,
186+
timeoutMillis: Long = 60_000,
187+
): T {
173188
ensureConnected()
174-
189+
175190
val id = requestId.getAndIncrement()
176-
val request = JsonObject().apply {
177-
addProperty("jsonrpc", "2.0")
178-
addProperty("method", method)
179-
addProperty("id", id)
180-
}
181-
191+
val request =
192+
JsonObject().apply {
193+
addProperty("jsonrpc", "2.0")
194+
addProperty("method", method)
195+
addProperty("id", id)
196+
}
197+
182198
val deferred = CompletableDeferred<JsonObject>()
183199
pendingRequests[id] = deferred
184-
200+
185201
return try {
186202
// Send request
187203
session?.send(Frame.Text(request.toString()))
188204
?: throw RpcException("WebSocket session is null")
189-
205+
190206
// Wait for response with timeout
191-
val response = withTimeout(timeoutMillis) {
192-
deferred.await()
193-
}
194-
207+
val response =
208+
withTimeout(timeoutMillis) {
209+
deferred.await()
210+
}
211+
195212
// Parse result
196213
val result = response.get("result")
197214
@Suppress("UNCHECKED_CAST")
@@ -206,36 +223,42 @@ class RpcClient(private val url: String) {
206223
throw RpcException("Request failed: ${e.message}", e.toString())
207224
}
208225
}
209-
226+
210227
/**
211228
* Invoke a JSON-RPC method that returns nothing
212229
*/
213-
suspend fun invokeVoid(method: String, params: Map<String, Any?> = emptyMap(), timeoutMillis: Long = 60_000) {
230+
suspend fun invokeVoid(
231+
method: String,
232+
params: Map<String, Any?> = emptyMap(),
233+
timeoutMillis: Long = 60_000,
234+
) {
214235
ensureConnected()
215-
236+
216237
val id = requestId.getAndIncrement()
217-
val request = JsonObject().apply {
218-
addProperty("jsonrpc", "2.0")
219-
addProperty("method", method)
220-
addProperty("id", id)
221-
if (params.isNotEmpty()) {
222-
add("params", gson.toJsonTree(params))
238+
val request =
239+
JsonObject().apply {
240+
addProperty("jsonrpc", "2.0")
241+
addProperty("method", method)
242+
addProperty("id", id)
243+
if (params.isNotEmpty()) {
244+
add("params", gson.toJsonTree(params))
245+
}
223246
}
224-
}
225-
247+
226248
val deferred = CompletableDeferred<JsonObject>()
227249
pendingRequests[id] = deferred
228-
250+
229251
try {
230252
// Send request
231253
session?.send(Frame.Text(request.toString()))
232254
?: throw RpcException("WebSocket session is null")
233-
255+
234256
// Wait for response with timeout
235-
val response = withTimeout(timeoutMillis) {
236-
deferred.await()
237-
}
238-
257+
val response =
258+
withTimeout(timeoutMillis) {
259+
deferred.await()
260+
}
261+
239262
// Check for errors
240263
if (response.has("error") && !response.get("error").isJsonNull) {
241264
val error = response.getAsJsonObject("error")
@@ -253,7 +276,7 @@ class RpcClient(private val url: String) {
253276
throw RpcException("Request failed: ${e.message}", e.toString())
254277
}
255278
}
256-
279+
257280
/**
258281
* Close the WebSocket connection
259282
*/
@@ -266,7 +289,7 @@ class RpcClient(private val url: String) {
266289
sessionJob = null
267290
scope.cancel()
268291
client.close()
269-
292+
270293
// Fail all pending requests
271294
val exception = RpcException("Client closed")
272295
pendingRequests.values.forEach { it.completeExceptionally(exception) }
@@ -276,11 +299,21 @@ class RpcClient(private val url: String) {
276299
}
277300

278301
/**
279-
* JSON-RPC exception thrown when RPC call fails
302+
* JSON-RPC exception thrown when RPC call fails.
303+
*
304+
* [code] mirrors the JSON-RPC error code:
305+
* -32001 = no result / app not found (expected, not a bug)
306+
* -32600 = invalid request
307+
* -32601 = method not found
308+
* -32602 = invalid params
309+
* -32603 = internal error
310+
* -32700 = parse error
280311
*/
281-
class RpcException(message: String, val data: String? = null) : RuntimeException(
282-
if (data != null) "$message: $data" else message
283-
)
312+
class RpcException(
313+
message: String,
314+
val data: String? = null,
315+
val code: Int = 0,
316+
) : RuntimeException(if (data != null) "$message: $data" else message)
284317

285318
/**
286319
* Type token helper for generic types with Gson

core-getter/src/main/rust/getter

core-websdk/src/main/java/net/xzos/upgradeall/core/websdk/api/client_proxy/ClientProxyApi.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,21 @@ internal class ClientProxyApi : BaseApi {
7474
try {
7575
func()
7676
} catch (e: Throwable) {
77-
Log.e(logObjectTag, TAG, "$funcName: ${e.msg()}")
77+
// RpcException.code == -32001 means "no result" (hub doesn't support this app) —
78+
// this is expected during batch renew; log at debug level to avoid noise.
79+
val rpcCode =
80+
try {
81+
val f = e.javaClass.getDeclaredField("code")
82+
f.isAccessible = true
83+
f.getInt(e)
84+
} catch (ignored: Throwable) {
85+
0
86+
}
87+
if (rpcCode == -32001) {
88+
Log.d(logObjectTag, TAG, "$funcName: no result — ${e.message}")
89+
} else {
90+
Log.w(logObjectTag, TAG, "$funcName: ${e.msg()}")
91+
}
7892
null
7993
}
8094

0 commit comments

Comments
 (0)