Unityでシグナルハンドリングする

# Unityでシグナルハンドリングする

## はじめに

本記事は [QualiArts Advent Calendar 2023] 21日目の記事です。
株式会社QualiArtsでUnityエンジニアをしている吉田です。

本投稿はUnityでシグナルハンドリングをするという、あまり需要がないであろうニッチな内容ですｗ。

## やりたいこと
Unityアプリに送信された**シグナル**(SIGTERM)を検知（ハンドリング）して、非同期処理実行後にアプリを終了させたい！

## シグナルとは

:::note warn
ChatGPT(linuxのシグナルについて 説明)
> *Linuxのシグナルはプロセス間通信の一種で、特定のイベントを通知するために使われます。例えば、**SIGTERMは終了要求**、SIGKILLは強制終了、SIGSTOPは一時停止を指示します。プロセスはシグナルを受けてデフォルトの動作を行うか、特定の関数（シグナルハンドラ）を実行します。ただし、SIGKILLとSIGSTOPは無視や捕捉が不可能です。*
:::

### Unityアプリにシグナル(SIGTERM)を送信する方法

ターミナルからkillコマンドを使用してシグナル送信できます！

```terminal
// 対象アプリのPIDを確認
ps -a
94447 ttys013    0:05.53 ./hogehoge.app/Contents/MacOS/hogehoge
// killコマンドでシグナル(SIGTERM)送信
kill 94447
```

## 検証環境
- 作業マシン：macOS 12.7
- Unity2022.3.14f
- xcode 14.2

## Unityでシグナル(SIGTERM)をハンドリングする方法

Unityでシグナルのハンドリング方法はこの記事 [Handle linux signals in Unity3D] に大変わかりやすい実装付きの説明がありました。

下記native plugin(sighandler.c)を使用すれば シグナル(SIGTERM)ハンドリングできる！

ありがとう。Sebastien！

```c:sighandler.c
#include <signal.h>

void (*c)(int);
void sig_callback(int n){
	c(n);
}
void OnTerm(void (*handler)(int)){
	c = handler;			
	signal(SIGTERM, sig_callback);
}
```

このように使用すれば,シグナル(SIGTERM)ハンドリングしてアプリ終了できる。

```csharp:HandleTerm.cs
using System.Collections;
using UnityEngine;

public class HandleTerm : MonoBehaviour {

	//This is the important part
	public delegate void handlerDelegate();
	[DllImport ("sighandler")]
	private static extern void OnTerm(handlerDelegate handler);
 
	void ExitHandler(){
		print("onExit");
		//We must call Application.Quit() manually :
		// The default built-in handler is cancelled.
		Application.Quit();
	}
 
	//Then it's just normal code
	// Use this for initialization
	void Start () {
		//Register handler on initialization
		OnTerm(new handlerDelegate(this.ExitHandler));
	}

	// Update is called once per frame
	void Update () {
	}
 
	void OnApplicationQuit(){
		//This doesn't get called under normal circumstances when app get a SIGTERM.
		print("Application Quit");
	}
}
```

### native pluginをmacOS用にビルドする
 [Handle linux signals in Unity3D] に掲載してあるnative pluginのビルド方法はLinux用なのでmacOS用にビルドして使用します。

```terminal
// macOSターミナルでsighandler.bundleを作成
gcc -c -fPIC -arch arm64 -arch x86_64 sighandler.c
gcc -shared -arch arm64 -arch x86_64 sighandler.o -o sighandler.bundle
```

## シグナル（SIGTERM）を検知して、アプリ終了前に非同期処理を実行させる！

新規プロジェクト(テンプレート3Dコアにしましたが、なんでもいいです)を作成し
下記DemoApp.csを追加、Pluginsフォルダにシグナルハンドリング用sighandler.bundleを配置して、macOS用アプリをビルドします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10939/6a3ddff1-0d36-786e-7dc9-b1a6a3123f10.jpeg" width="300"/>

```csharp:DemoApp.cs
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using UnityEngine;

public static class DemoApp
{
    // originalから少し修正
    // publicの必要はないのでprivateに変更
    // sighandler.cの実装的には引数あったほうがよいので修正
    private delegate void handlerDelegate(int sig);

    [DllImport ("sighandler")]
    private static extern void OnTerm(handlerDelegate handler);

    // RuntimeInitializeLoadTypeは適当
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    private static void RuntimeInitializeOnLoadMethod()
    {
        // シグナル(SIGTERM)を検知できるようにハンドラーを登録する
        OnTerm(SigTermHandler);
    }

    // シグナル(SIGTERM)発生時に呼び出すハンドラー
    // IL2CPPでも動作するようにstaticにしてMonoPInvokeCallbackAttributeをつける
    [AOT.MonoPInvokeCallbackAttribute(typeof(handlerDelegate))]
    private static void SigTermHandler(int sig)
    {
        // 終了処理を開始
        var _ = QuitAsync();
    }

    private static async Task QuitAsync()
    {
        // 10秒待機
        Debug.Log("wait for 10sec");
        await Task.Delay(10000);
        // 終了させる
        Debug.Log("Application.Quit");
        Application.Quit();
    }
}
```

### ビルド設定を変更

デフォルトビルド設定だとフルスクリーン起動します。
作業しずらいのでFullscreenModeをWindowedに, Screenサイズも小さめに設定を変更したほうが良いです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10939/de9a0238-13e1-1345-756d-8dcc8e4ec40d.png" width="650"/>

### ビルドしたアプリをテストする

ビルドしたアプリを起動させる

```terminal
// ターミナルからビルドしたアプリの起動
// 起動引数に"-logFile -"を追加し、ログ出力を標準出力に出力させる
./unity-signal.app/Contents/MacOS/unity-signal -logFile -
```

killコマンドでシグナル(SIGTERM)を送信

```terminal 
// 起動したアプリのPIDを確認
ps -a
  :
94517 ttys013    0:05.53 ./unity-signal.app/Contents/MacOS/unity-signal -logFile -
  :
// killコマンドでシグナル(SIGTERM)を送信
kill 94517
```

アプリを起動したターミナルに"wait for 10sec"が表示され アプリが終了すれば成功！

```terminal 
  :
- Finished resetting the current domain, in  0.001 seconds
UnloadTime: 0.699459 ms
wait for 10sec
Application.Quit
Thread 0x17141b000 may have been prematurely finalized
Setting up 5 worker threads for Enlighten.
  :
```

## 最後に

ニッチな内容でしたが、なにかの役に立てれば幸いです。

過去記事になりますが、QualiArts engineer blogでは[「IDOLY PRIDE」におけるマスターデータについて]も投稿していますので興味ありましたら参照してみてください。

[QualiArts Advent Calendar 2023]: https://qiita.com/advent-calendar/2023/qualiarts

[Handle linux signals in Unity3D]: https://sdumetz.github.io/2017/07/01/handle-unix-signals-unity.html

[「IDOLY PRIDE」におけるマスターデータについて]: https://technote.qualiarts.jp/article/34/