Blog

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 java.base@17.0.2/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base@17.0.2/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base@17.0.2/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base@17.0.2/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: tokuhirom@gmail.com
    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 "tokuhirom@gmail.com"
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

krangl で unpivot(melt) する方法

R の dplyr みたいなデータ操作ができる kotlin ライブラリである krangl でデータの unpivot をしてみる。

    val df = dataFrameOf(
        "product", "error", "warn"
    )(
        "CMS", 400, 343,
        "Admin", 534, 834,
    )

    df.gather("product", "n", listOf("error", "warn"))
        .print()

krangl はドキュメントがあんま検索に引っかからないが、dplyr でできることはだいたいできるので、krangl でどうしたらいいんだろう。。ってなったときは dplyr でどうしたらいいんだろう?という気持ちでググって見るのが良さそう。

ref. https://holgerbrandl.github.io/kotlin4ds_kotlin_night_frankfurt//emerging_kotlin_ds_ecosystem.html#1

Dell U4021QW というウルトラワイドモニターを買った

モニター3枚ほど並べていたのだが配線がぐちゃぐちゃすぎてうんざりしたので、ウルトラワイドモニター一枚にしてみた。 かなりポートがたくさんついているので、これに接続するだけでほとんどの接続が集約されていて、快適です。USB ハブ機能があるのが便利。デスクが広く使える。

Image from Gyazo

色々つないでたんですが、いまは↑のあたりをつないでいます

not stripped の linux バイナリは strip したら小さくなる

↓のように、非常に大きい実行ファイルがあったとする。

-rwxr-xr-x 1 tokuhirom tokuhirom 30M Jun 23 00:37 /tmp/thrift-linux-x86_64*

こういう場合には file コマンドを実行すると良い。

> file /thrift-linux-x86_64
thrift-linux-x86_64: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=f12655e32dd035a15ec1b40b181c8bcc76c55e4e, with debug_info, not stripped

ここで気になるのは not stripped というところである。これは、シンボルテーブルがついていることを表している。シンボルテーブルは、デバッガでデバッグするときには便利だが、そういう用途が想定されない場合には不要である。

> strip thrift-linux-x86_64
> ls -lah thrift-linux-x86_64
-rwxr-xr-x 1 tokuhirom tokuhirom 3.7M Jun 23 00:42 thrift-linux-x86_64*

strip することで、30MB → 3.7MB と 8割程度の容量が削減される。

ref.

山善の昇降式デスクを買った

昇降式デスクというと、Flexispot が有名なのだが、Amazon で買えて一人でも古いデスクの回収もやってくれて、新しいデスクの組み立ても代行してくれるものという条件で探したらこれになった。 そこまで天板にこだわりないので、板を自分で選ぶとかめんどくさいなっていう感じ。

Android の Launcher は、AIO Launcher がライフチェンジングだった件

Android は Launcher を切り替えて、全く違うルックアンドフィールを体験できるのが強みの一つだと思う。

色々な Launcher を試してきたが、昨日見つけた AIO Launcher ってやつが最高便利なので紹介したい。

イメージとしては、iOS の左側の画面みたいな widget list だけのランチャーというのが近いです。その中にアプリリストウィジェットを埋めて使う感じ。

Notification が home の中にそのまま表示されるというのが新鮮。Notification いちいち開くの、めんどくさかったんですよね。 ウィジェットをホームのウィンドウごとにうまいこと並べる、みたいなのも必要なくなって、スーッと縦にスワイプしていけば、一通りのウィジェットを閲覧できて、今日の分のブリーフィング終わり。って感じ。

Image from Gyazo

Image from Gyazo

Image from Gyazo

あと、ちょっとわかりにくいんだけど、AIO Launcher 自体の設定はフローティングな Search ボタンを長押しすればOK.

ウィジェットは Android widget ももちろん使えるんだけど(多分、有料版 feature だけど)、独自のウィジェット機構があって、lua で拡張できるっぽい。まだ試してないけど。

なお、これ多分日本語には対応してないです。

あと、Galaxy Z fold 3 みたいなフォルダブルの端末で画面を閉じたり開いたりしても画面が崩れないのもいいです。

コレはほんとにオススメです。

armeria-grpc で io.grpc.StatusRuntimeException: UNKNOWN になるとき

サーバー側がエラーを返しているが、その理由を返していないときにこのエラーになる。

内部コンポーネント同士の通信の場合、サーバー側での問題を propagate してしまうことも可能。

armeria-grpc なら -Dcom.linecorp.armeria.verboseResponses=true すればよい。 https://armeria.dev/docs/server-grpc/#exception-propagation