forked from openrs2/openrs2
Signed-off-by: Graham <gpe@openrs2.org>
parent
72e9107900
commit
f87d89fe7c
@ -0,0 +1,28 @@ |
|||||||
|
package org.openrs2.archive.cache |
||||||
|
|
||||||
|
import dev.openrs2.net.BootstrapFactory |
||||||
|
import dev.openrs2.net.suspend |
||||||
|
import javax.inject.Inject |
||||||
|
import javax.inject.Singleton |
||||||
|
import kotlin.coroutines.suspendCoroutine |
||||||
|
|
||||||
|
@Singleton |
||||||
|
public class CacheDownloader @Inject constructor( |
||||||
|
private val bootstrapFactory: BootstrapFactory, |
||||||
|
private val importer: CacheImporter |
||||||
|
) { |
||||||
|
public suspend fun download(hostname: String, port: Int, version: Int) { |
||||||
|
val group = bootstrapFactory.createEventLoopGroup() |
||||||
|
try { |
||||||
|
suspendCoroutine<Unit> { continuation -> |
||||||
|
val bootstrap = bootstrapFactory.createBootstrap(group) |
||||||
|
val handler = Js5ChannelHandler(bootstrap, hostname, port, version, continuation, importer) |
||||||
|
|
||||||
|
bootstrap.handler(Js5ChannelInitializer(handler)) |
||||||
|
.connect(hostname, port) |
||||||
|
} |
||||||
|
} finally { |
||||||
|
group.shutdownGracefully().suspend() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
package org.openrs2.archive.cache |
||||||
|
|
||||||
|
import com.github.ajalt.clikt.core.CliktCommand |
||||||
|
import com.google.inject.Guice |
||||||
|
import kotlinx.coroutines.runBlocking |
||||||
|
import org.openrs2.archive.ArchiveModule |
||||||
|
|
||||||
|
public class DownloadCommand : CliktCommand(name = "download") { |
||||||
|
override fun run(): Unit = runBlocking { |
||||||
|
val injector = Guice.createInjector(ArchiveModule) |
||||||
|
val downloader = injector.getInstance(CacheDownloader::class.java) |
||||||
|
// TODO(gpe): make these configurable and/or fetch from the database |
||||||
|
downloader.download("oldschool1.runescape.com", 43594, 193) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,176 @@ |
|||||||
|
package org.openrs2.archive.cache |
||||||
|
|
||||||
|
import io.netty.bootstrap.Bootstrap |
||||||
|
import io.netty.buffer.ByteBuf |
||||||
|
import io.netty.channel.ChannelHandler |
||||||
|
import io.netty.channel.ChannelHandlerContext |
||||||
|
import io.netty.channel.SimpleChannelInboundHandler |
||||||
|
import kotlinx.coroutines.runBlocking |
||||||
|
import org.openrs2.buffer.use |
||||||
|
import org.openrs2.cache.Js5Archive |
||||||
|
import org.openrs2.cache.Js5Compression |
||||||
|
import org.openrs2.cache.Js5Index |
||||||
|
import org.openrs2.cache.Js5MasterIndex |
||||||
|
import org.openrs2.protocol.Rs2Decoder |
||||||
|
import org.openrs2.protocol.Rs2Encoder |
||||||
|
import org.openrs2.protocol.js5.Js5Request |
||||||
|
import org.openrs2.protocol.js5.Js5RequestEncoder |
||||||
|
import org.openrs2.protocol.js5.Js5Response |
||||||
|
import org.openrs2.protocol.js5.Js5ResponseDecoder |
||||||
|
import org.openrs2.protocol.js5.XorDecoder |
||||||
|
import org.openrs2.protocol.login.LoginRequest |
||||||
|
import org.openrs2.protocol.login.LoginResponse |
||||||
|
import kotlin.coroutines.Continuation |
||||||
|
import kotlin.coroutines.resume |
||||||
|
import kotlin.coroutines.resumeWithException |
||||||
|
|
||||||
|
@ChannelHandler.Sharable |
||||||
|
public class Js5ChannelHandler( |
||||||
|
private val bootstrap: Bootstrap, |
||||||
|
private val hostname: String, |
||||||
|
private val port: Int, |
||||||
|
private var version: Int, |
||||||
|
private val continuation: Continuation<Unit>, |
||||||
|
private val importer: CacheImporter, |
||||||
|
private val maxInFlightRequests: Int = 200, |
||||||
|
maxVersionAttempts: Int = 10 |
||||||
|
) : SimpleChannelInboundHandler<Any>(Object::class.java) { |
||||||
|
private val maxVersion = version + maxVersionAttempts |
||||||
|
private val inFlightRequests = mutableSetOf<Js5Request.Group>() |
||||||
|
private val pendingRequests = ArrayDeque<Js5Request.Group>() |
||||||
|
private lateinit var indexes: Array<Js5Index?> |
||||||
|
private val groups = mutableListOf<CacheImporter.Group>() |
||||||
|
|
||||||
|
override fun channelActive(ctx: ChannelHandlerContext) { |
||||||
|
ctx.writeAndFlush(LoginRequest.InitJs5RemoteConnection(version), ctx.voidPromise()) |
||||||
|
ctx.read() |
||||||
|
} |
||||||
|
|
||||||
|
override fun channelRead0(ctx: ChannelHandlerContext, msg: Any) { |
||||||
|
when (msg) { |
||||||
|
is LoginResponse.Js5Ok -> handleOk(ctx) |
||||||
|
is LoginResponse.ClientOutOfDate -> handleClientOutOfDate(ctx) |
||||||
|
is LoginResponse -> throw Exception("Invalid response: $msg") |
||||||
|
is Js5Response -> handleResponse(ctx, msg) |
||||||
|
else -> throw Exception("Unknown message type: ${msg.javaClass.name}") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { |
||||||
|
releaseGroups() |
||||||
|
ctx.close() |
||||||
|
continuation.resumeWithException(cause) |
||||||
|
} |
||||||
|
|
||||||
|
private fun handleOk(ctx: ChannelHandlerContext) { |
||||||
|
val pipeline = ctx.pipeline() |
||||||
|
|
||||||
|
pipeline.remove(Rs2Encoder::class.java) |
||||||
|
pipeline.remove(Rs2Decoder::class.java) |
||||||
|
pipeline.addFirst( |
||||||
|
Js5RequestEncoder, |
||||||
|
XorDecoder(), |
||||||
|
Js5ResponseDecoder() |
||||||
|
) |
||||||
|
|
||||||
|
val request = Js5Request.Group(false, Js5Archive.ARCHIVESET, Js5Archive.ARCHIVESET) |
||||||
|
pendingRequests += request |
||||||
|
flushRequests(ctx) |
||||||
|
} |
||||||
|
|
||||||
|
private fun handleClientOutOfDate(ctx: ChannelHandlerContext) { |
||||||
|
if (++version > maxVersion) { |
||||||
|
throw Exception("Failed to identify current version") |
||||||
|
} |
||||||
|
|
||||||
|
ctx.close() |
||||||
|
bootstrap.connect(hostname, port) |
||||||
|
} |
||||||
|
|
||||||
|
private fun handleResponse(ctx: ChannelHandlerContext, response: Js5Response) { |
||||||
|
val request = Js5Request.Group(response.prefetch, response.archive, response.group) |
||||||
|
|
||||||
|
val removed = inFlightRequests.remove(request) |
||||||
|
if (!removed) { |
||||||
|
throw Exception("Received response for request not in-flight") |
||||||
|
} |
||||||
|
|
||||||
|
if (response.archive == Js5Archive.ARCHIVESET && response.group == Js5Archive.ARCHIVESET) { |
||||||
|
processMasterIndex(response.data) |
||||||
|
} else if (response.archive == Js5Archive.ARCHIVESET) { |
||||||
|
processIndex(response.group, response.data) |
||||||
|
} else { |
||||||
|
val version = indexes[response.archive]!![response.group]!!.version |
||||||
|
val encrypted = Js5Compression.isEncrypted(response.data.slice()) |
||||||
|
groups += CacheImporter.Group(response.archive, response.group, response.data.retain(), version, encrypted) |
||||||
|
} |
||||||
|
|
||||||
|
val complete = pendingRequests.isEmpty() && inFlightRequests.isEmpty() |
||||||
|
|
||||||
|
if (groups.size >= CacheImporter.BATCH_SIZE || complete) { |
||||||
|
runBlocking { |
||||||
|
importer.importGroups(groups) |
||||||
|
} |
||||||
|
|
||||||
|
releaseGroups() |
||||||
|
} |
||||||
|
|
||||||
|
if (complete) { |
||||||
|
ctx.close() |
||||||
|
continuation.resume(Unit) |
||||||
|
} else { |
||||||
|
flushRequests(ctx) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun processMasterIndex(buf: ByteBuf) { |
||||||
|
val masterIndex = Js5Compression.uncompress(buf.slice()).use { uncompressed -> |
||||||
|
Js5MasterIndex.read(uncompressed) |
||||||
|
} |
||||||
|
|
||||||
|
val rawIndexes = runBlocking { importer.importMasterIndexAndGetIndexes(masterIndex, buf) } |
||||||
|
try { |
||||||
|
indexes = arrayOfNulls(rawIndexes.size) |
||||||
|
|
||||||
|
for ((archive, index) in rawIndexes.withIndex()) { |
||||||
|
if (index != null) { |
||||||
|
processIndex(archive, index) |
||||||
|
} else { |
||||||
|
pendingRequests += Js5Request.Group(false, Js5Archive.ARCHIVESET, archive) |
||||||
|
} |
||||||
|
} |
||||||
|
} finally { |
||||||
|
rawIndexes.filterNotNull().forEach(ByteBuf::release) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun processIndex(archive: Int, buf: ByteBuf) { |
||||||
|
val index = Js5Compression.uncompress(buf.slice()).use { uncompressed -> |
||||||
|
Js5Index.read(uncompressed) |
||||||
|
} |
||||||
|
indexes[archive] = index |
||||||
|
|
||||||
|
val groups = runBlocking { |
||||||
|
importer.importIndexAndGetMissingGroups(archive, index, buf) |
||||||
|
} |
||||||
|
for (group in groups) { |
||||||
|
pendingRequests += Js5Request.Group(false, archive, group) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun flushRequests(ctx: ChannelHandlerContext) { |
||||||
|
while (inFlightRequests.size < maxInFlightRequests) { |
||||||
|
val request = pendingRequests.removeFirstOrNull() ?: break |
||||||
|
inFlightRequests += request |
||||||
|
ctx.write(request, ctx.voidPromise()) |
||||||
|
} |
||||||
|
|
||||||
|
ctx.flush() |
||||||
|
ctx.read() |
||||||
|
} |
||||||
|
|
||||||
|
private fun releaseGroups() { |
||||||
|
groups.forEach(CacheImporter.Group::release) |
||||||
|
groups.clear() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
package org.openrs2.archive.cache |
||||||
|
|
||||||
|
import io.netty.channel.Channel |
||||||
|
import io.netty.channel.ChannelInitializer |
||||||
|
import org.openrs2.protocol.Protocol |
||||||
|
import org.openrs2.protocol.Rs2Decoder |
||||||
|
import org.openrs2.protocol.Rs2Encoder |
||||||
|
import org.openrs2.protocol.login.ClientOutOfDateCodec |
||||||
|
import org.openrs2.protocol.login.InitJs5RemoteConnectionCodec |
||||||
|
import org.openrs2.protocol.login.IpLimitCodec |
||||||
|
import org.openrs2.protocol.login.Js5OkCodec |
||||||
|
import org.openrs2.protocol.login.ServerFullCodec |
||||||
|
|
||||||
|
public class Js5ChannelInitializer(private val handler: Js5ChannelHandler) : ChannelInitializer<Channel>() { |
||||||
|
override fun initChannel(ch: Channel) { |
||||||
|
ch.pipeline().addLast( |
||||||
|
Rs2Encoder(Protocol(InitJs5RemoteConnectionCodec)), |
||||||
|
Rs2Decoder(Protocol(Js5OkCodec, ClientOutOfDateCodec, IpLimitCodec, ServerFullCodec)), |
||||||
|
handler |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
plugins { |
||||||
|
`maven-publish` |
||||||
|
kotlin("jvm") |
||||||
|
} |
||||||
|
|
||||||
|
dependencies { |
||||||
|
api("com.google.inject:guice:${Versions.guice}") |
||||||
|
api("io.netty:netty-transport:${Versions.netty}") |
||||||
|
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlinCoroutines}") |
||||||
|
|
||||||
|
implementation(project(":buffer")) |
||||||
|
implementation("io.netty:netty-transport-native-epoll:${Versions.netty}:linux-aarch_64") |
||||||
|
implementation("io.netty:netty-transport-native-epoll:${Versions.netty}:linux-x86_64") |
||||||
|
implementation("io.netty:netty-transport-native-kqueue:${Versions.netty}:osx-x86_64") |
||||||
|
implementation("io.netty.incubator:netty-incubator-transport-native-io_uring:${Versions.nettyIoUring}:linux-x86_64") |
||||||
|
} |
||||||
|
|
||||||
|
publishing { |
||||||
|
publications.create<MavenPublication>("maven") { |
||||||
|
from(components["java"]) |
||||||
|
|
||||||
|
pom { |
||||||
|
packaging = "jar" |
||||||
|
name.set("OpenRS2 Network") |
||||||
|
description.set( |
||||||
|
""" |
||||||
|
Common Netty utility code. |
||||||
|
""".trimIndent() |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,74 @@ |
|||||||
|
package dev.openrs2.net |
||||||
|
|
||||||
|
import io.netty.bootstrap.Bootstrap |
||||||
|
import io.netty.bootstrap.ServerBootstrap |
||||||
|
import io.netty.buffer.ByteBufAllocator |
||||||
|
import io.netty.channel.ChannelOption |
||||||
|
import io.netty.channel.EventLoopGroup |
||||||
|
import io.netty.channel.epoll.Epoll |
||||||
|
import io.netty.channel.epoll.EpollEventLoopGroup |
||||||
|
import io.netty.channel.epoll.EpollServerSocketChannel |
||||||
|
import io.netty.channel.epoll.EpollSocketChannel |
||||||
|
import io.netty.channel.kqueue.KQueue |
||||||
|
import io.netty.channel.kqueue.KQueueEventLoopGroup |
||||||
|
import io.netty.channel.kqueue.KQueueServerSocketChannel |
||||||
|
import io.netty.channel.kqueue.KQueueSocketChannel |
||||||
|
import io.netty.channel.nio.NioEventLoopGroup |
||||||
|
import io.netty.channel.socket.nio.NioServerSocketChannel |
||||||
|
import io.netty.channel.socket.nio.NioSocketChannel |
||||||
|
import io.netty.incubator.channel.uring.IOUring |
||||||
|
import io.netty.incubator.channel.uring.IOUringEventLoopGroup |
||||||
|
import io.netty.incubator.channel.uring.IOUringServerSocketChannel |
||||||
|
import io.netty.incubator.channel.uring.IOUringSocketChannel |
||||||
|
import javax.inject.Inject |
||||||
|
import javax.inject.Singleton |
||||||
|
|
||||||
|
@Singleton |
||||||
|
public class BootstrapFactory @Inject constructor( |
||||||
|
private val alloc: ByteBufAllocator |
||||||
|
) { |
||||||
|
public fun createEventLoopGroup(): EventLoopGroup { |
||||||
|
return when { |
||||||
|
IOUring.isAvailable() -> IOUringEventLoopGroup() |
||||||
|
Epoll.isAvailable() -> EpollEventLoopGroup() |
||||||
|
KQueue.isAvailable() -> KQueueEventLoopGroup() |
||||||
|
else -> NioEventLoopGroup() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public fun createBootstrap(group: EventLoopGroup): Bootstrap { |
||||||
|
val channel = when (group) { |
||||||
|
is IOUringEventLoopGroup -> IOUringSocketChannel::class.java |
||||||
|
is EpollEventLoopGroup -> EpollSocketChannel::class.java |
||||||
|
is KQueueEventLoopGroup -> KQueueSocketChannel::class.java |
||||||
|
is NioEventLoopGroup -> NioSocketChannel::class.java |
||||||
|
else -> throw IllegalArgumentException("Unknown EventLoopGroup type") |
||||||
|
} |
||||||
|
|
||||||
|
return Bootstrap() |
||||||
|
.group(group) |
||||||
|
.channel(channel) |
||||||
|
.option(ChannelOption.ALLOCATOR, alloc) |
||||||
|
.option(ChannelOption.AUTO_READ, false) |
||||||
|
.option(ChannelOption.TCP_NODELAY, true) |
||||||
|
} |
||||||
|
|
||||||
|
public fun createServerBootstrap(group: EventLoopGroup): ServerBootstrap { |
||||||
|
val channel = when (group) { |
||||||
|
is IOUringEventLoopGroup -> IOUringServerSocketChannel::class.java |
||||||
|
is EpollEventLoopGroup -> EpollServerSocketChannel::class.java |
||||||
|
is KQueueEventLoopGroup -> KQueueServerSocketChannel::class.java |
||||||
|
is NioEventLoopGroup -> NioServerSocketChannel::class.java |
||||||
|
else -> throw IllegalArgumentException("Unknown EventLoopGroup type") |
||||||
|
} |
||||||
|
|
||||||
|
return ServerBootstrap() |
||||||
|
.group(group) |
||||||
|
.channel(channel) |
||||||
|
.option(ChannelOption.ALLOCATOR, alloc) |
||||||
|
.option(ChannelOption.AUTO_READ, false) |
||||||
|
.childOption(ChannelOption.ALLOCATOR, alloc) |
||||||
|
.childOption(ChannelOption.AUTO_READ, false) |
||||||
|
.childOption(ChannelOption.TCP_NODELAY, true) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
package dev.openrs2.net |
||||||
|
|
||||||
|
import io.netty.util.concurrent.Future |
||||||
|
import kotlin.coroutines.resume |
||||||
|
import kotlin.coroutines.resumeWithException |
||||||
|
import kotlin.coroutines.suspendCoroutine |
||||||
|
|
||||||
|
public suspend fun <V> Future<V>.suspend(): V { |
||||||
|
if (isDone) { |
||||||
|
if (isSuccess) { |
||||||
|
return now |
||||||
|
} else { |
||||||
|
throw cause() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return suspendCoroutine { continuation -> |
||||||
|
addListener { |
||||||
|
if (isSuccess) { |
||||||
|
continuation.resume(now) |
||||||
|
} else { |
||||||
|
continuation.resumeWithException(cause()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
package dev.openrs2.net |
||||||
|
|
||||||
|
import com.google.inject.AbstractModule |
||||||
|
import org.openrs2.buffer.BufferModule |
||||||
|
|
||||||
|
public object NetworkModule : AbstractModule() { |
||||||
|
override fun configure() { |
||||||
|
install(BufferModule) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue