WPP
ip a コマンドでIPv4アドレスを取り出すスクリプト

超小ネタです。

ip a するとたくさんのアダプタとそれぞれのアドレスが一覧で表示されますが、このままだとスクリプトで再利用する際にはちょっと都合が悪いときのワンライナーです。

例えば wlan0 の IPv4アドレスを取り出したいときはこんなふうに書けます。

$ ip a dev wlan0 |grep inet | awk '{print $2}' | awk -F '/' '{print $1}'

分解すると ip a show dev wlan0| gerp inet ではこんな感じの値が取得できます。

inet 192.168.1.111/24 brd 192.168.1.255 scope global wlan0

1つ目の awk は2番めのトークンを切り出すので

192.168.1.111/24

を出力します。

2つ目の awk では / を区切り文字として 1 番めを取り出すので

192.168.1.111

が手に入ります。これをスクリプト変数なりにいれるとスクリプトで使いまわすことができます。

makefile で使うには $1 は $$1 にする必要がある。

もともと Android デバイスを WiFiデバッグ接続しようとして調べたのだが、そもそも Android デバイスの IP アドレスをネットワーク経由で知る方法がないので無駄だったかもしれない。(IP アドレスを知るために USB デバッグで接続する必要があるのでもうつながっている。)

とはいえ、ip コマンドからアドレスだけを取り出したいシーンは結構あるので覚えておこう

Android が avahi 対応してくれるといいのだけれど…。多分されないでしょうね。

Android Studio でプロジェクト名を変更したら gralde プロジェクトが 2つになった

今日も Android Studio と格闘してます。

ちょっとプロジェクト名の typo が気になり安易に refactor したらちょっとハマりました。以前もハマったのですが全く対処方法を覚えていなかったのでメモ

ツリービューから Refactor でリネームしたのがそもそもの原因ですが、Refactor 後から gradle のシンクにエラーが出ます。

こんな感じでリネーム前の nfc01 と言う gradle プロジェクトが同期に失敗します。おもむろにファイル全体を検索しみますが、nfc01 はみつかりませんでした。

Android Studio をうろうろしていると全体の設定に変更する前の nfc01 がありました。

で、この nfc01 はどこから来たのかというと

.idea/gradle.xml にあるようです。

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="GradleMigrationSettings" migrationVersion="1" />
  <component name="GradleSettings">
    <option name="linkedExternalProjectsSettings">
      <GradleProjectSettings>
        <option name="externalProjectPath" value="/media/mnishi/opt/ws-jvc/nfc01" />
        <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
        <option name="modules">
          <set>
            <option value="/media/mnishi/opt/ws-jvc/nfc01" />
            <option value="/media/mnishi/opt/ws-jvc/nfc01/app" />
          </set>
        </option>
        <option name="resolveExternalAnnotations" value="false" />
      </GradleProjectSettings>
      <GradleProjectSettings>
        <option name="externalProjectPath" value="$PROJECT_DIR$" />
        <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
        <option name="modules">
          <set>
            <option value="/media/mnishi/opt/ws-jvc/nfc-01" />
            <option value="/media/mnishi/opt/ws-jvc/nfc-01/app" />
          </set>
        </option>
        <option name="resolveExternalAnnotations" value="false" />
      </GradleProjectSettings>
    </option>
  </component>
</project>

古いプロジェクト名である 6-17 行目を削除するとエラーはなくなりました。

こういうとこほんと良くないよ。些末なことですがこういうのでだんだん Android を嫌いになっていくのよ。

Android Kotlin インターフェイスの実装方法 3 パターン

さて引き続き Android プロジェクトをやっているわけですが、オブジェクトにリスナーを渡してコールバックを登録する処理があると思うのですが、そのコールバックがインターフェイスの場合の実装方法についての話です。

ひとことで言えば、インターフェイスは実装がないのでそのままでは変数にできないのでどうしよう ? ってときの定形のパターンです。

実装したクラスを作るパターン

素直にクラスを実装して渡すとこんな感じに書けます。

public interface ReadListener {
  fun onSuccess(message: String)
}

class MyReadListener: ReadListener {
  override fun onSuccess(message: String) {
    // do something
  }
}

val listener = MyReadListener()
target.addReadListener(listener)

クラス作るのはちょっと大げさなときはリスナーを登録する際に、定義する方法があるようだ。

登録するときに実装してしまうパターン

こっちのほうがよく見ると思う。そもそも変数いらなくねってパターン。

target.addReadListener(object: ReadListener {
  override fun onSuccess(message: String) {
    // do something
  }
})

object でiインターフェイスの型を指定すると {} で定義ができるっていうふうに解釈しましたがあっているのか?ちょっと不安ではあるが現時点の知識ではここまでが自分の限界。

変数に無名クラスで実装するパターン

調べていくと、パターン1 を無名クラスにする方法もあるようで今回はリスナー後で使いまわしたかったのでこのパターンにした。

val myListener = object: ReadListener {
  override fun onSuccess(message: String)
}
target.addREadListener(myListerner)

このへんのパターンは武道の型のように何度もやって覚えるしかないっていうのが個人的な感想。

WordPress の “Highlighting Code Block” で対応言語を追加する。

このブログのコードブロックでは “Highlighting Code Block” というプラグインを使っています。

がデフォルトで対応する言語はあまり多くありません。公式の説明では追加方法がわかりにくかったので調べてみまみました。

「ひいらぎナレッジ倉庫」さんのサイトによると新しく対応したい言語を追加したバージョンの Prism.js /Prism.css を追加する必要がありそうです。

ダウンロードとワードプレスへのアップロード

  1. WordPress で [設定] – [[HCB] 設定] に進み、ヘルプの 「こちら」 へリンクに進みます。
  2. Prism のダウンロードページが開くので、必要な言語をチェックしていきます。
  3. ページの下に進み、.js をダウンロードします。(.css は自分の環境では必要なさそうでした。)
  4. [HCB] 設定の [独自 Prism.js] フォルダの指し先に合うようにファイルをアップロードします。
    今回は [FQDN]/[ワードプレストップ]/wp-content/theme/[使用中のテーマ]/prism/prism.js
    になる場所にアップロードしました。
  5. 4. に合わせて [独自 Prism.js] にパスを指定します。
  6. [変更の保存] をクリックします。

対応言語の追加

今回は、XML と Kotlin を追加します。

同じく [HCB] 設定の [使用する言語セット] に以下の行を追加します。もともとの末尾に , がない場合はエラーになるので末尾に追加します。(PHP のハッシュや配列の場合、最後の要素に , が付いていてもエラーにならないので最初からつけておいたほうが簡単です。)

Kotlin: “Kotlin”,
xml: “XML”

これで、 Kotlin と XML を選択してハイライトできるようになった。

参考

Highlighting Code Blockで対応言語を追加する方法 | ひいらぎナレッジ倉庫

Android Kotlin の ViewModel で Model の変更を受け取る -1

今回は珍しく長いです。

先日から Android のタスクにまた取り組んでいます。リハビリをかねて Jetpack compose を勉強しています。

自分の理解では、@composable な関数は外から状態を変更できないので、何らかの方法で外部に状態を維持しなければなりません。

そうなると MVVM って話になるようなのですが、Model と ViewModel の連携についてはあまりいいサンプルが見つけられなかった。ネットに転がっているサンプルでは UI 操作で ViewModel の変更が完結しているものがほとんどです。このパターンでは Model が View をまたぐ場合のことは触れていないので自分で考えなければいけません。r

結局のところ、モデルをビュー(Activity) の外に置くのが一番簡単な解決策じゃないかと思います。

参考リンクが唯一、Model から ViewModel に更新を通知する例を見つけられたのでそれをそのまま写経してみた。

目次

前提

  • Jetpack Compose
  • Kotlin
  • Android Studio
  • Android Studio Koala | 2024.1.1 Patch 1
  • Build #AI-241.18034.62.2411.12071903, built on July 11, 2024

基本のビューモデル

まずは、compose を使って ViewModel の LiveData を UI に通知するパターンをつくる。 ついでに ViewModel を Model の一部とみなして Activity の外で宣言する例。ViewModel は別ファイルに分けるべきですが面倒なのでそのままです。

package xxx.yyy.logpanecompose

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import red.txn.logpanecompose.ui.theme.LogPaneComposeTheme

class MainActivity : ComponentActivity() {

    val vm = MyApp.getInstance().getViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

//        val vm = LogViewModel()

        setContent {
            LogPaneComposeTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    LogPane(vm=vm, modifier = Modifier.padding(innerPadding))
//                    LogPane(modifier=Modifier.padding(innerPadding))
                }
            }
        }
    }
}

class LogViewModel(): ViewModel() {
    val buffer = MutableLiveData("")

    private var count: Int = 0

    fun append(newLine: String) {
        buffer.value = buffer.value + newLine + count.toString() + "\n"
        count++
    }
}

@Composable
fun LogPane(
    modifier: Modifier=Modifier,
    vm: LogViewModel = viewModel()
) {
    val text: String by vm.buffer.observeAsState("")
    Column(modifier=modifier.padding(horizontal=16.dp)) {
        Button(
            onClick={vm.append("aZZZ")},
            modifier=modifier.padding(top=0.dp, bottom=4.dp )
        )
        {
            Text(text="add log")
        }
        TextField(
            value=text,
            onValueChange = { },
            singleLine = false,
            modifier = modifier.fillMaxSize()
        )
    }
}

@Preview(showBackground = true)
@Composable
fun LogPanePreview() {
    LogPaneComposeTheme {
        LogPane()
    }
}

MainActivity の先頭でアプリケーションクラスから ViewModel を持ってきています。

独自のアプリケーションクラスを書きます

package xxx.yyy.logpanecompose

import android.app.Application

class MyApp: Application() {
    public val vm = LogViewModel()  // private にするべきかも

    companion object {
        private var instance: MyApp? = null

        fun getInstance(): MyApp {
            if (instance == null) {
                instance = MyApp()
            }
            return instance!!
        }
    }

    public fun getViewModel() = vm
}

MyApp を使うように AndroidManifest.xml を変更します。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:name=".MyApp"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.LogPaneCompose"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.LogPaneCompose">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

LiveData でモデルの変更を受け取る

Model に LiveData を追加して ViewModel で監視するには下のように Model を追加し、 ViewModel を変更する。

// 省略

data class LogModel(val message: String) {
    val buffer = MutableLiveData(message)

    fun append(newMessage: String) {
        buffer.value += newMessage + "\n"
    }
}

class LogViewModel(val model: LogModel): ViewModel() {
    var buffer = MutableLiveData("")

    private val lifecycleOwner = CustomLifecycleOwner()

    init {
        lifecycleOwner.start()
        model.buffer.observe(lifecycleOwner) {
            println("viewmodel: $it")
            buffer.value = it
        }
    }

    fun append(newMessage: String) {
        model.append(newMessage)
    }

    inner class CustomLifecycleOwner: LifecycleOwner {
        private val registry = LifecycleRegistry(this)

        override val lifecycle: Lifecycle
            get() = registry

        fun start() {
            registry.currentState = Lifecycle.State.STARTED
        }

        @Suppress("unused")
        fun stop() {
            registry.currentState = Lifecycle.State.CREATED
        }
    }
}

MyApp もモデルを宣言するように変更する

package xxx.yyy.logpanecompose

import android.app.Application

class MyApp: Application() {
    val model = LogModel("")
    val vm = LogViewModel(model=model)

    companion object {
        private var instance: MyApp? = null

        fun getInstance(): MyApp {
            if (instance == null) {
                instance = MyApp()
            }
            return instance!!
        }
    }

    fun getViewModel() = vm
}

StateFlow でモデルの変更を受け取る

Model、ViewModel 層で LiveData を使うのはあまりよくないらしいとのことで、StateFlow パターンもやってみる。

// いろいろ省略

data class LogModel(val message: String) {
//    val buffer = MutableLiveData(message)
    val buffer = MutableStateFlow(message)

    fun append(newMessage: String) {
        buffer.value += newMessage + "\n"
    }
}

class LogViewModel(val model: LogModel): ViewModel() {
    var buffer = MutableLiveData("")

    init {
        viewModelScope.launch(Dispatchers.IO) {
            model.buffer.collect {
                println("viewmodel: $it")
                buffer.postValue(it)
            }
        }
    }

    fun append(newMessage: String) {
        model.append(newMessage)
    }
}

参考

僕たちはどのようにしてViewModelに通知すべきなのか #Android - Qiita
Linux Mint 22 で RabbitVCS を使う

さて、今度のプロジェクトでは Subversion (SVN) を使うようなので GUI クライアントの RabbitVCS をインストールしてみた。本体 ? のインストールはパッケージマネージャでインストールできるがそれだけでは、Nemo で TortoiseSVN みたいなことはできない。

パッケージのインストールはこんな感じ

$ sudo apt install rabbitvcs-core

これだけだと Nemo になんの変化もない。 >> つまり使うことができない。

参考リンクの Github のページから RabbitVCS.py をダウンロードして Nemo が認識できるようにする必要がある。

説明に従い /usr/share/nemo-python/extentsions にコピーする

$ sudo cp ./RabbitVCS.py /usr/share/nemo-python/extensions

$ nemo -q
$ pgrep -f service.py | xargs kill
$ nohup nemo > /dev/null &

自分の環境では、該当するプロセスがな存在しないようで xargs kill はエラーになった。心配なら再起動なり、X から一旦ログインすれば多分大丈夫。

最後の nohup nemo … は RabbitVCS のログがコンソールに流れないようになるようだ。普通に Nemo を起動しても動作に支障はなさそう。

使ってみて思い出したが、そもそも TortoiseSVN の動きが感覚的に好きでないのと、右クリックするとップアップメニューに RabbitVCS の項目が追加されるのだが、かなりの頻度でメニューの改変がされないことがありストレスを感じた。この現象は、PCのスペック不足が原因かもしれないので問題ない人は気にしなくていい。

で結局削除して kdesvn をインストールすることにした。こっちは、SrouceTree のような感じといったらわかりやすいだろうか。

参考

[rabbitvcs/clients/nemo/README at master · rabbitvcs/rabbitvcs](https://github.com/rabbitvcs/rabbitvcs/blob/master/clients/nemo/README)

Linux Mint で btrfs にスワップファイルを作成する

Android エミュレータを起動するとメモリフルで張り付いてしまった。確認してみるとスワップファイルがなかったので再作成した話。

わかってしまえば簡単なのですが、btrfs の場合 ext4 の操作に追加して行うことがあるらしい。

この例では、/ が btrfs パーティションです。最初の 3 行を予め実行しておかないと swapon で
”swapon: swapfile2: swapon failed: Invalid argument” とエラーが出てスワップ領域が有効になりません。

# truncate -s 0 /swapfile
# chattr +C /swapfile
# btrfs property set /swapfile compression none
# fallocate -l 16G /swapfile
# chomod 600 /swapfile
# mkswap /swapfile
# swapon /swapfile

参考

linux – Swapfile Swapon invalid argument – Unix & Linux Stack Exchange

Btrfs#スワップファイル – ArchWiki

Linux Mint 21にスワップスペースを追加する方法 >> ext4 ならこれだけでいい。

Android ワイアレスデバッグのショートカット設定

また、Android アプリの開発に戻ってきたがあまり好きになれないのは相変わらず。そんなことはさておき、wireless debug の設定画面が深すぎて面倒なのでショートカットを探した話。

しょーもない小ネタではあるのだが、自分用のメモとして残す。

開発者設定の [クイック設定開発者用タイル] で[ワイヤレスでバッグ] をオンにすると、ショートカットが設定できる。端末のメーカーによってはこの設定項目がないらしい。

設定をオンにするとどの画面でも上からスワイプで出てくる画面(これをクイックタイルと呼ぶ?)で切り替えられるようになる。

Synology NAS で Forgejo をちょっと試してみる

自宅 NAS では Gitlab を動かしています。これは機能しているのでいいといえばいいのですが、遅いです。それほど強力でない NAS 上の docker とはいえ起動して使い始めるまで大体 5 分位かかります。

それから、Gitlab のアップデートは異常に早いです。大抵 2、3 週間放置しているとアップデートされ、critical update も非常に頻繁です。そのたびにアップデートするのですが正直面倒なのと失敗するかもしれないというドキドキはあまり嬉しいものではありません。

なのでちょっと違うものを試してみることにしました。Gitea が良さそうと思いましたが、更に Forgejo が fork されているようです。なので Forgejo を動かしてみることにします。

Forgejo は dockerhub でホストされていないようなので、Project を作成します。

Create Project ダイアログは以下のように指定します。

Project name: 適当な名前
Path: NAS 上の composer.yaml を置く場所
Source: docker-compose.yaml の作成を選択する
テキストボックスに docker-compose.yaml の内容を指定して [Next] へ進む
Web ポータルの設定はせずに先に進む。

version: '3'

networks:
  forgejo:
    external: false

services:
  server:
    image: codeberg.org/forgejo/forgejo:7
    container_name: forgejo
    environment:
      - USER_UID=1000
      - USER_GID=1000
    restart: always
    networks:
      - forgejo
    volumes:
      - /volume1/docker/forgejo:/data
      #- /etc/timezone:/etc/timezone:ro
      #- /etc/localtime:/etc/localtime:ro
    ports:
      - '8002:3000'
      - '8003:22'

ポート番号は適当に変えています。適宜、状況に合わせて調整してください。

リバースプロキシ設定で https://forgejo.xxx.myds.me を 8002 に流して https の設定は終わり。

ssh 接続は確認していないので後ほど追記することになると思う。

参考

Installation with Docker | Forgejo – Beyond coding. We forge.

synology の Docker で code-server を動かす

タブレットをせっかく買ったので個人開発をタブレットでもできるようにということでいろいろと調べてみる。

coder っていう Web IDE が見つかり、それはどうやら VS Code を動かしているものっぽい。その coder が出している code-server ってので Docker で動くらしいのでやってみた。

自分の環境では、Docker と Synology NAS のリバースプロキシを使っているが Docker だけの場合は R.P. 部分は読み飛ばせばいい。リバースプロキシで SSL しているので Docker 側では何もしていない。

Docker 設定

Docker の設定は、 Synology NAS にならう感じで書いてくので、いい感じで読み替えて Dockerfile なりを書いてほしい。

イメージ

codercom/code-server を duckerhub からダウンロードする

ポート

ここでは、ホスト側 9001 をコンテナ側 8080 になるように expose した。

ボリューム

ここは、dockerhub の説明とは少し変えた。というのもボリュームリンクなしで起動してみると /home/config の中には、.config や、 .local の他にもいろいろ保存されそうなだったのでざっくりと指定した。まとめると

ホスト側はあくまで自分の環境でのことなので、好きな場所に変えてください。

ホスト側コンテナ側
/volume1/docker/code-server/config/home/coder
/volume1/docker/code-server/project/home/project

ここまでで、 9001 ポートに http アクセスすれば使えるようになっている。

ログインパスワード

コンテナははじめからパスワード認証が有効になっている。そのパスワードは初期値が config.yaml に生成されるようだ。

場所は上の例で行くと、/volume1/docker/code-server/config/.config/code-server/config.yaml です。

リバースプロキシ設定

ここも、synology NAS のでやる場合の内容です。

外側 443ポートに対しネームベースで振り分けるように設定します。また、code-server は WebSocket を使っているらしいので ws が通るように設定します。

xxx.myds.me は、NAS に付属の DDNS ホスト名です。そのサブドメインを作成し、コンテナに流す設定をします。

コンテナは、先程 9001 を開けたので 9001 に流します。

[ソース側]

Protocol: HTTPS
Hostname: code.xxx.myds.me
Port: 443

[転送先 (destination) ]

Protocol: HTTP
Hostname: localhost
Port: 9001

[カスタムヘッダー]

WebSocket を通すためにカスタムヘッダーを設定します。 Synology のリバースプロキシの実態は nginx なのでその設定と言ったらわかりやすいでしょうか?

Custom Header タブに行き Create > Websocket を選択すうと結果こうなります

Upgrade => $http_upgrade
Connection => $connection_upgrade

ここまで終えると、

https:/code.xxx.myds.me/ でアクセルすると、VS-Code っぽい画面がでてきます。初回はパスワード認証で先程のパスワードを入力すると認証をパスします。

エディタで開くフォルダは、 ?folder=/home/project とかすればいいようです。場所によってかけなかったり、コンテナ停止で消えたりするので注意が必要ですね。

code-server のコンテナには python とか入っていないので、ちゃんとやりたければ別のコンテナかサーバーに ssh したほうがよさそう。直接コンテナにインスールしても消えてしまう。(コンテナの / をどっかにマウントするのもちょっとなんだし)

参考

codercom/code-server – Docker Image | Docker Hub

Install – code-server Docs

Websoc kets for Synology DSM – Matthias Lohr – Synology のリバースプロキシで websocket を通す方法