Windows版QuickLookのプラグインの読み込み・インストールのロジックを読む

OSXにある便利機能のQuickLook。それのwindows版のフリーソフトQuickLookのプラグイン部分のロジックを読んでみた。
調査したレポジトリのコミットはこちら[github.com/QL-Win/QuickLook](https://github.com/QL-Win/QuickLook/tree/2d8a38f922f302875bd138876e7ac466006950d5)。2019年11月12日 22:32 JST時点のコミット。

# まとめ

- Namespaceプロパティが重要で、バージョンは関係なかった。UIに表示するだけで、値の大小比較はしていない
- プラグインのインストール先はユーザーがインストールするもの、システムにプリインストールされているものの2つある。
- インストール先は2つに別れているが、読み込み後に同じ配列に追加されてPriorityプロパティだけでソートしている。システムにプリインストールされているプラグインが優先される事も無いし、Priorityが同じ時にどうなるかは未定義。

# QuickLookとは

OSXはファイルを選択してスペースキーを押すとプレビューが表示される機能がある。プレビューが表示されてもフォーカスは元のファイラー側にあり、表示中にカーソルキーで選択状態のファイルを変更するとプレビューの内容も更新される。
これをwindowsで再現したソフトがQuickLook。OSXの機能と同じ名前でちょっと紛らわしい。OS標準の機能ではないのでサクサク感は少し劣る。

# プラグインをインストールするロジック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5825/05135597-b70c-26f0-763f-b2d63d7aaeaf.png)

QuickLook.Plugin.PluginInstallerプロジェクトがプラグインインストーラー。

最初に実行されるのは[Plugin.cs](https://github.com/QL-Win/QuickLook/blob/2d8a38f922/QuickLook.Plugin/QuickLook.Plugin.PluginInstaller/Plugin.cs)のinitメソッド。この中でApp.UserPluginPathの中の"*.to_be_deleted"ファイルを削除している。
QuickLookのプラグインはインストール時に同じ名前のプラグインがあると、そのプラグインのファイルを削除するのではなく、xxx.to_be_deleted というファイルにリネームする処理がある。


```
public void Init()
{
    CleanupOldPlugins(App.UserPluginPath);
}
private static void CleanupOldPlugins(string folder)
{
    if (!Directory.Exists(folder))
        return;
    Directory.GetFiles(folder, "*.to_be_deleted", SearchOption.AllDirectories).ForEach(file =>
    {
        try
        {
            File.Delete(file);
        }
        catch (Exception)
        {
            // ignored
        }
    });
}

```

次は[PluginInfoPanel.xaml.cs](https://github.com/QL-Win/QuickLook/blob/2d8a38f922/QuickLook.Plugin/QuickLook.Plugin.PluginInstaller/PluginInfoPanel.xaml.cs)のReadInfoメソッド。_path変数にはプラグインファイルのパスが入っている。
プラグインファイルを解凍し、"QuickLook.Plugin.Metadata.config"という名前固定のxmlファイルを読み込み。xmlの名前空間、説明文、バージョン情報を読み込む。
名前空間は"QuickLook.Plugin."から始まらない場合はエラーとなる。バージョン情報や説明文はUIに表示するだけ。

```
private void ReadInfo()
{
    filename.Text = Path.GetFileNameWithoutExtension(_path);

    var xml = LoadXml(GetFileFromZip(_path, "QuickLook.Plugin.Metadata.config"));

    _namespace = GetString(xml, @"/Metadata/Namespace");

    var okay = _namespace != null && _namespace.StartsWith("QuickLook.Plugin.");

    filename.Text = okay ? _namespace : "Invalid plugin.";
    version.Text = "Version " + GetString(xml, @"/Metadata/Version", "not defined");
    description.Text = GetString(xml, @"/Metadata/Description", string.Empty);

    btnInstall.Visibility = okay ? Visibility.Visible : Visibility.Collapsed;
}
```

プラグインをインストールボタンが押されたら同ファイルのDoInstallが実行。CleanUp()は既存のプラグインを.to_be_deletedファイルにリネームする。その後、インストールファイルを名前空間のディレクトリに解凍する。この後再起動を促すメッセージを表示してインストールは終了。

```
private Task DoInstall()
{
    var targetFolder = Path.Combine(App.UserPluginPath, _namespace);
    return new Task(() =>
    {
        CleanUp();

        try
        {
            ZipFile.ExtractToDirectory(_path, targetFolder);
        }
        catch (Exception ex)
        {
            Dispatcher.BeginInvoke(new Action(() => description.Text = ex.Message));
            Dispatcher.BeginInvoke(new Action(() => btnInstall.Content = "Installation failed."));
            CleanUp();
        }
    });

    void CleanUp()
    {
        if (!Directory.Exists(targetFolder))
        {
            Directory.CreateDirectory(targetFolder);
            return;
        }

        try
        {
            Directory.GetFiles(targetFolder, "*", SearchOption.AllDirectories)
                .ForEach(file => File.Move(file,
                    Path.Combine(Path.GetDirectoryName(file), Guid.NewGuid() + ".to_be_deleted")));
        }
        catch (Exception ex)
        {
            Dispatcher.BeginInvoke(new Action(() => description.Text = ex.Message));
            Dispatcher.BeginInvoke(new Action(() => btnInstall.Content = "Installation failed."));
        }
    }
}
```

# プラグインをロードするロジック

[PluginManager.cs](https://github.com/QL-Win/QuickLook/blob/2d8a38f922/QuickLook/PluginManager.cs) のコンストラクタでユーザが追加したプラグイン・システムデフォルトのプラグインを読み込んで初期化する。
App.UserPluginPathは`C:\Users\UserName\AppData\Roaming\pooi.moe\QuickLook\QuickLook.Plugin\`。App.AppPathはexeファイルの位置。

```
private PluginManager()
{
    LoadPlugins(App.UserPluginPath);
    LoadPlugins(Path.Combine(App.AppPath, "QuickLook.Plugin\\"));
    InitLoadedPlugins();
}
```

LoadPluginsの中身。フォルダに対して"QuickLook.Plugin.*.dll"のファイルを全てリストアップ。dllファイルを読み込んで、Priorityプロパティでソート。

```
private void LoadPlugins(string folder)
{
    if (!Directory.Exists(folder))
        return;

    Directory.GetFiles(folder, "QuickLook.Plugin.*.dll",
            SearchOption.AllDirectories)
        .ToList()
        .ForEach(
            lib =>
            {
                (from t in Assembly.LoadFrom(lib).GetExportedTypes()
                        where !t.IsInterface && !t.IsAbstract
                        where typeof(IViewer).IsAssignableFrom(t)
                        select t).ToList()
                    .ForEach(type => LoadedPlugins.Add(type.CreateInstance<IViewer>()));
            });

    LoadedPlugins = LoadedPlugins.OrderByDescending(i => i.Priority).ToList();
}
```

その後各プラグインのinitメソッドを呼ぶ。

```
private void InitLoadedPlugins()
{
    LoadedPlugins.ForEach(i =>
    {
        try
        {
            i.Init();
        }
        catch (Exception e)
        {
            ProcessHelper.WriteLog(e.ToString());
        }
    });
}
```