added networking to the CLI

This commit is contained in:
Sergey Chernov 2026-04-02 20:25:44 +03:00
parent 9bee0aed5b
commit f61ac35580
5 changed files with 182 additions and 0 deletions

View File

@ -0,0 +1,23 @@
#!/env/bin lyng
import lyng.io.http
// Step 1: download the main lynglang.com page.
val home = Http.get("https://lynglang.com").text()
// Step 2: find the version-script reference in the page HTML.
val jsRef = "src=\"([^\"]*lyng-version\\.js)\"".re.find(home)
require(jsRef != null, "lyng-version.js reference not found on the homepage")
// Step 3: extract the referenced script path from the first regex capture.
val versionJsPath = jsRef[1]
// Step 4: download the script that exposes `window.LYNG_VERSION`.
val versionJs = Http.get("https://lynglang.com/" + versionJsPath).text()
// Step 5: pull the actual version string from the JavaScript source.
val versionMatch = "LYNG_VERSION\\s*=\\s*\"([^\"]+)\"".re.find(versionJs)
require(versionMatch != null, "LYNG_VERSION assignment not found")
// Step 6: print the discovered version for the user.
println("Lynglang.com version: " + ((versionMatch as RegexMatch)[1]))

View File

@ -14,6 +14,7 @@ firebaseCrashlyticsBuildtools = "3.0.3"
okioVersion = "3.10.2" okioVersion = "3.10.2"
compiler = "3.2.0-alpha11" compiler = "3.2.0-alpha11"
ktor = "3.3.1" ktor = "3.3.1"
slf4j = "2.0.17"
[libraries] [libraries]
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
@ -40,6 +41,7 @@ ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" } ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" }
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" } ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor" } ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor" }
slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "slf4j" }
[plugins] [plugins]
androidLibrary = { id = "com.android.library", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" }

View File

@ -80,6 +80,11 @@ kotlin {
val nativeMain by creating { val nativeMain by creating {
dependsOn(commonMain) dependsOn(commonMain)
} }
val jvmMain by getting {
dependencies {
implementation(libs.slf4j.nop)
}
}
val jvmTest by getting { val jvmTest by getting {
dependencies { dependencies {
implementation(kotlin("test")) implementation(kotlin("test"))

View File

@ -34,9 +34,15 @@ import net.sergeych.lyng.ScriptError
import net.sergeych.lyng.Source import net.sergeych.lyng.Source
import net.sergeych.lyng.io.console.createConsoleModule import net.sergeych.lyng.io.console.createConsoleModule
import net.sergeych.lyng.io.fs.createFs import net.sergeych.lyng.io.fs.createFs
import net.sergeych.lyng.io.http.createHttpModule
import net.sergeych.lyng.io.net.createNetModule
import net.sergeych.lyng.io.ws.createWsModule
import net.sergeych.lyng.obj.* import net.sergeych.lyng.obj.*
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
import net.sergeych.lyngio.http.security.PermitAllHttpAccessPolicy
import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy
import net.sergeych.lyngio.ws.security.PermitAllWsAccessPolicy
import net.sergeych.mp_tools.globalDefer import net.sergeych.mp_tools.globalDefer
import okio.FileSystem import okio.FileSystem
import okio.Path.Companion.toPath import okio.Path.Companion.toPath
@ -74,6 +80,11 @@ val baseScopeDefer = globalDefer {
// Install console access by default for interactive CLI scripts. // Install console access by default for interactive CLI scripts.
// Scripts still need to `import lyng.io.console` to use it. // Scripts still need to `import lyng.io.console` to use it.
createConsoleModule(PermitAllConsoleAccessPolicy, this) createConsoleModule(PermitAllConsoleAccessPolicy, this)
// Install network-oriented lyngio modules for CLI scripts.
// Scripts still need to import the modules they use explicitly.
createHttpModule(PermitAllHttpAccessPolicy, this)
createWsModule(PermitAllWsAccessPolicy, this)
createNetModule(PermitAllNetAccessPolicy, this)
} }
} }

View File

@ -0,0 +1,141 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng_cli
import com.sun.net.httpserver.HttpExchange
import com.sun.net.httpserver.HttpServer
import net.sergeych.jvmExitImpl
import net.sergeych.runMain
import org.junit.After
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import java.net.InetSocketAddress
class CliNetworkJvmTest {
private val originalOut: PrintStream = System.out
private val originalErr: PrintStream = System.err
private class TestExit(val code: Int) : RuntimeException()
@Before
fun setUp() {
jvmExitImpl = { code -> throw TestExit(code) }
}
@After
fun tearDown() {
System.setOut(originalOut)
System.setErr(originalErr)
jvmExitImpl = { code -> kotlin.system.exitProcess(code) }
}
private data class CliResult(val out: String, val err: String, val exitCode: Int?)
private fun runCli(vararg args: String): CliResult {
val outBuf = ByteArrayOutputStream()
val errBuf = ByteArrayOutputStream()
System.setOut(PrintStream(outBuf, true, Charsets.UTF_8))
System.setErr(PrintStream(errBuf, true, Charsets.UTF_8))
var exitCode: Int? = null
try {
runMain(arrayOf(*args))
} catch (e: TestExit) {
exitCode = e.code
} finally {
System.out.flush()
System.err.flush()
}
return CliResult(outBuf.toString("UTF-8"), errBuf.toString("UTF-8"), exitCode)
}
@Test
fun cliHasAllNetworkingModulesInstalled() {
val server = newServer()
try {
val script = """
import lyng.io.http
import lyng.io.ws
import lyng.io.net
assert(Http.isSupported())
println("ws=" + Ws.isSupported())
println("net=" + Net.isSupported())
val home = Http.get("http://127.0.0.1:${server.address.port}/").text()
val jsRef = "src=\"([^\"]*lyng-version\\.js)\"".re.find(home)
require(jsRef != null, "lyng-version.js reference not found")
val versionJsPath = (jsRef as RegexMatch)[1]
val versionJs = Http.get("http://127.0.0.1:${server.address.port}/" + versionJsPath).text()
val versionMatch = "LYNG_VERSION\\s*=\\s*\"([^\"]+)\"".re.find(versionJs)
require(versionMatch != null, "LYNG_VERSION assignment not found")
println("version=" + ((versionMatch as RegexMatch)[1]))
""".trimIndent()
val result = runCli("-x", script)
assertNull(result.exitCode)
assertTrue(result.err, result.err.isBlank())
assertTrue(result.out, result.out.contains("ws="))
assertTrue(result.out, result.out.contains("net="))
assertTrue(result.out, result.out.contains("version=9.9.9-test"))
} finally {
server.stop(0)
}
}
private fun newServer(): HttpServer {
val server = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0)
server.createContext("/") { exchange ->
when (exchange.requestURI.path) {
"/" -> writeResponse(
exchange,
200,
"""
<!doctype html>
<html>
<head>
<script src="lyng-version.js"></script>
</head>
<body>
<span id="lyng-version-ribbon"></span>
</body>
</html>
""".trimIndent()
)
"/lyng-version.js" -> writeResponse(exchange, 200, """window.LYNG_VERSION = "9.9.9-test";""")
else -> writeResponse(exchange, 404, "missing")
}
}
server.start()
return server
}
private fun writeResponse(exchange: HttpExchange, status: Int, body: String) {
val bytes = body.toByteArray()
exchange.sendResponseHeaders(status, bytes.size.toLong())
exchange.responseBody.use { out ->
out.write(bytes)
}
}
}