Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Provide better error handling with custom exceptions #11

Merged
merged 1 commit into from
Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions src/commonMain/kotlin/io/github/vyfor/kpresence/RichClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

package io.github.vyfor.kpresence

import io.github.vyfor.kpresence.exception.InvalidClientIdException
import io.github.vyfor.kpresence.ipc.*
import io.github.vyfor.kpresence.rpc.Activity
import io.github.vyfor.kpresence.rpc.Packet
Expand All @@ -17,7 +18,7 @@ import kotlinx.serialization.json.Json
* @property clientId The Discord application client ID.
*/
class RichClient(var clientId: Long) {
var state = State.DISCONNECTED
var connectionState = ConnectionState.DISCONNECTED
private set

private val connection = Connection()
Expand All @@ -32,13 +33,13 @@ class RichClient(var clientId: Long) {
* @return The current Client instance for chaining.
*/
fun connect(callback: (RichClient.() -> Unit)? = null): RichClient {
if (state != State.DISCONNECTED) {
if (connectionState != ConnectionState.DISCONNECTED) {
callback?.invoke(this)
return this
}

connection.open()
state = State.CONNECTED
connectionState = ConnectionState.CONNECTED
handshake()

callback?.invoke(this)
Expand All @@ -51,7 +52,7 @@ class RichClient(var clientId: Long) {
* @return The current Client instance for chaining.
*/
fun reconnect(): RichClient {
require(state != State.DISCONNECTED) { "Reconnection is not possible while disconnected." }
require(connectionState != ConnectionState.DISCONNECTED) { "Reconnection is not possible while disconnected." }

shutdown()
connect()
Expand All @@ -66,7 +67,7 @@ class RichClient(var clientId: Long) {
* @return The current Client instance for chaining.
*/
fun update(activity: Activity?): RichClient {
require(state == State.SENT_HANDSHAKE) { "Presence updates are not allowed while disconnected." }
require(connectionState == ConnectionState.SENT_HANDSHAKE) { "Presence updates are not allowed while disconnected." }
if (lastActivity == activity) return this
lastActivity = activity
val currentTime = epochMillis()
Expand All @@ -90,7 +91,7 @@ class RichClient(var clientId: Long) {
* @return The current Client instance for chaining.
*/
fun clear(): RichClient {
require(state == State.SENT_HANDSHAKE) { "Cannot clear presence while disconnected." }
require(connectionState == ConnectionState.SENT_HANDSHAKE) { "Cannot clear presence while disconnected." }
update(null)
return this
}
Expand All @@ -100,12 +101,12 @@ class RichClient(var clientId: Long) {
* @return The current Client instance for chaining.
*/
fun shutdown(): RichClient {
if (state == State.DISCONNECTED) return this
if (connectionState == ConnectionState.DISCONNECTED) return this
// TODO: Send valid payload
connection.write(2, "{\"v\": 1,\"client_id\":\"$clientId\"}")
connection.read()
connection.close()
state = State.DISCONNECTED
connectionState = ConnectionState.DISCONNECTED
updateTimer?.cancel()
updateTimer = null
lastActivity = null
Expand All @@ -114,7 +115,7 @@ class RichClient(var clientId: Long) {
}

private fun sendActivityUpdate() {
if (state != State.SENT_HANDSHAKE) return
if (connectionState != ConnectionState.SENT_HANDSHAKE) return
val packet = Json.encodeToString(Packet("SET_ACTIVITY", PacketArgs(getProcessId(), lastActivity), "-"))
connection.write(1, packet)
connection.read()
Expand All @@ -123,17 +124,17 @@ class RichClient(var clientId: Long) {
private fun handshake() {
connection.write(0, "{\"v\": 1,\"client_id\":\"$clientId\"}")
if (connection.read().decodeToString().contains("Invalid client ID")) {
throw RuntimeException("Provided invalid client ID: $clientId")
throw InvalidClientIdException("'$clientId' is not a valid client ID")
}
state = State.SENT_HANDSHAKE
connectionState = ConnectionState.SENT_HANDSHAKE
}

companion object {
private const val UPDATE_INTERVAL = 15000L
}
}

enum class State {
enum class ConnectionState {
DISCONNECTED,
CONNECTED,
SENT_HANDSHAKE,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@file:Suppress("unused", "MemberVisibilityCanBePrivate")

package io.github.vyfor.kpresence.exception

/**
* Exception thrown when a pipe does not exist.
*/
class PipeNotFoundException : Exception()

/**
* Exception thrown when there is a connection issue.
*
* @param exception The underlying exception that caused the connection issue.
*/
class ConnectionException(val exception: Exception) : Exception()

/**
* Exception thrown when a connection is unexpectedly closed.
*
* @param message The message indicating why the connection was closed.
*/
class ConnectionClosedException(message: String) : Exception(message)

/**
* Exception thrown when an invalid client ID is provided.
*
* @param message The message containing the invalid client ID.
*/
class InvalidClientIdException(message: String) : Exception(message)

/**
* Exception thrown when trying to perform an operation without connecting to Discord.
*/
class NotConnectedException : Exception("The connection has not yet been established")

/**
* Exception thrown when there is an issue reading from a pipe.
*
* @param message The message indicating the specific read issue.
*/
class PipeReadException(message: String) : Exception(message)

/**
* Exception thrown when there is an issue writing to a pipe.
*
* @param message The message indicating the specific write issue.
*/
class PipeWriteException(message: String) : Exception(message)
100 changes: 64 additions & 36 deletions src/jvmMain/kotlin/io/github/vyfor/kpresence/ipc/Connection.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package io.github.vyfor.kpresence.ipc

import io.github.vyfor.kpresence.exception.*
import io.github.vyfor.kpresence.utils.putInt
import io.github.vyfor.kpresence.utils.reverseBytes
import java.io.EOFException
import java.io.FileNotFoundException
import java.io.InputStream
import java.io.OutputStream
import java.io.RandomAccessFile
import java.lang.IllegalArgumentException
import java.lang.System.getenv
import java.net.UnixDomainSocketAddress
import java.nio.channels.Channels
import java.nio.channels.SocketChannel
import java.nio.file.InvalidPathException

actual class Connection {
private val con =
Expand Down Expand Up @@ -49,34 +53,45 @@ actual class Connection {
try {
pipe = RandomAccessFile("\\\\.\\pipe\\discord-ipc-$i", "rw")
return
} catch (_: Exception) {}
} catch (_: FileNotFoundException) {
} catch (e: Exception) {
throw ConnectionException(e)
}
}

throw RuntimeException("Could not connect to the pipe!")
throw PipeNotFoundException()
}

override fun read(): ByteArray {
pipe?.let { stream ->
stream.readInt()
val length = stream.readInt().reverseBytes()
val buffer = ByteArray(length)

stream.read(buffer, 0, length)
return buffer
} ?: throw IllegalStateException("Not connected")
try {
stream.readInt()
val length = stream.readInt().reverseBytes()
val buffer = ByteArray(length)

stream.read(buffer, 0, length)
return buffer
} catch (e: Exception) {
throw PipeReadException(e.message.orEmpty())
}
} ?: throw NotConnectedException()
}

override fun write(opcode: Int, data: String) {
pipe?.let { stream ->
val bytes = data.encodeToByteArray()
val buffer = ByteArray(bytes.size + 8)

buffer.putInt(opcode.reverseBytes())
buffer.putInt(bytes.size.reverseBytes(), 4)
bytes.copyInto(buffer, 8)

stream.write(buffer)
} ?: throw IllegalStateException("Not connected")
try {
val bytes = data.encodeToByteArray()
val buffer = ByteArray(bytes.size + 8)

buffer.putInt(opcode.reverseBytes())
buffer.putInt(bytes.size.reverseBytes(), 4)
bytes.copyInto(buffer, 8)

stream.write(buffer)
} catch (e: Exception) {
throw PipeWriteException(e.message.orEmpty())
}
} ?: throw NotConnectedException()
}

override fun close() {
Expand All @@ -103,40 +118,53 @@ actual class Connection {
inputStream = Channels.newInputStream(pipe!!)
outputStream = Channels.newOutputStream(pipe!!)
return
} catch (_: Exception) {}
} catch (_: InvalidPathException) {
} catch (_: IllegalArgumentException) {
} catch (e: Exception) {
throw ConnectionException(e)
}
}

throw RuntimeException("Could not connect to the pipe!")
throw PipeNotFoundException()
}

override fun read(): ByteArray {
inputStream?.let { stream ->
stream.readInt()
val length = stream.readInt()
val buffer = ByteArray(length)

stream.read(buffer, 0, length)
return buffer
} ?: throw IllegalStateException("Not connected")
try {
stream.readInt()
val length = stream.readInt()
val buffer = ByteArray(length)

stream.read(buffer, 0, length)
return buffer
} catch (e: Exception) {
throw PipeReadException(e.message.orEmpty())
}
} ?: throw NotConnectedException()
}

override fun write(opcode: Int, data: String) {
outputStream?.let { stream ->
val bytes = data.encodeToByteArray()
val buffer = ByteArray(bytes.size + 8)

buffer.putInt(opcode.reverseBytes())
buffer.putInt(bytes.size.reverseBytes(), 4)
bytes.copyInto(buffer, 8)

stream.write(buffer)
} ?: throw IllegalStateException("Not connected")
try {
val bytes = data.encodeToByteArray()
val buffer = ByteArray(bytes.size + 8)

buffer.putInt(opcode.reverseBytes())
buffer.putInt(bytes.size.reverseBytes(), 4)
bytes.copyInto(buffer, 8)

stream.write(buffer)
} catch (e: Exception) {
throw PipeWriteException(e.message.orEmpty())
}
} ?: throw NotConnectedException()
}

override fun close() {
pipe?.close()
inputStream?.close()
outputStream?.close()
pipe = null
}

private fun InputStream.readInt(): Int {
Expand Down
19 changes: 12 additions & 7 deletions src/linuxMain/kotlin/io/github/vyfor/kpresence/ipc/Connection.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.vyfor.kpresence.ipc

import io.github.vyfor.kpresence.exception.*
import io.github.vyfor.kpresence.utils.byteArrayToInt
import io.github.vyfor.kpresence.utils.putInt
import io.github.vyfor.kpresence.utils.reverseBytes
Expand All @@ -19,7 +20,7 @@ actual class Connection {
"/tmp"

val socket = socket(AF_UNIX, SOCK_STREAM, 0)
if (socket == -1) throw RuntimeException("Failed to create socket")
if (socket == -1) throw ConnectionException(Exception(strerror(errno)?.toKString().orEmpty()))

memScoped {
for (i in 0..9) {
Expand All @@ -32,16 +33,18 @@ actual class Connection {
if (err == 0) {
pipe = socket
return
} else if (errno != ENOENT) {
throw ConnectionException(Exception(strerror(errno)?.toKString().orEmpty()))
}
}
}

close(socket)
throw RuntimeException("Could not connect to the pipe!")
throw PipeNotFoundException()
}

actual fun read(): ByteArray {
if (pipe == -1) throw IllegalStateException("Not connected")
if (pipe == -1) throw NotConnectedException()

readBytes(4)
val length = readBytes(4).first.byteArrayToInt().reverseBytes()
Expand All @@ -52,7 +55,7 @@ actual class Connection {
}

actual fun write(opcode: Int, data: String) {
if (pipe == -1) throw IllegalStateException("Not connected")
if (pipe == -1) throw NotConnectedException()

val bytes = data.encodeToByteArray()
val buffer = ByteArray(bytes.size + 8)
Expand All @@ -65,23 +68,25 @@ actual class Connection {
if (bytesWritten < 0) {
close()

throw RuntimeException("Error writing to socket")
throw PipeWriteException(strerror(errno)?.toKString().orEmpty())
}
}

actual fun close() {
shutdown(pipe, SHUT_RDWR)
close(pipe)
pipe = -1
}

private fun readBytes(size: Int): Pair<ByteArray, Long> {
val bytes = ByteArray(size)
recv(pipe, bytes.refTo(0), bytes.size.convert(), 0).let { bytesRead ->
if (bytesRead < 0L) {
if (errno == EAGAIN) return bytes to 0
throw RuntimeException("Error reading from socket")
throw PipeReadException(strerror(errno)?.toKString().orEmpty())
} else if (bytesRead == 0L) {
close()
throw RuntimeException("Connection closed")
throw ConnectionClosedException(strerror(errno)?.toKString().orEmpty())
}
return bytes to bytesRead
}
Expand Down
Loading
Loading