「コンピュータシステムの理論と実装」5章

前回の記事で、「コンピュータシステムの理論と実装」の5章で学んだ考え方について書きました。今回は具体的な実装を書いていきます。

CPUの実装

これまでに登場したHack機械語の仕様、およびHack命令セットの一覧を参照しながら実装しました。   特に面白かったのは、命令セットの各ビットをもとに、どのように命令を絞り込むかの部分です。特に comp を表す acccccc の7ビットの扱い方がぱっと浮かばなかったのですが、何度も仕様を見返した結果、  

  • a == 0 なら comp に使うのはDかAのみ、 a == 1 ならDかMのみ

  • cccccc の6ビットは、ALUの6つの入力にそのまま対応している

ということに気づいて驚きました。ALUでHack命令セットの計算全てをカバーしているので冷静に考えれば当たり前ではありますが、今までの章で登場した仕様はこのためにあったのかと伏線回収されたような気分でした。  

一方で、PCチップにおいてジャンプ条件を満たしているかを jjj の3ビットで判定する部分は、便利なチップはないため、地道に1ビットずつ判定していくしかありません。ALUからは zr (0であるか)と ng (負であるか)が出力されるため、正の数であるかの判定はこれらを組み合わせて行います。   

最終的な実装は以下のようになりました。

CHIP CPU {

    IN  inM[16],         // M value input  (M = contents of RAM[A])
        instruction[16], // Instruction for execution
        reset;           // Signals whether to re-start the current
                         // program (reset==1) or continue executing
                         // the current program (reset==0).

    OUT outM[16],        // M value output
        writeM,          // Write to M? 
        addressM[15],    // Address in data memory (of M)
        pc[15];          // address of next instruction

    PARTS:
    // Aレジスタ手前のMux16
    Mux16(a=instruction, b=fromALU, sel=instruction[15], out=toA);

    // Aレジスタ
    Not(in=instruction[15], out=nonOpeCode);
    Or(a= nonOpeCode, b= instruction[5], out= isLoadA);
    ARegister(in= toA, load= isLoadA, out= fromA, out[0..14]= addressM);

    // Dレジスタ
    And(a= instruction[4], b= instruction[15], out= load);
    DRegister(in= fromALU, load= load, out= fromD);

    // ALU手前のMux16
    Mux16(a= fromA, b= inM, sel= instruction[12], out= fromAOrM);

    // ALU
    ALU(x= fromD, y= fromAOrM, zx= instruction[11], nx= instruction[10], zy= instruction[9], ny= instruction[8], f= instruction[7], no= instruction[6], out= fromALU, out= outM, zr= zr, ng= ng);

    // writeM
    And(a= instruction[3], b= instruction[15], out= writeM);

    // PC
    And(a= instruction[2], b= ng, out= jlt);
    And(a= instruction[1], b= zr, out= jeq);
    Or(a= ng, b= zr, out= ngOrZr);
    Not(in= ngOrZr, out= ngNorZr);
    And(a= instruction[0], b= ngNorZr, out= jgt);
    Or(a= jlt, b= jeq, out= tempJumpCond);
    Or(a= tempJumpCond, b= jgt, out= jumpCond);
    And(a= jumpCond, b= instruction[15], out= jump);
    PC(in= fromA, load= jump, inc= true, reset= reset, out[0..14]= pc);
}

データメモリの実装

RAM16K, Screen, Keyboardのアドレス空間を連続したものとして扱う必要があります。どのアドレス空間が使われるかが、 address 入力の上位2ビットを見れば判別できるようになっているのがポイントです。これをセレクタビットとして利用できます。  

上位2ビット 対応するメモリ
00 RAM16K
01 RAM16K
10 Screen
11 Keyboard

(範囲外のアドレスにアクセスされるケースは、仕様に定義されていないので考慮しなくてOKです)     

ただし、誤った状態変化を起こさないように気をつける必要があります。自分は最初、素直に全部のメモリに address の下位ビットを渡してから Mux4Way16 で結果を選ぶ実装にしましたが、これだとデータを書き込む際に全メモリで書き込みが発生してしまいます。まず最初に上位2ビットを見て、対応するもの以外のメモリはloadフラグを常に0にしておく必要があります。

最終的な実装は以下のようになりました。

CHIP Memory {
    IN in[16], load, address[15];
    OUT out[16];

    PARTS:
    // アドレス空間に該当しないメモリは、Dmux4Wayによってloadフラグを0にしておく
    DMux4Way(in= load, sel= address[13..14], a= loadData1, b= loadData2, c= loadScreen, d= loadKeyboard);
    Or(a= loadData1, b= loadData2, out= loadData);

    RAM16K(in= in, load= loadData, address= address[0..13], out= data);
    Screen(in= in, load= loadScreen, address= address[0..12], out= screen);
    Keyboard(out= keyboard);
    Mux4Way16(a= data, b= data, c= screen, d= keyboard, sel= address[13..14], out= out);    
}

コンピュータの実装

CPU・データメモリ・命令メモリを接続すればOKです。

CHIP Computer {

    IN reset;

    PARTS:
    ROM32K(address= pc, out= instruction);
    Memory(in= outM, load= writeM, address= addressM, out= inM);
    CPU(inM= inM, instruction= instruction, reset= reset, outM= outM, writeM= writeM, addressM= addressM, pc= pc);
}

シミュレーションとはいえ、「コンピュータを作る」ということを実際に体験できて感慨深いです。Hackコンピュータで使われているノイマン型アーキテクチャについてはざっくりとは知っていましたが、こうして自分で作ってみると、いかに考え抜かれた設計であるかを強く感じられます。
6章以降も内容が盛りだくさんなので、引き続き取り組んでいくつもりです。

「コンピュータシステムの理論と実装」でHDLを書くときにハマった考え方

GWから「コンピュータシステムの理論と実装」を少しずつやっています。前半最大の山場が5章のコンピュータアーキテクチャの構築でした。
ここではHDL (Hardware Description Language) を使って回路を定義していくのですが、普段使っているような高級プログラミング言語とは異なる考え方をいろいろ求められてかなり苦戦しました。具体的な実装は別の記事にするつもりですが、1~4章までの概念をしっかり理解していないと5章は突破できないようになっているので、自分が理解できるまで特に混乱してしまったポイントをまとめます。
(なお、この本のHDLはHackコンピュータ用の独自仕様を含むため、一般のHDLに適用できないものも含むかもしれません)

電気信号に逐次的条件分岐はない

地味に頭のパラダイムシフトが必要だったのがここです。高級プログラミング言語なら、 IF THEN ELSE 構文を使えば条件によってTHENとELSEのどちらかのみが実行されます。4章で扱った機械語にも同様の機能があります。
しかしHDLではこのようなことはできません。なぜなら HDLで扱うのは電気信号を流す回路であり、同じピンから配線が複数伸びていれば必ず両方に信号が流れる ためです。

そのため、HDLで条件分岐的なことを実装したいなら、「あらかじめ全ての場合の処理を行ったうえで、Muxを利用して出力を選択する」という形がALUにおける定石です。フリップフロップ回路などを使って電気信号を逐次的に変える方が一見スマートですが、回路が複雑になると使うチップの数が増え、回路の面積や電力消費が増えてしまいます。

レジスタの入力はload=0なら無視できる

電気信号は回路がつながっていれば常に流れるので、レジスタには常に何らかの入力が来ています。しかしレジスタのloadビットが0の場合、出力は前のクロックと同じになります。逆にいえば、loadビットが0であれば入力として何が来ても無視されるということです。
回路がつながっている以上、使わない値・使ってはいけない値が入力として来ることもありますが、loadビットが0であれば何も問題ありません。

outは複数書いてもよい

CHIPは関数ではなく回路なので、outを複数書いて両方のピンに出力を流すことも可能です。これは出力した値を複数の用途に使いたいときに便利です。

CHIP Or16 {
    IN a[16], b[16];
    OUT out[16];
}

Or16(a=a, b=b, out=out1, out=out2);

さらに、outの値にインデックスを指定して、一部分のビットのみを取り出すこともできます。取り出さなかったビットはどこからも利用されないので無視できます。

Or16(a=a, b=b, out[0..14]=out);

これらの記法は付録のB.6節で解説されています。なお、HackコンピュータのHDLの仕様の都合で、inではこのような書き方はできないようです。

組み合わせ回路は瞬時に処理を行うとみなせる

ALUなどマスタークロックと接続されていない組み合わせ回路は、モデル上は瞬時に処理を行うため、ここでかかる時間を考える必要はありません。 Hackコンピュータのモデルにおいては、 1サイクルが時間の最小単位 です。このサイクルという概念を提供しているのがクロックであり、クロックに接続されたレジスタは全てこの離散的な時間を基準に処理を行います。
逆に、組み合わせ回路はクロックに接続されておらず、内部で状態を持つこともないため、時間という概念から独立したものと捉えることができるわけです。もちろん実際にはごくわずかな物理的時間がかかっていると思われますが、コンピュータにおけるサイクルは、チップ内の演算処理とチップ間の信号伝送にかかる最大時間よりも長くなるように設定されています。ここは物理的な回路設計の世界であり、モデル上では無視できるものです。 (5章で登場する命令メモリ(ROM32K)も、組み合わせ回路で実現できる読み取り専用メモリであるため、命令フェッチにかかる時間を無視できます)

そして、時間が離散的ということは、 状態が変化したかどうかも1サイクル単位で判定される ということです。例えばALUの2つの入力値がやってくるまでにわずかな物理的時間差がある場合、一時的にALUの出力が期待と異なるものになりえます。しかし重要なのはサイクルの境目で状態が安定していることであり、サイクル内部で状態が何回変わろうがモデル上は無視できます。
自分が5章で無駄に悩んでしまった最大の原因は、この時間モデルを腹落ちしていなかったことでした。これについては3.2.1節で詳しく説明されています。


最初は難問に思えたCPUやメモリの実装課題ですが、前の章を何度も振り返るうちに、今までの概念の積み上げによって単純化して考えられる面が大きいことに気づき、Hackコンピュータの設計の巧みさを感じられました。

SignalベースのAngular HttpResourceでAPIを叩いてみた

世間ではすっかりSignalが定着し、状態管理やFormなどの領域にもSignalベースの手法が模索されるようになりましたが、自分はまだ基本的なSignalの使い方しか知らなかったのでちょっと練習してみました。
以前Angular AI Tutorで書いた料理レシピ取得アプリケーションをベースに、今回はHttpResourceを導入しました。

HttpResourceの導入

HttpResourceは、Angular v19.2から実験的に導入されたHTTP Clientで、API経由の非同期的なデータ取得をラップし、Signalとして扱えるようになります。

angular.jp

今までのObservableの世界のHttpClientとは異なるやり方ができそうで、興味を持って導入してみました。

RecipeService

以下の2つのAPIを叩いて結果を返す RecipeService を実装してみました。 - /api/recipes - /api/recipes/{recipeId} の2つのエンドポイントを利用しています。

(※ Angular v21.0.0で実装しています。HttpResourceはまだexperimentalなので、今後APIが変わって実装が動かなくなる可能性もあることに留意してください)

@Injectable({ providedIn: 'root' })
export class RecipeService {
  private readonly httpClient = inject(HttpClient);
  private readonly recipeId = signal('');

  private readonly listRecipesResource = httpResource<RecipeModel[]>(() => '/api/recipes', {
    defaultValue: [],
  });
  
  private readonly getRecipeResource = httpResource<RecipeModel>(() => {
    const recipeId = this.recipeId();
    if (recipeId === '') {
      return undefined;
    }
    return `/api/recipes/${recipeId}`;
  });

  public listRecipes(): Signal<RecipeModel[]> {
    return computed(() => this.listRecipesResource.value());
  }

  public getRecipe(recipeId: string): Signal<RecipeModel | undefined> {
    this.recipeId.set(recipeId);
    return computed(() => {
      return this.getRecipeResource.value();
    })
  }

  constructor() { 
    effect(() => {
      if (this.listRecipesResource.status() === 'error') {
        this.handleError(this.listRecipesResource.error() as HttpErrorResponse);
      }
    });
    effect(() => {
      if (this.getRecipeResource.status() === 'error') {
        this.handleError(this.getRecipeResource.error() as HttpErrorResponse);
      }
    });
  }

  private handleError(error: HttpErrorResponse) {
    if (error.status === 0) {
      console.log('An error occurred:', error.error.message);
    } else {
      console.log(`Backend returned code ${error.status}, reason: ${error.error.message}`);
    }
    return throwError(() => new Error('Something bad happened; please try again later.'));
  }
}

HttpResourceをそのまま露出させてもよかったのですが、Component側では単純なSignalだけを扱うようにしたいと思い、メソッドで隠蔽しています。(この辺はベストプラクティスがまだよくわかっていません...)

初期値問題

今回つきまとったのが、流れてくる初期値にどう対応するかです。何も意識しないと、HttpResourceの初期値としてundefinedが流れてきます。
List APIの場合は簡単で、 defaultValue を設定することでundefinedからおさらばできます。普通のSignalでいう initialValue に相当します。

private readonly listRecipesResource = httpResource<RecipeModel[]>(() => '/api/recipes', {
  defaultValue: [],
});

問題はGet APIです。今回は呼び出し元から受け取った recipeId をSignalにsetし、それをトリガーにAPIを叩くようにしているので、下手に recipeId の初期値を設定すると不要なリクエストが飛びかねないと思っていました。
しかしHttpResourceでは、URLのstringを導出する関数でnullやundefinedを返せば、リクエストが実行されないようです。ここで初期値であればリクエストを飛ばさないようにすることができました。

private readonly recipeId = signal('');

private readonly getRecipeResource = httpResource<RecipeModel>(() => {
  const recipeId = this.recipeId();
  if (recipeId === '') {
    return undefined;
  }
  return `/api/recipes/${recipeId}`;
});

effectでのエラーハンドリング

HttpResourceを叩くときのエラーハンドリングをどこでやるのか迷いましたが、これはエラーメッセージを返すような副作用を伴うので、 effectの中で行っています。
HttpResourceでは error() を叩くことでエラーレスポンスを受け取れるのですが、エラーがないときに叩くとこれもundefinedを返してくるのでまた面倒なことになります。そのため、 status() を叩いて今の状態を取得し、errorであればエラーハンドリング用のメソッドに飛ばす方法を取りました。

constructor() { 
    effect(() => {
      if (this.listRecipesResource.status() === 'error') {
        this.handleError(this.listRecipesResource.error() as HttpErrorResponse);
      }
    });
    effect(() => {
      if (this.getRecipeResource.status() === 'error') {
        this.handleError(this.getRecipeResource.error() as HttpErrorResponse);
      }
    });
  }

Componentからの呼び出し

List APIはレシピ一覧画面のComponentから呼んでいます。ここでは取得したレシピ全件に対するキーワード検索機能をつけているので、 computedを利用してServiceからのSignalの戻り値を加工しています。
computedは読み取り専用であり、副作用のない純粋関数的な処理を想定して作られているため、本来はcomputedの中で副作用を起こすことはできません。しかし、HttpResourceが内部での非同期的なAPIリクエストを隠蔽し、Signalとしてアクセスできるようにしてくれています。

@Component({
  selector: 'app-recipe-list',
  imports: [FormsModule, RouterLink],
  templateUrl: './recipe-list.component.html',
  styleUrls: ['./recipe-list.component.css'],
})
export class RecipeList {
  private readonly recipeService = inject(RecipeService);
  private readonly recipes = this.recipeService.listRecipes();

  protected readonly keyword = signal('')

  protected readonly filteredRecipes = computed(() => {
    return this.recipes().filter(
      recipe => recipe.name.toLowerCase().includes(this.keyword().toLowerCase())
    );
  });
}

これに対し、Get APIはレシピ詳細画面のComponentから利用し、effectの中からServiceを叩いています。

@Component({
  selector: 'app-recipe-detail',
  imports: [],
  templateUrl: './recipe-detail.component.html',
  styleUrls: ['./recipe-detail.component.css'],
})
export class RecipeDetail {
  private readonly recipeService = inject(RecipeService);
  protected readonly recipeId = input.required<string>()
  protected readonly recipe = signal<RecipeModel | null>(null);

  constructor() {
    effect(() => {
      const recipe = this.recipeService.getRecipe(this.recipeId());
      this.recipe.set(recipe() || null);
    });
  }
}

なぜGet APIではeffectを使っているのかというと、Service側の getRecipe() の実装で recipeId Signalに値をsetしているためです。getRecipe() をcomputedの中で呼び出すと Writing to signals is not allowed in a computed と怒られます。
「それなら recipeId Signalを使うのをやめて、リクエストのたびにHttpResourceにrecipeIdを渡せばいいのでは?」と思ったのですが、現時点(Angular v21.0.0)でHttpResource内のurlコールバック関数に外から引数は渡せません。IDなどを渡したければSignalを経由させるしかなさそうです。

// url関数に引数を渡せない
httpResource<RecipeModel>(url: () => string | undefined, options?: HttpResourceOptions<RecipeModel, unknown> | undefined): 

なかなか使用例を見つけられず苦労しましたが、なんとか動作させられました。自分で試行錯誤して理解していくのも楽しいものですね。

Kotlin Fest 2025に参加しました

11/1に行われたKotlin Fest 2025に参加してきました。

大規模なテックカンファレンスに参加するのは久しぶりでした。大きなのぼりが立っていたり、コーヒーやペットボトルが無料だったり、懇親会でカレーが出たりととても豪華で、スタッフの皆様がどれほど準備に尽力されたのかと思うと感謝の気持ちでいっぱいです。

セッションには少しだけ出るつもりだったのですが、面白そうなものばかりで気づいたらいくつも参加していました。

Kotlin言語仕様書への招待 〜コードの「なぜ」を読み解く〜

Kotlinのドキュメントは普段よく見ているのですが、言語仕様書までは読んだことがありませんでした。いろいろな文法が仕様としてEBNF表記で定義されていて面白いです。
trailing lambdaやinfixなど他のKotlin文法の仕様も気になっているので、とりあえずNotebookLMに仕様書を食わせていろいろ見てみることにします。

デッドコード消せてますか? - 構文解析とGradleプラグイン開発で始めるコードベース改善

構文解析ができれば自動でのコード削除や生成に大きな力を発揮しそうですね。Gradleプラグイン開発にも興味があったのですが、スライドを見た限りでは思った以上に簡単に完成させていた印象を受けて驚いています。

文字列操作の達人になる ~ Kotlinの文字列の便利な世界 ~

知らなかったStringメソッドやIDEの機能が多くありました。特にmulti-line stringのシンタックスハイライトは、まさにテストコードの入出力をJSONでやるときにいらついたことがあるのでぜひ使ってみたいです。

「動く」サンプルでスムーズなコミュニケーションを 〜CMP時代のKotlinPlayground活用最前線〜

Kotlin Playgroundが予想以上に高機能であることを知りました。自分はサーバサイドKotlinしか書いていませんが、マルチプラットフォーム開発でちょっとサンプルコードを動かして画面を出したいときにも役立つんですね。
あとKotlin Playgroundには補完機能がなくて不便だなと思っていたのですが、ちゃんとあることがわかって不覚でした。(とはいえ自分の端末では他のショートカットキーとかぶってしまっていますが...)

Functional Calisthenics in Kotlin: Kotlinで「関数型エクササイズ」を実践しよう

とても共感できることが多いセッションでした。Kotlinの好きなところはJavaの要素を踏襲しながらも関数型プログラミングの要素を取り入れやすいところだと思っています。関数型の言語は以前挑戦してみて難しかった印象があるのですが、改めて試してみたいです。

Kotlin 2.2が切り拓く:コンテキストパラメータで書く関数型DSLと新しい依存管理のかたち

コンテキストパラメータの話は全く知らなかったので濃密なセッションでした... DIがアプリケーションロジックの依存を記述する手法とすれば、コンテキストパラメータはロギングや認証など実行時に利用するコンテキストを記述できる手法、という理解をしていますがあまり自信がありません。
Spring Bootなどのフレームワークでは実行時コンテキストがどう設定されているのかがなかなか見えづらい側面があるので、新しい書き方へのアプローチとしてとても興味深いです。

ノベルティ: Loglass Tech Blog Sprint Review 2025

ログラスさんのブースでいただいた技術記事の冊子です。ログラスさんはZennにKotlin関連の記事をたくさん投稿されていて、何度も参考にしてきたので大変ありがたいです...!じっくり読みます。

Kotlin Festのテーマである「Kotlinを愛でる」ができた日になったと思います。業務とか勉強とかの文脈に関係なく、個人として単純に技術を好きでいることの大切さを改めて感じました。
開催いただきありがとうございました!

【Spring Boot + Kotlin】 REST APIサーバとして認証機能を実装する

最近はSpring Bootを使った個人用のWebアプリケーションを作っているが、認証機能で思った以上に苦戦中。フロントエンドはSpring Bootのtemplateを使っていないため、単純なREST APIとしてSpring Bootを使う場合どう認証をすればいいのか理解が大変だった。 今回はユーザ名とパスワードによる基本的なログイン処理を作る。JWTなどのトークンは導入せず、サーバ側でセッション管理できるようにする。

まずspring-securityを入れる。

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-security")
}

次にConfigurationを設定する。Spring Securityでは、実際の認証処理(ユーザ名・パスワードの照合)は AuthenticationProvider で行われる。ユーザ名・パスワード認証では DaOAuthenticationProvider で行われ、その中ではサーバに保持されているユーザの情報を 受け取るための UserDetailService と、入力されたパスワードをEncodeする PasswordEncoder が使われる。これらを定義して渡してやると認証ができるようになる。

@Configuration
@EnableWebSecurity
class WebSecurityConfig {
    @Bean
    fun filterChain(http: HttpSecurity, authenticationManager: AuthenticationManager): SecurityFilterChain {
        http {
            // /api/loginには認証なしでアクセス可、それ以外は要認証
            authorizeHttpRequests {
                authorize("/api/login", permitAll)
                authorize(anyRequest, authenticated)
                HttpMethod.PUT
                HttpMethod.DELETE
            }
            csrf { disable() } // APIサーバなのでdisableしておく
        }

        return http.build()
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return BCryptPasswordEncoder(8)
    }

    @Bean
    fun authenticationManager(
        userDetailsService: UserDetailsService,
        passwordEncoder: PasswordEncoder,
    ): AuthenticationManager {
        val authenticationProvider = DaoAuthenticationProvider()
        authenticationProvider.setUserDetailsService(userDetailsService)
        authenticationProvider.setPasswordEncoder(passwordEncoder)

        return ProviderManager(authenticationProvider)
    }
}

UserDetailService に実装を加える。今回はデータベースの users テーブルから、ユーザ名を使ってユーザ情報を取得できるようにする。

@Service
@Transactional
class UserDetailServiceImpl(
    private val userRepository: UserRepository,
) : UserDetailsService {
    override fun loadUserByUsername(username: String): UserDetails {
        val loginUser = userRepository.findByUsername(username)
            ?: throw UsernameNotFoundException("User not found")

        return LoginUserDetails(loginUser)
    }
}

次にDBからユーザ情報を取得する部分を実装する。以下の資料がとても参考になった。

最新の6.0で学ぶ!初めてのひとのためのSpring Security | ドクセル

UserRepositoryは以下。今回はO/R mapperとしてExposedを使っている。 UserTable の定義はテーブルのスキーマ定義があればほぼそのまま書けるので省略。

@Repository
class UserRepository {
    fun findByUsername(username: String): LoginUser? {
        return UserTable.select { UserTable.username eq username }
            .map { LoginUser(it[UserTable.username], it[UserTable.password]) }
            .firstOrNull()
    }
}

Repositoryで返している LoginUser というのは自作の型。認証のためには UserDetails を返さなければいけないが、フィールドが多くてRepositoryが知るべき情報ではないので、Repositoryの段階では簡易的な LoginUser 型を返し、それをもとにUserDetails の実装である LoginUserDetails を生成する形にする。

data class LoginUser(
    val username: String,
    val password: String,
)

class LoginUserDetails(private val loginUser: LoginUser) : UserDetails {
    override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
        return mutableListOf()
    }

    override fun getPassword(): String {
        return loginUser.password
    }

    override fun getUsername(): String {
        return loginUser.username
    }

    override fun isAccountNonExpired(): Boolean {
        return true
    }

    override fun isAccountNonLocked(): Boolean {
        return true
    }

    override fun isCredentialsNonExpired(): Boolean {
        return true
    }

    override fun isEnabled(): Boolean {
        return true
    }
}

最後にログイン時に使うControllerを作る。Springのドキュメントの例を参考にする。

spring.pleiades.io

注意点として、今回のようにREST APIサーバとして認証を行うときは、その後にセッションを保持する処理は自分で明示的に書かなければいけない。 Spring Bootからテンプレートを使ってログインフォームを配信する場合は自動で行ってくれるらしいが、上のドキュメントには以下のような注意書きがある。

この例では、必要に応じて、認証されたユーザーを SecurityContextRepository に保存するのはユーザーの責任です。例: HttpSession を使用してリクエスト間で SecurityContext を永続化する場合は、HttpSessionSecurityContextRepository を使用できます。

セッション保持の処理はこちらのドキュメントを参考にする。

spring.pleiades.io

authenticationManager.authenticate() が終わった後の処理は、セッション保持のためのもの。

data class LoginRequestParams(
    val username: String,
    val password: String,
)

@RestController
class LoginController(private val authenticationManager: AuthenticationManager) {
    private val securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy()
    private val securityContextRepository: SecurityContextRepository = HttpSessionSecurityContextRepository()

    @PostMapping("/api/login")
    fun login(
        @RequestBody loginRequestParams: LoginRequestParams,
        request: HttpServletRequest,
        response: HttpServletResponse,
    ) {
        val authenticationToken =
            UsernamePasswordAuthenticationToken(loginRequestParams.username, loginRequestParams.password)
        val authentication = authenticationManager.authenticate(authenticationToken)

        val securityContext = securityContextHolderStrategy.createEmptyContext()
        securityContext.authentication = authentication
        securityContextHolderStrategy.context = securityContext
        securityContextRepository.saveContext(securityContext, request, response)
    }
}

これでログイン認証はできたので、APIでアクセスを試してみる。新規登録導線は作っていないので、DBに手動でユーザ名とパスワード ( PasswordEncoder で指定したのと同じ形式でハッシュ化したもの) を入れておく。

curl -X POST -H "Content-Type: application/json" -d '{"username": "Udomomo", "password": "<password>"}' http://localhost:8080/api/login -i -c cookie.txt
HTTP/1.1 200
Set-Cookie: JSESSIONID=<cookie value>; Path=/; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Sat, 25 Nov 2023 10:45:44 GMT

無事に200が返ってきて、 JSESSIONIDcookieにセットされる。

【Alfred Workflow】Script Filterで自作コマンドを開発する

せっかくAlfred Powerpackに入っているのでもっと使いこなしたいと思い、Workflowを使ってツールを作ってみた。
作ったのはこんな感じのモールス信号解読機。趣味でパズルを解いたりしていると、ふとしたときに意外と出てくるので、爆速で立ち上がるAlfredで解読したいと思っていた。

Alfred Workflowとは

Alfred Workflowを使うと、Alfredを拡張し、独自のキーワードで様々な機能を開発することができる。ただし有料ライセンスであるPowerpackを購入していることが条件。
全体の流れはGUIで作成するが、ビジネスロジックの部分はスクリプトで書くことができる。対応言語もBash, PHP, Ruby, Pythonなどいろいろある。

Script Filterを設定する

今回のツールでは、

  • モールス信号を入力している間に結果がリアルタイムで下に表示され、更新されていく
  • Enterキーを押すと結果がクリップボードにコピーされる

という挙動にしたかったので、Script Filterを使用した。
Script Filterはもともとは検索機能のために用意されているもので、スクリプトの実行中にAlfredに結果を表示できる。また、どのような書式で結果を表示するかもカスタマイズできる。

www.alfredapp.com

Script Filterを設定する

Alfred Preferenceの画面からWorkflowsを選ぶと、新しいWorkflowを開発できる。ちなみにAlfredの入力画面で alfred と入力すれば、Alfred Preferenceに飛べる。
右クリックで Inputs以下にある Script Filter を選択すると、新しいScript Filterを作成できる。また、最終結果をクリップボードに渡したいので、Outputとして Copy to Clipboardを選び、Script Filterから線をドラッグしてつないでおく。

f:id:Udomomo:20220206110325p:plain

Script Filterを作成すると、以下のような設定画面になる。 Keyword の欄には、このworkflowを開始するためにAlfred上で入力するキーワードを指定する。引数はqueryとargvのどちらでも良いが、エスケープの心配がいらない点やパフォーマンス面から、Alfredではargvを推奨している。

f:id:Udomomo:20220206110440p:plain

Run Behaviourからは、入力中のスクリプトの挙動を設定できる。文字をどんどん入力していったら前の段階での結果はいらなくなるので、Queue ModeTerminate previous script にする。また、結果のリアルタイム表示といっても、入力が一段落した時点でスクリプトを実行すれば十分なので、 Queue DelayAutomatic delay after last character typed にする。

f:id:Udomomo:20220206111256p:plain

スクリプトを作成する

今回はPythonで作成した。日本語と英語の解読ができれば良かったので、モールス信号の辞書をそのまま貼ったのだが、その部分は長いので省略している。

# coding: utf-8

import json
import sys

MORSE_CODE_DICT_EN = {
  'a': '.-', 'b': '-...', 'c':'-.-.', 'd':'-..', 'e':'.', 'f':'..-.', 
  ...
}

MORSE_CODE_DICT_JA = {
  'あ': '--.--', 'い': '.-', 'う': '..-', 'え': '-.---', 'お': '.-...',
 ...
}

def decrypt(lang, input):
  if lang == 'ja':
    return _morse_to_letters(input, MORSE_CODE_DICT_JA)
  elif lang == 'en':
    return _morse_to_letters(input, MORSE_CODE_DICT_EN)
  else:
    return 'Invalid argument. Usage: <lang (`ja` or `en`)> <morse code (`.` or `-`, split by space)>)'
  

def _morse_to_letters(input, morse_dict):
  if not input:
    return 'No morse code input.'
  reversed_dict = {value:key for key,value in morse_dict.items()}
  result = ''
  for m in input:
    result += reversed_dict.get(m, '#')
  return result

if __name__ == '__main__':
  args = sys.argv[1].split()
  lang, input = args[0], args[1:]
  result = {
    'items': [{
      'uid': 'morse',
      'title': decrypt(lang, input),
      'arg': decrypt(lang, input)
    }]
  }
  sys.stdout.write(json.dumps(result, ensure_ascii=False))

注意すべきポイントは3つある。まず、通常のPythonスクリプトsys.argvとは異なり、Script Filterではキーワードの後に入力した引数全てがsys.argv[1] に入る。例えば morse ja .-- --.-- と入力する場合、ja .-- --.--sys.argv[1]の値になる。最終的なargsの数を予測できないので当然といえば当然だが、自分はここでかなりハマった。
2つ目は、Alfred WorkflowはPython 2のみに対応していること。これはWorkflowがMacのビルトイン環境のみで動くことを重視しているためらしい。今回の場合、コード内でひらがなを使っているため、冒頭に # coding: utf-8 をつけないとエラーになった。
最後に、Script FilterからAlfredに結果を渡すためには、専用のJSONまたはXMLフォーマットに従う必要がある。JSONフォーマットは以下のページに記載されている。

www.alfredapp.com

複雑なWorkflowになると、専用のライブラリ(例: alfred-workflow )を使うことも多いが、今回は単純なスクリプトなので手動でJSONを作った。

{
    'items': [{
       'uid': 'morse',
       'title': decrypt(lang, input),
       'arg': decrypt(lang, input)
    }]
}

uidはAlfredが結果のitemを識別するためのID(必須ではない)。titleの部分に解読結果を載せている。また、 args に指定された値は、Enterキーを押した時点でOutputに渡される。今回の場合、argsにも解読結果を指定することで、最終的な結果をクリップボードに渡すことができる。
この他にも任意のフィールドがたくさんあり、アイコンの指定などもできる。

これでいつものAlfredウィンドウから、morseコマンドが使えるようになった。
なお、Alfred Workflowにはデバッグツールも用意されているので、Script Filterが思うように動かない場合はログを確認してみよう。

www.alfredapp.com

AWS SysOps Administrator Associateに合格しました

少し前になってしまうが、3月末にAWS SysOps Administrator Associate試験を受験して合格した。1月末から試験勉強を始めたのだが、幸い1回で合格できた。

aws.amazon.com

受験の経緯

会社のシステムがAWS上で動いており、運用のためには体系的な知識が必要だと感じたため。社内ではSolution Architect Associateの方を受験する人もいたが、自分の興味がアプリケーションというよりもミドルウェア・インフラ寄りだったため、SysOpsを選択した。

学習法

まずUdemyのコースで一通りの範囲を学習した。

www.udemy.com

このコースは実際にAWSの各サービスを利用するハンズオンが充実しており、単なる試験対策にとどまらない実践的な知識を得ることができる。ただし量が多めで、1月末から観始めて3月上旬くらいまでかかった。自分はEC2やS3・VPC等についての基本的な知識がある状態で勉強を開始したので、何も知識がない場合はもっとかかるかもしれない。

このコースには最後にmock examがついているが、実際の試験よりも難しめで細かな内容が多い。そのため、できなくてもあまり自信を失う必要はない。そのかわり、各コースの最後についている小テストは基本的な内容なので、ここは満点を取れるまで復習した。

また、模擬試験には以下のサービスを利用した。

aws.koiwaclub.com

このサービスは無料会員でも#1~#5までの5回分の模擬試験を受けられるが、有料会員になるのがおすすめ。なぜかというと、後ろの模擬試験になるほど現在の本番試験に即した問題になっているからである。自分は#86から解きはじめ、#60くらいまでを2周したのだが、本番で全く同じ問題が何問か出てきて驚いた。

本番について

試験はオンラインで自宅から受験した。予約はAWS認定の公式サイトから行う。
受験システムはPSIかピアソンを選ぶことができるが、ピアソンの場合Karabiner-Elementsが入っているPCではうまく動作しないという情報があったため、PSIを選択した。

yoshitaku-jp.hatenablog.com

当日は特に問題なく受験できたが、PSIは試験官とのやり取りが英語のみであるため、英語ができない人はピアソンの方が良いかもしれない。

また、他のオンライン試験同様、机の周りはあらかじめ片付けておく必要がある。自分は机の上の物はもちろん、壁のカレンダーや掲示物等もすべて剥がしたうえで臨んだのだが、机の隣に本棚があったため、試験官から場所を移動するよう命じられた(他にPCを置ける場所がなかったので、無理やり本棚を動かしてなんとかした)。机の周りに物や家具がある人は、試験当日にあらかじめ移動させておくか、試験を受けられるどこか別の場所を探しておいた方が良い。