Blog

Boot 3 というか micrometer の新しいやつが Exemplar 使えてめっちゃ良さそう。

https://ik.am/entries/715

tracing(zipkin とか) を使っていても、トラブルシューティングのタイミングで開くのって億劫で、なかなか開くことがなかった。 が、micrometer Observaiton を使うと、brave でやっていたような tracing をすると、同時に metrics も出すことができる。めっちゃ便利。

https://micrometer.io/docs/observation

しかも、Exemplars を使うと、prometheus の metrics に対して、その metrics に関連した trace id を一個付与することができる。 これを利用して、prom 側に tracing id を入れることができる。

https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#exemplars

grafana では Exemplars に対応しているので、metrics をグラフにレンダリングするときに Exemplars に指定した spanid もレンダリングされるようになって、そこから tracing にサッいけるようになるので、tracing がより有効活用できるようになると思う。そして、troubleshooting の際に、たとえば HTTP Server の latency が悪化した場合に、どこで悪化しているのかをスッと探せるようになるんじゃないかなぁ、と思っています。

https://grafana.com/docs/grafana/latest/fundamentals/exemplars/

なので、boot 3 へのアップグレードがなされれば、是非活用したいなーと思ってます(手元のプロダクトの Java 17 へのアップグレードから)。

kweb の router では KVar を意識しよう

kweb の router においては、

path("/entry/{id}") {params ->
    val id = params["id"]!!.value
    val entry = entryRepository.findById(id)
    div().text(entry.title)
}

のように書きたくなりがちだが、同一パスで path variable のみが変化した場合は KVar の変化しかおきなくて、path の callback のみが呼ばれるわけではない。

なので、以下の様に書く必要がある。

path("/entry/{id}") {params ->
    render(params["id"]!!) { id ->
        val entry = entryRepository.findById(id)
        div().text(entry.title)
    }
}

冒険ダンジョン村2

Android でプレイしました。

2週間ぐらいかけて75年目ぐらいまでやって、全村星5、周辺地域住民コンプ、街ガイド検定コンプまでやった。 ちまちま動くのが可愛いというカイロソフトの安定感があるゲームでした。

街をせっかくつくっても周辺地域住民とか街ガイドを効率的に埋めようとすると結局町並みを壊すことになるので、あんまきれいに並べようっていうモチベが薄いのと、街ごとにそこまで変わらないのでわりと後半飽きるなーという感じではありつつ。

kweb でファイルアップロードをハンドリングしたい

kweb を使ってインタラクティブなサイトを作っているときに、ファイルをアップロードする機能が欲しくなることがある。 そういう場合は KwebPlugin を継承して実装すればよい。appServerConfigurator を実装すると、ktor のコードを自由に書ける。ので、ここで簡単に設定できる。

class FileUploadPlugin : KwebPlugin() {
    override fun appServerConfigurator(routeHandler: Routing) {
        routeHandler.post(path) {
               // ここの中は ktor のコードとして書ける
        }
    }
}

ということは call.receiveStream() とかで InputStream を取れば好きに input できる。

JS 側では以下のようにする。window.fetch 使うとめっちゃ簡単にアップロードできて便利。

const response = await fetch("/upload_attachments", {
    method: "POST",
    body: item.getAsFile()
})

Rogue with dead というゲームをクリアした

rogue like だが、周回前提でジェムを集めるタイプ。アーティファクトという、死んでも持ち越せる強化アイテムを集めつつ、周回でもらえるジェムを集めて回していく。 パソコンはさわれないし集中して何かをできるわけでもないがスマホをなんとなく触れるという期間があったので、それを利用してプレイ。

放置ゲーとしてもプレイできるのだが、かなりアクティブめに周回回して4日ぐらいでクリアした。

課金が1500円で30日間広告なしで広告引いた時のリワードを受け取れるので、さっくり課金してプレイと言う感じ。無期限で広告アンロックするやつもあるのだが、30日を超えてプレイする感じはしなかったので30日の方で。課金したほうが圧倒的に楽に進められる。

この手の放置ゲー、Galaxy Z fold 4 で画面分割して半分でプレイしつつ、もう半分で twitter 眺めたり、もう片方で kindle 読んだりすると快適すぎるのでおすすめ。

トータルで 36時間ぐらいプレイしたっぽい。放置してるので実際にはそんなに長いこと触ってないけど。

Kotlin でかんたんにインタラクティブなウェブアプリを書ける kweb とは結局なんなのか?

一般的なウェブアプリというのは、クライアントサイドを JS で書いて、サーバーサイドを kotlin 等の別の言語で書く、といったアプローチが一般的です。 このアプローチになるのは、基本的にクライアントサイドでは JavaScript で書く必要があるからです。近年では JavaScript に transpile したりwasm で動かしたりといろんなアプローチが生まれています。 kotlin 界隈でも kotlin/js を react で動かすなど、様々なアプローチが行われています。

一般的なこの方式では、クライアントサイドとサーバーサイドでのコードの共有が難しかったり、接続部分のコーディングが面倒だったりします。

一方 kweb では、クライアントサイドで動くコードを書くから大変なんだ!クライアントサイドで動くコードを書くのが大変なら全部サーバーサイドで動かせばいいじゃない!というアプローチをとっています。 ブラウザ上でのイベント発生時にサーバー側に通信を行って、サーバー側で処理します。そして、サーバー側で DOM を操作すれば、それがクライアントサイドに伝搬する感じになっています。 これらの通信を都度都度行うと高コストなので、websocket で通信を張りっぱなしにしています。 これはなんというか、まぁ通信コスト等も高いですし、富豪的プログラミング だなぁ、という感じですね! 富豪的ではありつつも、めちゃくちゃ簡単にインタラクティブなウェブページを宣言的に書くことができて楽しいです。僕のこのブログの編集画面も kweb で書かれていますが、以前の spring webmvc で書かれていたバージョンに比べると圧倒的にいじりやすくなっていますし、SPA をめちゃくちゃ簡単に実装できるようになっています。

たとえば、簡単な web chat システムを書く場合、以下のように書くだけ。client side と server side の通信プロセスは隠蔽されています。ObservableList に入れたものは、それを watch しているページを表示しているクライアントがあれば、それらすべてに websocket で描画の更新内容が伝搬されます。 実際にどういうふうに通信されてるかは chrome の inspector などで見るのが良いです。

import kweb.ButtonType
import kweb.InputElement
import kweb.InputType
import kweb.Kweb
import kweb.button
import kweb.form
import kweb.input
import kweb.li
import kweb.new
import kweb.state.KVar
import kweb.state.ObservableList
import kweb.state.renderEach
import kweb.ul

val list = ObservableList<String>()

fun main() {
    Kweb(port = 16097) {
        doc.body.new {
            lateinit var inputElement: InputElement
            lateinit var input: KVar<String>

            form {
                inputElement = input(type = InputType.text)
                input = inputElement.value

                button(type = ButtonType.submit)
                    .text("Tweet")
            }.on(preventDefault = true).submit {
                list.add(0, input.value)
                inputElement.setValue("")
            }

            ul {
                renderEach(list) { msg ->
                    li().text(msg)
                }
            }
        }
    }
}

一方で、サーバーサイドで state を持つことによる弊害もあります。 現行バージョンではサーバー側が再起動した場合の挙動がまだちょっと怪しいかもです。あと、Load balancer で sticky session にしないといけないですね。同じインスタンスに常にいかないといけないので。 あとドキュメントがちょっと荒削りだったりとか、まだまだ伸びしろがあるフレームワークだなーという感じ。

業務で使うにはまだちょっと早いかな、という感じはしつつ、考え方は面白いかなーと思うので最近これで遊んでいるというワケ。 コードベースも非常に小さいんで気軽にイジれるし。

kweb で再利用可能なコンポーネントを使いたいばあいは kweb.state.Component を実装する

https://docs.kweb.io/book/components.html

kweb でサイトを作っている場合、複数画面で再利用可能なパーツが出来上がる時がある。そういう場合は、kweb.state.Component を継承させれば良い。

このブログで使っている編集フォームはエントリー作成画面とほぼ同一なので共通化してみたので以下のようになった。

package blog3.admin.form

import kweb.ButtonType
import kweb.Element
import kweb.ElementCreator
import kweb.button
import kweb.div
import kweb.form
import kweb.input
import kweb.option
import kweb.plugins.fomanticUI.fomantic
import kweb.select
import kweb.state.Component
import kweb.state.KVar
import kweb.textArea
import kweb.util.json

class EntryForm(
    private val initialTitle: String? = null,
    private val initialBody: String? = null,
    private val initialStatus: String = "draft",
    private val onSubmit: (title: String, body: String, status: String) -> Unit,
) : Component {
    override fun render(elementCreator: ElementCreator<Element>) {
        with(elementCreator) {
            lateinit var titleVar: KVar<String>
            lateinit var bodyVar: KVar<String>
            lateinit var statusVar: KVar<String>

            val form = form(fomantic.ui.form) {
                div(fomantic.field) {
                    titleVar = input(
                        initialValue = initialTitle,
                        name = "title",
                        attributes = mapOf("required" to true.json)
                    ).value
                }
                div(fomantic.field) {
                    val textArea = textArea(required = true, cols = 80, rows = 20)
                    textArea.text(initialBody.orEmpty())
                    bodyVar = textArea.value
                }
                div(fomantic.field) {
                    statusVar = select(required = true) {
                        listOf("draft", "published").forEach { status ->
                            option(mapOf("value" to status.json)) {
                                it.text(status)
                                if (initialStatus == status) {
                                    it.setAttributes("selected" to "selected".json)
                                }
                            }
                        }
                    }.value
                    statusVar.value = initialStatus
                }
                div(fomantic.field) {
                    button(fomantic.button, type = ButtonType.submit).text("Update")
                }
            }
            form.on(preventDefault = true).submit {
                println("SUBMIT! title=${titleVar.value} body=${bodyVar.value} status=${statusVar.value}")
                onSubmit(titleVar.value, bodyVar.value, statusVar.value)
            }
        }
    }
}

なお、select, input, required, initialValue の interface が統一感ないのは PR マージ済みなので次回以後のアップデートで修正される予定です。 全般、現在の kweb は実用にはちょっと早いかなという感じはしつつも、方向性は面白いなと思っているというのが現時点での感想。

selenium で処理を待つときは WebDriverWait を使う

kweb のテストケースが Awaitility 使ってたのでそれをそれを参考にテストを書いていたのだが問題が発生した。 https://github.com/kwebio/kweb-core/blob/master/src/test/kotlin/kweb/InputCheckedTest.kt#L49

        driver.get("http://localhost:$port/")

        val createLink = driver.findElement(By.className("createLink"))
        createLink.click()

        await().untilAsserted {
            URI(driver.currentUrl).path shouldBe "/create"
        }

例えばこんなふうにかいたら以下のようなエラーが発生する。selenium のインスタンスを生成したスレッド以外から selenium の情報を取ろうとしたらエラーになるようだ。

Thread safety error; this instance of WebDriver was constructed on thread Test worker (id 1) and is being accessed by thread awaitility-thread (id 92)This is not permitted and *will* cause undefined behaviour
Build info: version: '4.6.0', revision: '79f1c02ae20'
System info: os.name: 'Mac OS X', os.arch: 'aarch64', os.version: '12.6', java.version: '17.0.2'
Driver info: driver.version: unknown
org.openqa.selenium.WebDriverException: Thread safety error; this instance of WebDriver was constructed on thread Test worker (id 1) and is being accessed by thread awaitility-thread (id 92)This is not permitted and *will* cause undefined behaviour
Build info: version: '4.6.0', revision: '79f1c02ae20'
System info: os.name: 'Mac OS X', os.arch: 'aarch64', os.version: '12.6', java.version: '17.0.2'
Driver info: driver.version: unknown
	at app//org.openqa.selenium.support.ThreadGuard$WebDriverInvocationHandler.invoke(ThreadGuard.java:88)
	at app/jdk.proxy3/jdk.proxy3.$Proxy28.getCurrentUrl(Unknown Source)
	at app//InputCheckedTest.checkBeforeAndAfterClick$lambda$0(MyTest.kt:55)
	at app//org.awaitility.core.AssertionCondition.lambda$new$0(AssertionCondition.java:53)
	at app//org.awaitility.core.ConditionAwaiter$ConditionPoller.call(ConditionAwaiter.java:248)
	at app//org.awaitility.core.ConditionAwaiter$ConditionPoller.call(ConditionAwaiter.java:235)
	at [email protected]/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at [email protected]/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at [email protected]/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at [email protected]/java.lang.Thread.run(Thread.java:833)

2022-11-12 09:15:39.580 [Test worker] INFO  kweb.Kweb - Shutting down Kweb
2022-11-12 09:15:39.586 [Test worker] INFO  o.e.jetty.server.AbstractConnector - Stopped ServerConnector@4f824872{HTTP/1.1, (http/1.1, h2c)}{0.0.0.0:7660}

InputCheckedTest > checkBeforeAndAfterClick() FAILED

ということで、調べていたら以下のような情報を得た。 https://note.com/shift_tech/n/n6983acabb51a

というわけで以下のように書いたら、問題が解決した。

        val wait = WebDriverWait(driver, Duration.ofSeconds(5))
        wait.until {
            URI(driver.currentUrl).path == "/create"
        }
       URI(driver.currentUrl).path shouldBe "/create"

blog の管理画面を kweb で作り直した

作り直したので、k8s にデプロイするところまで完了。

もともと、/ 以下に読者が読むページ、 /admin/ 以下に 管理画面という構成の一つのサービスだったのだが、kweb で管理画面を作ろうと思うと、ユーザー側と管理画面を完全に分離する必要があった。なぜかというと、kweb は websocket で通信を全部管理する前提なので。

なので、もともと 8080 ポートで動いている spring boot アプリの中で 8180 ポートで動く ktor 版のアプリと 8280 ポートで動く kweb の admin 画面を入れてみた。 で、kweb 版の方を使えるように ingress-nginx を設定して、ingress-nginx 側で basic 認証をかけることとした。basic 認証も ingress-nginx でサッとかけられるの便利だねぇ。

あとは、ktor 側を見るようにユーザー画面を切り替えて spring boot 側のコードを消したりなんだりすると、また開発しやすくなるかなといったところ。

kweb で開発している上で、いくつか method が使いにくいものがあったので、報告したりなんだり。

kweb 自体、だいぶ荒削りで使いやすいとは言えないのだが、、このぐらい荒いプロダクトのほうがOSSとして使ってて楽しいみたいなところあるよね(ギョームで使えるレベルではまだないけど)。PRを送る余地がふんだんにあるもののほうが、OSS活動しやすいというアレ。

Integrate kweb and spring-boot

kweb provides the document to integrate spring-boot. https://docs.kweb.io/book/integrations.html#spring-boot

But in my opinion, it's bit complex and tricky way.

In my opinion, spring-boot users want to use the spring-boot's configuration loading feature and the bean management feature. But the sample code demonstrates the kweb integration with the spring-webmvc's servlet container.

Then, I wrote a sample code to use kweb on spring boot framework. https://github.com/tokuhirom/ktor-spring-boot-demo/blob/kweb-spring-boot-demo/src/main/kotlin/com/example/springbootdemo/SpringbootdemoApplication.kt

This code demonstrates, the code starts kweb server and it runs with spring-boot's beans and configurations. While the shutdown process, spring will call the "close" method on the Kweb instance!

Blog を SSL にした

let's encrypt で certbot で複数ドメインでやるのめんどくさくて https://64p.org/ だけ TLS 対応しているという状況を長らく続けていたのだが、k8s 化するついでに対応。

https://www.javachinna.com/deploy-angular-spring-boot-mysql-digitalocean-kubernetes/

を参考にして対応。

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.10.0/cert-manager.yaml

として cert-manager を入れる(digital ocean の web ui からも入れられるけど)

以下のようにして cert-manager を管理する。これも kubectl apply -f すれば OK

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    # Email address used for ACME registration
    email: [email protected]
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      # Name of a secret used to store the ACME account private key
      name: letsencrypt-prod-private-key
    # Add a single challenge solver, HTTP01 using nginx
    solvers:
    - http01:
        ingress:
          class: nginx
kubectl apply -f https://raw.githubusercontent.com/compumike/hairpin-proxy/v0.2.1/deploy.yml

として hairpin-proxy を入れる。

PROXY protocol support for internal-to-LoadBalancer traffic for Kubernetes Ingress users, specifically for cert-manager self-checks.

という感じのやつ(ref. https://github.com/compumike/hairpin-proxy )。

あとは kubectl apply -f k8s/ingress-nginx.yml などとして以下のファイルを適用すれば終わり。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: blog3-app-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
  - hosts:
    - blog.64p.org
    - 64p.org
    secretName: hp-64p-app-tls
  rules:
  - host: "blog.64p.org"
    http:
      paths:
      - pathType: Prefix
        path: /
        backend:
          service:
            name: blog3-app-server
            port:
              number: 8080
  - host: "64p.org"
    http:
      paths:
      - pathType: Prefix
        path: /
        backend:
          service:
            name: hp-64p-app-server
            port:
              number: 8080

blog に syntax highlight 機能をつけた

https://prismjs.com/

prism.js がナウいのかどうかはよくわからないけれど、とりあえず入れた。markdown のレンダリングに使っている flexmark が prism.js で使うように language-kotlin などの class をふってくれるので、そのまま読み込むだけ。

https://github.com/tokuhirom/blog3/pull/20/files

println("Hello")
s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s s s chr length q q each die local chown rmdir tie chr q semop and s s s chr length q qr our xor getc dump order goto log join eof write time crypt dump exit vec index open ord tied q semop and s s s chr length q q each die local chown rmdir tie chr dump fcntl warn ord eval q semop and s s s chr length q qr our break chop given given pack q semop and s s s chr length q qr our break chop given given pack q semop and s s s chr length q qr our break chop given given pack exit ord goto read undef q semop and s s s chr length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time bind tell each given q semop and s s s length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time bind tell our log q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie flock hex alarm undef cmp each ord glob ioctl untie die chr untie pack exp cmp given ref q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie flock hex alarm undef cmp each ord glob q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie flock hex alarm undef cmp each ord glob ioctl untie die chr link tie q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time bind fcntl q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time bind fcntl q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie flock hex alarm undef cmp each ord glob ioctl untie die chr untie pack exp cmp q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack q semop and eval eval

fish の greeting message を無効化する

set -U fish_greeting

とする。

検索用キーワード: fish welcome message

https://fishshell.com/docs/current/faq.html#faq-greeting

CPAN module を release するための docker image を作った

https://github.com/tokuhirom/docker-minil-release

CPAN module をリリースするには、色々と前準備が必要というか、、色々な CPAN module をインストールしなきゃいけなくてちょっと手間だった。最近は年に数回しかしないのだが、、リリースのたびに準備の時間がかかるので、ちょっと時間がかかって面倒なのだった。 面倒なので、ある程度設定した状態の VPS を維持していたのだが、それもなぁ、という。 このブログなども k8s に移行したので、VPS を維持する必要がなくなったのでその VPS を廃止したい。

本来なら github actions などからリリースできるようにしたいのだが、その手間もまぁまぁデカイということで、コスパないしタイパが良い範囲でやろうかなと。。 ということで、ある程度設定された状態の docker image を作っておこうかなと言う感じ。

実装の解説

FROM perl:latest

とすることにより、perl の docker image を利用する。正直、常に最新版を使ってリリースすりゃいいので、latest image を利用。

RUN apt update
RUN apt upgrade -y
RUN apt install -y vim

vim がはいってないと minil の機能である Changes の編集機能がつかえないので、vim を入れる。

ENV PERL_CPANM_OPT="--notest --no-man-pages --with-recommends --with-suggests --with-all-features --with-configure --with-develop"

ENV を設定して、とにかく依存っぽいやつをガンガン入れるように設定する。こうしないとテストコケることが多くなる。

続いて肝心の minilla を入れる。

RUN cpanm Minilla

あとは、インストールに時間かかる感じの依存をガンガン入れていく。

RUN cpanm Amon2
RUN cpanm DBD::SQLite LWP::Protocol::https DBD::mysql
RUN cpanm Catalyst::Runtime
RUN cpanm DBIx::Class
RUN cpanm Dist::Milla
RUN cpanm Tiffany
RUN cpanm Task::BeLike::TOKUHIROM
RUN cpanm Test::Perl::Critic Perl::Critic
RUN cpanm HTTP::Server::Simple::CGI Spiffy WWW::MobileCarrierJP

minilla はリリースプロセスの中で git commit するので、user.email, user.name を設定しておく。

RUN git config --global user.email "[email protected]"
RUN git config --global user.name "Tokuhiro Matsuno"

起動時に、/target/ にマウントされたソースコードを対象に、cpanm を使って依存を入れて、minil release が実行されるようにする。

WORKDIR /target/

CMD [ "/bin/bash", "-c", "cpanm --installdeps . && minil release" ]

利用方法

docker pull してイメージを取得する。

docker pull ghcr.io/tokuhirom/docker-minil-release:latest

あとは適当に volume を mount しながら実行するだけ。簡単。

docker run -it --rm -v $PWD:/target/ -v $HOME/.pause:/root/.pause -v $HOME/.ssh:/root/.ssh --name minil-release-app ghcr.io/tokuhirom/docker-minil-release:latest

k8s への移行

業務で k8s 化を推進しているので、趣味でも k8s にしようかなぁ、と。

デプロイしているコードはこのへん。Docker image をビルドするように変更している。

k8s の設定はこのへん。

思ったより簡単にできたが、デプロイ次に 502 になりまくるのがダメなのと TLS 関連の設定を真面目にやってないので暇をみてやらないと。

k8s 化したことによって構成管理がめちゃくちゃ楽になった一方で、どうしてもノードを3台ぐらいいれないとまともに動かないという話にはなっていて、個人で運用するにはまだまだびみょいな、という感じではある。が、そもそもブログを個人で運用する必要がそもそもないので、、という話ではあり。

wezterm への移行

iTerm2 がなんかもっさりするというのはあるので他のターミナルへの移行。sugarlife さんがおすすめしてくれたので移行してみる。

brew install wezterm で入った。

~/.config/wezterm/wezterm.lua にはとりあえず以下を記載してみた。

local wezterm = require 'wezterm';

return {
  use_ime = true,
  font_size = 14.0,
  -- https://wezfurlong.org/wezterm/colorschemes/index.html
  color_scheme = "OneHalfDark",
}

一旦これで十分動いているようだ。

Scraping でも kotlin を使いたい

groovy で scraping するという記事をたまたま見かけた。 ここで、JVM言語でのscraping という観点でいうと、groovy というものは優れているわけだが、、実は kotlin でも scripting は可能。 kotlin 本体の scripting サポートは 2022年10月現在、まだまだ開発途中でとても実用に耐えるものではない。。

https://kotlinlang.org/docs/custom-script-deps-tutorial.html

のだが、実は kscript というコマンドがあって、これを使えば先取りして kotlin scripting の世界を実用することが可能だ。

https://github.com/kscripting/kscript

mac の homebrew でインストールする場合、以下のようにすればかんたんに使う事ができる。

brew install holgerbrandl/tap/kscript

idea で開く

kscript --idea scraping.kts とすると、scraping.kts を開いた状態の IDEA が起動する。この状態からであればすぐにスクリプトを実行可能。便利〜 さきに touch scraping.kts などとしてファイルを作っておく必要があることに注意。

kscript --idea scraping.kts した時点で @file:DependsOn にかかれている依存については build.gradle.kts の中に記載されるため、補完がきく。そうでない場合は IDEA を開き直すか build.gradle.kts を手で編集しないと補完がきかないので注意。

上記ブログ記事のスクリプトを kotlin で実装してみる

kscript scraping.kts とすればすぐに実行もできる。

scraping.kts の中身は以下のように書く。groovy の grab のように依存もかけるので最高便利。

@file:DependsOn("org.jodd:jodd-http:6.2.1")
@file:DependsOn("org.jodd:jodd-lagarto:6.0.6")

import jodd.http.HttpRequest
import jodd.jerry.Jerry

data class Comment(
    val id: String,
    val user: String,
    val time: String,
    val comment: String,
    val parent: String?,
)

var url = args[0]

val commentList = mutableListOf<Comment>()

while (true) {
    println("Scraping $url")

    // Send the HTTP request to our URL
    val response = HttpRequest.get(url).send()

    // Parse the HTML document into a Jerry DOM object
    val doc = Jerry.of(response.bodyText())

    // Find all comments <tr>s in the main comment table
    val comments = doc.find("table.comment-tree tr.comtr")

    // Iterate over each comment and extract its data
    comments.forEach { element ->

        val id = element.attr("id")
        val user = element.find("a.hnuser").text()
        val time = element.find("span.age").attr("title")
        val comment = element.find("div.comment").text()
        val parent = element.find("a:contains(parent)").attr("href")

        // Append the data to comment_list
        commentList.add(Comment(id, user, time, comment, parent))
    }

    // If there is a next link, set the URL and continue the while, otherwise exit
    val next = doc.find("""a[rel="next"]""").attr("href")
    if (next != null) url = "https://news.ycombinator.com/$next"
    else break
}

println(commentList)

配布用パッケージの作成

kscript --package scraping.kts

とすると、java コマンドさえあれば実行できるバイナリを生成可能。このバイナリは mac でも linux でも動く。windows なら WSL の上でなら動くはず。

その実態は↓のような prefix を uber jar の先頭にくっつけるという感じ。

#!/usr/bin/env bash
exec java  -jar $0 "$@"

docopt の利用

docopt というライブラリを利用すると python の docopt のような感じでコマンドライン引数パーサーも定義可能。便利〜

#!/usr/bin/env kscript
@file:DependsOn("com.offbytwo:docopt:0.6.0.20150202", "log4j:log4j:1.2.14")

import org.docopt.Docopt
import java.util.*

val usage = """
Use this cool tool to do cool stuff
Usage: cooltool.kts [options] <igenome> <fastq_files>...

Options:
 --gtf <gtfFile>     Custom gtf file instead of igenome bundled copy
 --pc-only           Use protein coding genes only for mapping and quantification
"""

val doArgs = Docopt(usage).parse(args.toList())

println("Hello from Kotlin!")
println("Parsed script arguments are: \n" + doArgs)

Ruby の ARGF 的な利用

kscript-support-api を使うと、ARGF みたいなこともかんたんにできる。

kscript --text オプションを使うと以下のスクリプトが prepend されるので、lines という変数を使えばかんたんにテキスト処理できる。

@file:DependsOn("com.github.holgerbrandl:kscript-support-api:1.2.5")
        
import kscript.text.*
val lines = resolveArgFile(args)

外部コマンドの実行

kutils を使うとかんたんに外部コマンドを実行できる。便利。

@file:DependsOn("com.github.holgerbrandl:kutils:0.12")

import de.mpicbg.scicomp.kutils.evalBash

val result = evalBash("date")
println(result.exitCode)
println(result.sout())

include の利用

@file:Include("utils.kt") のように書くこともできるので、例えば、依存関係をコピペして維持するのがダルいって場合にはこれをつかって共通化することも可能だ。

ローカルパスの他に URL を指定することができる。

see also

Java で -Xmx 指定してない場合のデフォルトのヒープメモリ使用上限

Linux の 64bit 環境だと、32GBと物理メモリの1/4の少ない方が上限となる。

『Javaパフォーマンス』に載っている。この本自体古いので、最近の JVM だとまたちょっと違うかもしれない。

java -XX:+PrintFlagsFinal -version とかすると実際の値を確認できる。

Java 17 でインドネシア語の Locale の扱いが変わっている

Java 11

|  Welcome to JShell -- Version 11.0.16
|  For an introduction type: /help intro
 
jshell> new Locale("id")
$1 ==> in
 
jshell> new Locale("in")
$2 ==> in

Java 17

|  Welcome to JShell -- Version 17.0.4
|  For an introduction type: /help intro
 
jshell> new Locale("id")
$1 ==> id
 
jshell> new Locale("in")
$2 ==> id

Perl のワンライナーで複数行置換したい

BEGIN{undef $/;} を入れると良い。

perl -i -pe 'BEGIN{undef $/;} s/START.*STOP/replace_string/smg' file_to_change

参考: https://stackoverflow.com/questions/1030787/multiline-search-replace-with-perl