2022年3月19日土曜日

Visual Studio 2022からSSHでgit pushする

備忘録程度に。

以前Synology NAS上に立てたGitサーバーにVS2022からSSH経由でpushしようとしたところ、以下のようなエラーが出て失敗しました。

master をプッシュしています
ssh_askpass: exec(c:/program files/microsoft visual studio/2022/community/common7/ide/commonextensions/microsoft/teamfoundation/team explorer/Git/mingw32/libexec/git-core\\git-askpass.exe): No such file or directory
Permission denied, please try again.
ssh_askpass: exec(c:/program files/microsoft visual studio/2022/community/common7/ide/commonextensions/microsoft/teamfoundation/team explorer/Git/mingw32/libexec/git-core\\git-askpass.exe): No such file or directory
Permission denied, please try again.
ssh_askpass: exec(c:/program files/microsoft visual studio/2022/community/common7/ide/commonextensions/microsoft/teamfoundation/team explorer/Git/mingw32/libexec/git-core\\git-askpass.exe): No such file or directory
gituser@192.168.*.*: Permission denied (publickey,password).
リモート リポジトリにブランチをプッシュできませんでした。詳しくは、出力ウィンドウをご覧ください。
リモート リポジトリへのブランチのプッシュ中にエラーが発生しました: Git failed with a fatal error.
Git failed with a fatal error.
Could not read from remote repository.

git-askpass.exeというバイナリが無いのが原因のようです。このバイナリは以下のサイトから入手できます。 

https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases/tag/v1.16.3

ここからダウンロードしたファイルを解凍し、そのまま当該フォルダに入れてやればOKです。

ちなみに最新のSynologyのOSではSSHポートが2222に変更されているようです(設定から変えられます)。ここもハマりポイントですので注意してください。

2022年3月14日月曜日

e-Paper(電子ペーパー)を制御する

先日日本橋の電気街をぶらぶらしていたところ、組み込み用の電子ペーパーを見かけました。

電子ペーパーと言えばKindleなどに採用されているディスプレイで、電源を切っても表示内容を維持することができるタイプの画面です。そのため、原理的に画面更新時にしか電力を消費せず、電子書籍のように1分に1回とかの更新で良いような用途では有用なディスプレイですね。

しかし、そのお店で売っていた電子ペーパーは1万円台前半と非常に高く、「ちょっと買ってみるか」くらいでは手が出るものではありませんでした。ですが、ちょっと調べてみると、秋月電子で2,500円の電子ペーパーが売っているじゃありませんか。普通の液晶に比べればちょっと高めですが、まあ、買ってやれないものでもないでしょう。

ということで、実際に買って制御してみました。まずは完成形から見ていきましょう。

見ての通り、画面の更新に約30秒かかります。これは仕様のようです。

SPIでコントローラーのSRAMのバッファーを更新するのはほとんど一瞬で終わりますが、その後の画面リフレッシュに30秒くらいかかるようです。 時計などの用途を考えるとあんまりイケてなさそうですね。

回路設計

回路自体はそんなに難しくないのですが、ドキュメントがイケてないのでめちゃめちゃわかりにくいです。

File:2.13inch e-Paper HAT Schematic.pdf

こちらが回路図になります。ただとても読みにくい。何がどこにつながっているのかかなり難解ですし、挙句の果てには「P1」「J2」「U1」などの部品番号も基板上にプリントされていないという有様です。

解説すると、液晶コントローラー自体は3.3Vで動作するようで、 5V系のマイコンでも操作できるようにレギュレーターとレベルコンバーターが付いているようです。そのレベルコンバーターがU1、レギュレーターがU2です。P3(基板に横向きに出ている白色コネクタ)からコントロールする場合は5Vで制御できます。

一方で、P1はピンソケットで、Raspberry Pi Zeroに直接接続することを意図しているようです(というか液晶サイズもRaspberry Pi Zeroピッタリです)。ですので、ピン配列はRaspberry Pi Zeroのものを参照するとわかりやすいでしょう。ここのコネクタから3.3V系の回路に直結できますので、3.3Vで動作するマイコンに接続する場合はP3を使用する必要はなく、P1から直接接続することができます。

今回はPIC16F18326を使うことにしました。理由は、170円と安価な割にプログラムメモリが16kワードと大きいからです。今回の液晶は104×212ドットの2色表示のため、画像1枚で5.5kBほど使います。

回路図はこんな感じになります。一応e-paper側でDeep Sleepに入ると消費電力が数μAまで下がるようですが、完全にシャットアウトできるようにPICでe-paperの電源を掌握できるようにしています。

ソフト作成

ドライバソフトはメーカーのGithubに載っています。

e-Paper

ただし、PIC用は無いため、Arduino用のを移植する必要があります。今回使うのは3色タイプのLCDなので、e-Paper/Arduino/epd2in13bc/ 以下のファイルを移植していきます。

#include "epd2in13bc.h"


int EpdIf_Init(void) {
    /*
    SPI.begin();
    SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0));
    */
    
    return 0;
}

void EpdIf_SpiTransfer(unsigned char data) {
    DO_SCL = 0;
    DO_CS = 0;

    for(int i = 0; i < 8; i ++) {
        DO_SDA = (data & 0x80) ? 1 : 0;
        data <<= 1;
        DO_SCL = 1;
        NOP();
        NOP();
        DO_SCL = 0;
    }

    DO_CS = 1;
}



int Epd_Init(void) {
    /* this calls the peripheral hardware interface, see epdif */
    if(EpdIf_Init() != 0) {
        return -1;
    }
    /* EPD hardware init start */
    Epd_Reset();
    Epd_SendCommand(BOOSTER_SOFT_START);
    Epd_SendData(0x17);
    Epd_SendData(0x17);
    Epd_SendData(0x17);
    Epd_SendCommand(POWER_ON);
    Epd_WaitUntilIdle();
    Epd_SendCommand(PANEL_SETTING);
    Epd_SendData(0x8F);
    Epd_SendCommand(VCOM_AND_DATA_INTERVAL_SETTING);
    Epd_SendData(0x37);
    Epd_SendCommand(RESOLUTION_SETTING);
    Epd_SendData(EPD_WIDTH);     // width: 104
    Epd_SendData(0x00);
    Epd_SendData(EPD_HEIGHT);     // height: 212
    /* EPD hardware init end */
    return 0;

}

/**
 *  @brief: basic function for sending commands
 */
void Epd_SendCommand(unsigned char command) {
    DO_DC = 0;
    EpdIf_SpiTransfer(command);
}

/**
 *  @brief: basic function for sending data
 */
void Epd_SendData(unsigned char data) {
    DO_DC = 1;
    EpdIf_SpiTransfer(data);
}

/**
 *  @brief: Wait until the busy_pin goes HIGH
 */
void Epd_WaitUntilIdle(void) {
    while(DI_BUSY == 0) {      //0: busy, 1: idle
        __delay_ms(100);
    }      
}

/**
 *  @brief: module reset. 
 *          often used to awaken the module in deep sleep, 
 *          see Epd::Sleep();
 */
void Epd_Reset(void) {
    DO_RST = 0;
    __delay_ms(200);
    DO_RST = 1;
    __delay_ms(200);   
}

/**
 *  @brief: transmit partial data to the SRAM
 */
void Epd_SetPartialWindow(const unsigned char* buffer_black, const unsigned char* buffer_red, int x, int y, int w, int l) {
    Epd_SendCommand(PARTIAL_IN);
    Epd_SendCommand(PARTIAL_WINDOW);
    Epd_SendData(x & 0xf8);     // x should be the multiple of 8, the last 3 bit will always be ignored
    Epd_SendData((unsigned char)(((x & 0xf8) + w  - 1) | 0x07));
    Epd_SendData((unsigned char)(y >> 8));        
    Epd_SendData(y & 0xff);
    Epd_SendData((unsigned char)((y + l - 1) >> 8));        
    Epd_SendData((y + l - 1) & 0xff);
    Epd_SendData(0x01);         // Gates scan both inside and outside of the partial window. (default) 
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_1);
    if (buffer_black != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_black[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_2);
    if (buffer_red != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_red[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(PARTIAL_OUT);  
}

/**
 *  @brief: transmit partial data to the black part of SRAM
 */
void Epd_SetPartialWindowBlack(const unsigned char* buffer_black, int x, int y, int w, int l) {
    Epd_SendCommand(PARTIAL_IN);
    Epd_SendCommand(PARTIAL_WINDOW);
    Epd_SendData(x & 0xf8);     // x should be the multiple of 8, the last 3 bit will always be ignored
    Epd_SendData((unsigned char)(((x & 0xf8) + w  - 1) | 0x07));
    Epd_SendData((unsigned char)(y >> 8));        
    Epd_SendData(y & 0xff);
    Epd_SendData((unsigned char)((y + l - 1) >> 8));        
    Epd_SendData((y + l - 1) & 0xff);
    Epd_SendData(0x01);         // Gates scan both inside and outside of the partial window. (default) 
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_1);
    if (buffer_black != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_black[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(PARTIAL_OUT);  
}

/**
 *  @brief: transmit partial data to the red part of SRAM
 */
void Epd_SetPartialWindowRed(const unsigned char* buffer_red, int x, int y, int w, int l) {
    Epd_SendCommand(PARTIAL_IN);
    Epd_SendCommand(PARTIAL_WINDOW);
    Epd_SendData(x & 0xf8);     // x should be the multiple of 8, the last 3 bit will always be ignored
    Epd_SendData((unsigned char)(((x & 0xf8) + w  - 1) | 0x07));
    Epd_SendData((unsigned char)(y >> 8));
    Epd_SendData(y & 0xff);
    Epd_SendData((unsigned char)((y + l - 1) >> 8));
    Epd_SendData((y + l - 1) & 0xff);
    Epd_SendData(0x01);         // Gates scan both inside and outside of the partial window. (default) 
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_2);
    if (buffer_red != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_red[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(PARTIAL_OUT);  
}

/**
 * @brief: refresh and displays the frame
 */
void Epd_DisplayFrame(const unsigned char* frame_buffer_black, const unsigned char* frame_buffer_red) {
    if (frame_buffer_black != NULL) {
        Epd_SendCommand(DATA_START_TRANSMISSION_1);
        __delay_ms(2);
        for (int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
            Epd_SendData(frame_buffer_black[i]);
            //Epd_SendData(~(i & 0xFF));
        }
        __delay_ms(2);
    }
    if (frame_buffer_red != NULL) {
        Epd_SendCommand(DATA_START_TRANSMISSION_2);
        __delay_ms(2);
        for (int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
            Epd_SendData(frame_buffer_red[i]);
            //Epd_SendData(0xFF);
        }
        __delay_ms(2);
    }
    Epd_SendCommand(DISPLAY_REFRESH);
    Epd_WaitUntilIdle();
}

/**
 * @brief: clear the frame data
 */
void Epd_ClearFrame(void) {
    Epd_SendCommand(DATA_START_TRANSMISSION_1);           
    __delay_ms(2);
    for(int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
        Epd_SendData(0xFF);  
    }  
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_2);           
    __delay_ms(2);
    for(int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
        Epd_SendData(0xFF);  
    }  
    __delay_ms(2);

    Epd_SendCommand(DISPLAY_REFRESH);
    Epd_WaitUntilIdle();
}

/**
 * @brief: This displays the frame data from SRAM
 */
void Epd_Refresh(void) {
    Epd_SendCommand(DISPLAY_REFRESH); 
    Epd_WaitUntilIdle();
}


/**
 * @brief: After this command is transmitted, the chip would enter the deep-sleep mode to save power. 
 *         The deep sleep mode would return to standby by hardware reset. The only one parameter is a 
 *         check code, the command would be executed if check code = 0xA5. 
 *         You can use Epd::Reset() to awaken and use Epd::Init() to initialize.
 */
void Epd_Sleep() {
    Epd_SendCommand(POWER_OFF);
    Epd_WaitUntilIdle();
    Epd_SendCommand(DEEP_SLEEP);
    Epd_SendData(0xA5);     // check code
}
#ifndef EPD2IN13B_H
#define EPD2IN13B_H

#include <xc.h>
#define _XTAL_FREQ 32000000    // Fosc = 8MHz


#define DO_SDA          LATCbits.LATC2
#define DO_SCL          LATCbits.LATC0
#define DO_CS           LATCbits.LATC4
#define DO_DC           LATCbits.LATC5
#define DO_RST          LATCbits.LATC3
#define DI_BUSY         PORTAbits.RA4


// Display resolution
#define EPD_WIDTH       104
#define EPD_HEIGHT      212

// EPD2IN13B commands
#define PANEL_SETTING                               0x00
#define POWER_SETTING                               0x01
#define POWER_OFF                                   0x02
#define POWER_OFF_SEQUENCE_SETTING                  0x03
#define POWER_ON                                    0x04
#define POWER_ON_MEASURE                            0x05
#define BOOSTER_SOFT_START                          0x06
#define DEEP_SLEEP                                  0x07
#define DATA_START_TRANSMISSION_1                   0x10
#define DATA_STOP                                   0x11
#define DISPLAY_REFRESH                             0x12
#define DATA_START_TRANSMISSION_2                   0x13
#define VCOM_LUT                                    0x20
#define W2W_LUT                                     0x21
#define B2W_LUT                                     0x22
#define W2B_LUT                                     0x23
#define B2B_LUT                                     0x24
#define PLL_CONTROL                                 0x30
#define TEMPERATURE_SENSOR_CALIBRATION              0x40
#define TEMPERATURE_SENSOR_SELECTION                0x41
#define TEMPERATURE_SENSOR_WRITE                    0x42
#define TEMPERATURE_SENSOR_READ                     0x43
#define VCOM_AND_DATA_INTERVAL_SETTING              0x50
#define LOW_POWER_DETECTION                         0x51
#define TCON_SETTING                                0x60
#define RESOLUTION_SETTING                          0x61
#define GET_STATUS                                  0x71
#define AUTO_MEASURE_VCOM                           0x80
#define READ_VCOM_VALUE                             0x81
#define VCM_DC_SETTING                              0x82
#define PARTIAL_WINDOW                              0x90
#define PARTIAL_IN                                  0x91
#define PARTIAL_OUT                                 0x92
#define PROGRAM_MODE                                0xA0
#define ACTIVE_PROGRAM                              0xA1
#define READ_OTP_DATA                               0xA2
#define POWER_SAVING                                0xE3

int  Epd_Init(void);
void Epd_SendCommand(unsigned char command);
void Epd_SendData(unsigned char data);
void Epd_WaitUntilIdle(void);
void Epd_Reset(void);
void Epd_SetPartialWindow(const unsigned char* buffer_black, const unsigned char* buffer_red, int x, int y, int w, int l);
void Epd_SetPartialWindowBlack(const unsigned char* buffer_black, int x, int y, int w, int l);
void Epd_SetPartialWindowRed(const unsigned char* buffer_red, int x, int y, int w, int l);
void Epd_DisplayFrame(const unsigned char* frame_buffer_black, const unsigned char* frame_buffer_red);
void Epd_Refresh(void);
void Epd_ClearFrame(void);
void Epd_Sleep(void);

#endif

オリジナルのライブラリでは、ハードウェアのインターフェース部と論理部できれいに分けて実装されていたのですが、移植するにあたってごちゃ混ぜにしてしまいました。XC8がC++をサポートしていないうえ、PICのピン操作は直接レジスタ触ることになるというのが主な理由です。もう少しやりようはあったかもしれませんが、まあ処理速度優先ということで。

あと、一応回路上ではPICのMSSP(ハードウェアSPI)に対応したピンにアサインしているのですが、SPI送信中に何か別のことをしたいわけでもないので、ソフトウェアで簡単にSPIを実装しています。SPI程度ならMSSPの使い方をデータシート見ながら実装するよりGPIOで作っちゃったほうが簡単ですね。

void UpdateLcd()
{
    DO_LCD_PWR = 1;
    TRIS_LCD_PWR = 0;
    
    __delay_ms(100);
    
    Epd_Init();

    switch(content++) {
        case 0:
            Epd_DisplayFrame(img1_black, img1_yellow);
            break;
        case 1:
            Epd_DisplayFrame(img2_black, img2_yellow);
            break;
        default:
            Epd_ClearFrame();
            content = 0;
            break;
    }

    Epd_Sleep();    
    TRIS_LCD_PWR = 1;
    LATA = 0;
    LATC = 0;
}

void init()
{
    LATA = 0;
    WPUA = 0x20;
    ODCONA = 0;
    ANSELA = 0;
    TRISA = 0xFF;

    LATC = 0;
    WPUC = 0;
    ODCONC = 0;
    ANSELC = 0;
    TRISC = 0xC2;

    IOCAN = 0x20;
    PIE0 = 0x10;
    INTCON = 0x80;
}

void loop()
{
    SLEEP();
    NOP();
    
    if(DI_SW == 0)
        UpdateLcd();    
}

void __interrupt() ISR(void)
{
    if(PIR0bits.IOCIF) {
        if(IOCAFbits.IOCAF5) {
            IOCAFbits.IOCAF5 = 0;
        }
    }
}

void main(void) {
    init();
    
    while(1)
        loop();
}

main.cでは処理をしていない間はPICをスリープにするようにしています。スイッチを押されるとInterrupt On Change (IOC)にてスリープからウェイクアップすると続きの処理を実行するようになります。この際発生する割り込みの割り込みフラグはちゃんとリセットしないとスリープに入れない(常にスリープ復帰条件ができてしまう)ので、割り込みルーチンではフラグのリセットのみを行っています。

また、e-Paperをdeep sleepにした後はLCDの電源を切っていますが、その際に制御用ピンがHighになっていると、そちらから電源側へ電流が流れてしまう(おそらく液晶コントローラー内に保護用にダイオードが入っている)ので、同時にすべてのピンの出力をゼロにしています。

これで、待機中はナノアンペアオーダーの電流しか流れない、ボタン電池で駆動しても何の支障もないe-Paperができました。

 

それにしても、更新に30秒かかるのはちょっと残念ですね…。せめて数秒程度にしてほしかった…。

2022年2月19日土曜日

Wordleを攻略する

最近巷で流行っているゲームです。

Wordle - The New York Times 

入力された単語から「その文字は含まれない」「その文字は含まれるが別の場所に入る」「その文字はその場所に含まれる」の3通りの判定結果が出るので、それをもとにお題の英単語を当てるというゲームです。

やってみると意外と難しくて、2, 3個の単語を入れた後にすべての条件を満たす単語をいろいろと考えてもなかなか思いつきません。辞書を使っても絶妙に単語が見つけられないくらいのゲームバランスは何といっても絶妙です。

また、日替わりで全員が同じお題でやるのですが、3色の文字色を使ったSNS共有も印象的です。私もTwitterでWordleのツイートが流れてきたのを見て知ったくちです。

候補の単語を知る

さて、攻略をしようとすると 真っ先に必要なのは候補の単語です。実は単語リストは 公開されています。

Wordle 単語リスト

12,972語あります。この中から条件を満たす単語を抽出することができればゲームをスムーズに進められるようになります。チートですが。

public static IReadOnlySet<string> NominateCandidates(IEnumerable<IWordHint> wordhints)
{
	var original = WordList.All;

	// 正しい場所の文字を抽出
	var exactpos = wordhints
		.SelectMany(p => p.Characters.Select((p, i) => (character: p, pos: i)).Where(p => p.character.Hint == HintState.ExactPosition))
		.Select(p => (character: char.ToLower(p.character.Character), pos: p.pos))
		.Distinct()
		.ToArray();

	if(exactpos.Length > 0) {
		var filtered = new SortedSet<string>();
		foreach(var w in original) {
			if(exactpos.All(ep => w[ep.pos] == ep.character))
				filtered.Add(w);
		}
		original = filtered;
	}

	// 含まれていない文字を除外
	var notcontained = wordhints
		.SelectMany(p => p.Characters)
		.Where(p => p.Hint == HintState.NotContained)
		.Select(p => char.ToLower(p.Character))
		.Distinct()
		.ToArray();

	if(notcontained.Length > 0) {
		var filtered = new SortedSet<string>();
		foreach(var w in original) {
			if(notcontained.All(nc => !w.Contains(nc)))
				filtered.Add(w);
		}
		original = filtered;
	}

	// 異なる場所の文字を抽出
	var notinpos = wordhints
		.SelectMany(p => p.Characters.Select((p, i) => (character: p, pos: i)).Where(p => p.character.Hint == HintState.NotInPosition))
		.Select(p => (character: char.ToLower(p.character.Character), pos: p.pos))
		.Distinct()
		.ToArray();

	if(notinpos.Length > 0) {
		var filtered = new SortedSet<string>();
		foreach(var w in original) {
			if(notinpos.All(nip => w[nip.pos] != nip.character && w.Contains(nip.character)))
				filtered.Add(w);
		}
		original = filtered;
	}

	return original;
}

Wordleに単語を入力すると、それぞれの文字が3種類の判定結果になるので、3パターンの検索をします。

正しい場所の文字を抽出→含まれていない文字を除外→異なる場所の文字を抽出の順で処理をしているのは、数がグッと減る検索を先にやったほうが効率が良いと考えたからです。本当にこの順で数がグッと減るかどうかはわかりませんが。

ちなみに、上のコードに出てくるインターフェースや列挙体などは以下の通りです。

public interface IWordHint
{
	IReadOnlyList<ICharacterHint> Characters { get; }
}

public interface ICharacterHint
{
	char Character { get; }
	HintState Hint { get; }
}

public enum HintState
{
	NotContained,
	NotInPosition,
	ExactPosition,
}

あとはIWordHintを入力するUIを実装してやればいいだけです。

ここに置いておきます。

WordleAssistant.zip

次に何の単語を入れたらいいか考える

さて、じゃあ肝心の「次に入力する単語」はどうやって選べば良いでしょうか。

これを体系化して導くのはおそらく一筋縄にいきません。また機会があったら(めどが立ったら)記事にしてみようかと思います。

2021年12月11日土曜日

WPFのMenuを動的に生成する(もちろんMVVM)

今の時代メニューなんて流行らないんですかね。まあそういう私もRibbon UIでソフトを作ることが多いのであまりメニューを使うことも無いのですが、たまに使った際に動的なMenuを生成しようとしてググったところあまり良い記事が無かったのでここでまとめておきます。

動的なメニュー

こういうやつです。

今回はNIC(Network Interface Card)を列挙してメニュー項目として登録しています。当然NICはパソコンによって搭載しているものが違うため、ソフトウェアを実行するまで名前はわかりません。よって、ソフト実行中に(=動的に)メニュー項目を追加する必要が出てくるのです。

もちろんNICじゃなくても構いません。COMポートでも良いし、「最近使ったファイル」でも良いです。そういうところで動的なメニューが欲しくなることはしばしばあります。 

ItemsSource

さて、もはやこれが答えですが、MenuItemクラスにはItemsSourceプロパティがあります。勘のいいひとは気づくと思いますが、このプロパティに子アイテムのObservableCollectionをバインディングしてあげれば終わりです。基本的な作戦はこれになります。

<DockPanel>
	<Menu DockPanel.Dock="Top">
		<MenuItem Header="File" >
			<MenuItem Header="Open NIC" ItemsSource="{Binding NICs}" />
			<MenuItem Header="Close NIC" />
		</MenuItem>
	</Menu>
	<Grid />
</DockPanel>
public class MainWindowViewModel : ViewModel
{
	MainWindowModel model;
	public MainWindowViewModel()
	{
		model = MainWindowModel.GetInstance();
		NICs = model.NICs;
	}

	public void Initialize()
	{
		model.LoadNICList();
	}

	public ReadOnlyReactiveCollection<string> NICs { get; }

}
public class MainWindowModel
{
	#region Singleton

	static MainWindowModel? instance = null;
	public static MainWindowModel GetInstance()
	{
		if(instance == null)
			instance = new MainWindowModel();
		return instance;
	}

	#endregion

	MainWindowModel()
	{
		_NICs = new ReactiveCollection<string>();
		NICs = _NICs.ToReadOnlyReactiveCollection();
	}

	public void LoadNICList()
	{
		foreach (var capture in LibPcapLiveDeviceList.Instance.OrderBy(p => p.Interface.FriendlyName))
			_NICs.Add($"{capture.Interface.FriendlyName} ({capture.Description})");
	}

	readonly ReactiveCollection<string> _NICs;
	public ReadOnlyReactiveCollection<string> NICs { get; }
}

通知可能コレクションは私はReactivePropertyのReactiveCollection派なのでこんなコードになっていますが、そこは好みに合わせてなんでもいいです。ちなみにMVVMインフラはLivetを使っていて、NICの列挙にはSharpPcapを使っています。

一応コードの解説をしておくと、Windowロード時にViewModelのInitializeメソッドが呼ばれるため、そこでModelにNIC名の取得を指示しています。ModelではNICs通知可能コレクションにNIC名を追加し、ViewModelを介してMenuItemのItemsSourceにバインディングされます。これだけでもう最初に示した画像のような動的なメニューは完成です。

MVVM化

さて、stringのコレクションをItemsSourceにバインドすることで動的にメニューを生成することができましたが、すでに気づいている人もいるかもしれませんが、このままではそれらのメニューを押した時のイベントに対応することができません。さすがにそれではメニューとして使い物にならないでしょう。ほかにも、動的に生成したメニューを無効化したりチェックマークを入れたりしたくなることもあるかと思います。

そこで、MenuItem一つにつき一つのViewModelを持たせる方法で実装をします。MenuItemに限らず、MVVMではMainWindowViewModelで書ききれないもの、個数が変化する要素などがその典型ですが、別のViewModelを作ってバインディングしてしまうのがベストプラクティスです。

public class NICMenuItemViewModel : ViewModel
{
	NICMenuItemModel model;

	public NICMenuItemViewModel(NICMenuItemModel model)
	{
		this.model = model;

		MenuHeader = model.MenuHeader;
		NICName = model.NICName;
		
		MenuCommand = new ReactiveCommand();
		MenuCommand.Subscribe(() => model.Invoke());
	}

	public string MenuHeader { get; }
	public string NICName { get; }

	public ReactiveCommand MenuCommand { get; }
}
public class NICMenuItemModel
{
	Action<string> menuclicked;

	public NICMenuItemModel(string header, string name, Action<string> menuclicked)
	{
		MenuHeader = header;
		NICName = name;
		this.menuclicked = menuclicked;
	}

	public string MenuHeader { get; }
	public string NICName { get; }

	public void Invoke()
	{
		menuclicked?.Invoke(NICName);
	}
}

まずこれがMenuItemに対応するViewModelとModelです。MenuItemに表示されるテキストを表すMenuHeaderプロパティと、その項目に対応するNICNameプロパティを用意しています。通常は通知可能プロパティにすべきですが、不変で困らないのでそこはサボっています。

ViewModelにはさらにMenuCommandというプロパティを用意しており、メニューがクリックされたときはこのコマンドが発動するようにしています。それが押されるとModelのInvokeメソッドが呼ばれ、Model生成時にコンストラクタで渡していたActionが発動するようにしています。この辺りはEventで実装するなどアレンジは自由かと思います。

public class MainWindowModel
{
	#region Singleton

	static MainWindowModel? instance = null;
	public static MainWindowModel GetInstance()
	{
		if(instance == null)
			instance = new MainWindowModel();
		return instance;
	}

	#endregion

	MainWindowModel()
	{
		_NICs = new ReactiveCollection<NICMenuItemModel>();
		NICs = _NICs.ToReadOnlyReactiveCollection();
	}

	public void LoadNICList()
	{
		foreach (var capture in LibPcapLiveDeviceList.Instance.OrderBy(p => p.Interface.FriendlyName))
			_NICs.Add(new NICMenuItemModel($"{capture.Interface.FriendlyName} ({capture.Description})", capture.Name, name => OpenNIC(name)));
	}

	public void OpenNIC(string name)
	{
		//throw new NotImplementedException();
	}

	readonly ReactiveCollection<NICMenuItemModel> _NICs;
	public ReadOnlyReactiveCollection<NICMenuItemModel> NICs { get; }
}
public class MainWindowViewModel : ViewModel
{
	MainWindowModel model;
	public MainWindowViewModel()
	{
		model = MainWindowModel.GetInstance();
		NICs = model.NICs.ToReadOnlyReactiveCollection(p => new NICMenuItemViewModel(p));
	}

	public void Initialize()
	{
		model.LoadNICList();
	}

	public ReadOnlyReactiveCollection<NICMenuItemViewModel> NICs { get; }
}

MainWindowModelではNICロード時にNICMenuItemModelインスタンスを生成しています。MainWindowViewModelではModelのNICsプロパティの中身をModelからViewModelに変換してViewModelのNICsとして保持しています。この辺りはMVVMのお決まりパターンです。

最後にXAMLです。

<DockPanel>
	<Menu DockPanel.Dock="Top">
		<MenuItem Header="File" >
			<MenuItem Header="Open NIC" ItemsSource="{Binding NICs}" >
				<MenuItem.ItemContainerStyle>
					<Style TargetType="MenuItem">
						<Setter Property="Header" Value="{Binding MenuHeader}" />
						<Setter Property="Command" Value="{Binding MenuCommand}" />
					</Style>
				</MenuItem.ItemContainerStyle>
			</MenuItem>
			<MenuItem Header="Close NIC" />
		</MenuItem>
	</Menu>
	<Grid />
</DockPanel>

NICsプロパティをItemsSourceにバインディングしただけではメニューの表示項目はNICMenuItemViewModel.ToString()の実行結果になってしまいます。そこで、ItemContainerStyleを使い、各子要素のHeaderプロパティをViewModelのMenuHeaderプロパティ、CommandプロパティをViewModelのMenuCommandプロパティにバインディングしています。

これで意図したとおり動的に生成したメニューをクリックしたらいろいろ伝わって最終的にMainWindowModelのOpenNICメソッドが呼ばれるようになります。ここで煮るなり焼くなり好きにすれば良いでしょう。

まとめ

今回はMVVMを保ちながらWPFのMenuを動的に生成する方法を見てきました。なんだかついでにMVVMの実装実例を紹介する記事にもなってしまいましたね。

MVVMでは数が減ったり増えたりするものにはItemsSourceに要素用のViewModelを作ってバインディング、これさえ覚えておけばメニュー以外でもいろいろと応用できそうです。

2021年11月23日火曜日

弾性衝突で円周率を求める話

最近、YouTubeでこんな動画を見かけました。

2つの物体を衝突させる話です。左端には壁があり、2つの物体を並べ、右側の物体を左に向かって滑らせます。摩擦はなく、すべての衝突が弾性衝突だとすると、左と右の物体の質量比が$100^n$のとき、左の物体の衝突回数は円周率の小数第$n$位までを整数で表したものになるというのです。

少し調べてみると比較的有名なことのようですが、私は初耳でした。私はうっかり解説動画から見てしまったので、自分で考える間もなくなぜそうなるかを知ってしまったのですが、もしも初めて聞いた人は答えを見ずに自分で考えてみることをお勧めします。何もヒントなしに証明できた人がいたとしたら大したものです。

自分で考えてみる 

さて、解説動画を見ているとところどころで天才的なひらめきが出てくるのですが、いったんそれは忘れて少し考えてみましょう。

右向きに$x$軸を取り、右側の物体を物体1とし、その質量と速度をそれぞれ$m_1$, $v_1$、左側の物体を物体2としk、その質量と速度をそれぞれ$m_2$, $v_2$と置きます。

こういうのはそれぞれの物体の位置と速度を計算するとドツボにはまります。使用すべきはエネルギー保存則と運動量保存則です。2つの物体が衝突するときは弾性衝突なので2つの物体のエネルギーの合計も運動量の合計も保存します。左の物体が壁にぶつかるときは、弾性衝突なのでエネルギーは保存しますが運動量は正負反転します。

まずは2つの物体が衝突するときのことを考えてみましょう。

\[\left\{
\begin{array}{l}
\dfrac{1}{2}m_1v_1^2+\dfrac{1}{2}m_2v_2^2=k \\
m_1v_1+m_2v_2=p
\end{array}
\right. \]

何の変哲もない、ただのエネルギー保存則と運動量保存則です。私は凡人なので、これを連立させて2次方程式を解こうとしました。$v_2$を削除し$v_1$を求めます。ただの2次方程式なので解の公式を使えば求められますが、結構計算量が多くしんどい計算となります。

\[v_1=\dfrac{p\pm{m_2\sqrt{2k(\frac{1}{m_1}+\frac{1}{m_2})-\frac{1}{m_1m_2}p^2}}}{m_1+m_2}\]

ルートの中身を$m_1$と$m_2$が対称になるように整理するのがポイントです。これを運動量保存則の式に当てはめて$v_2$を計算します。

\[v_2=\dfrac{p\mp{m_1\sqrt{2k(\frac{1}{m_1}+\frac{1}{m_2})-\frac{1}{m_1m_2}p^2}}}{m_1+m_2}\]

ルートの中身を対称にしたおかげでかなりきれいに計算できました。

この式が意味するところは、「2つの物体のエネルギーの合計が$k$、運動量の合計が$p$の場合、$v_1$と$v_2$の取りうる組み合わせは2ペアある」ということになります。これはすなわち物体の衝突前の状態と衝突後の状態です。衝突の後は物体1はより右向きの速度が上がりますので、下側の符号が衝突前、上側の符号が衝突後の速度ということになりますね。

さて、物体同士が衝突した後に物体2が壁にぶつかると物体2の運動量の正負が反転します。すなわち、物体1,2の運動量の合計としては

\[p=m_1v_1-m_2v_2\]

と表せます。これを使えば、物体1,2がぶつかってから物体2が壁にぶつかるという1サイクルでの運動量の変化を漸化式で表すことができるようになります。わかりやすく、サイクル開始時点での運動量を$p_n$、終了時点での運動量を$p_{n+1}$としてみましょう。

\[\begin{eqnarray}p_{n+1}&=&m_1\dfrac{p_n+m_2\sqrt{2k(\frac{1}{m_1}+\frac{1}{m_2})-\frac{1}{m_1m_2}p_n^2}}{m_1+m_2}-m_2\dfrac{p_n-m_1\sqrt{2k(\frac{1}{m_1}+\frac{1}{m_2})-\frac{1}{m_1m_2}p_n^2}}{m_1+m_2}\nonumber\\&=&\dfrac{(m_1-m_2)p_n+2m_1m_2\sqrt{2k(\frac{1}{m_1}+\frac{1}{m_2})-\frac{1}{m_1m_2}p_n^2}}{m_1+m_2}\nonumber\end{eqnarray}\]

さて、運動量$p$に関する漸化式ができました。この漸化式のnを数えれば衝突回数がわかるはずです(※1)。終了条件はもう衝突が起こらなくなるということで、それはすなわち運動量の絶対値が初期運動量の絶対値以上になったときと言えます(※2)。

※1:nが1増えるたびに衝突は2回起こります。
※2:これは正確には壁にぶつからなくなる条件で、この後に物体同士でぶつかるかは検証する必要があります。

シミュレーション

ここまで来たので、今までの計算が正しいことを確認するために実際にプログラムを書いて漸化式を数値的に解いてみます。

double n = 7;
double v1 = -1;
double v2 = 0;
double m1 = Math.Pow(10, n * 2);
double m2 = 1;

double p = m1 * v1 + m2 * v2;
double p0abs = Math.Abs(p);
double k = (m1 * v1 * v1 + m2 * v2 * v2) / 2;

uint collision = 0;

while(true) {
	var pprev = p;
	var sqrt = CalcSqrt(p, k, m1, m2);
	p = CalcNextMomentum(sqrt, p, m1, m2);
	collision += 2;

	if(Math.Abs(p) >= p0abs) {  // 合計運動量が初期運動量の反対向きを超えていたらもう壁にはぶつからない
		// -v2 > v1 ⇔ v1 + v2 < 0 だったらもう1回物体同士でぶつかる
		if(2 * pprev + (m2 - m1) * sqrt < 0)
			collision++;
		break;
	}
}

Console.WriteLine($"Pi = {collision / Math.Pow(10, n)}");


static double CalcSqrt(double p, double k, double m1, double m2)
{
	return Math.Sqrt(2 * k * (1 / m1 + 1 / m2) - 1 / (m1 * m2) * p * p);
}

static double CalcNextMomentum(double sqrt, double p, double m1, double m2)
{
	return ((m1 - m2) * p + 2 * m1 * m2 * sqrt) / (m1 + m2);
}

余談ですがC#10.0/.NET6.0になってnamespaceやMainメソッドが省略されるようになりました。このままのコピペでコンパイルが通ります。

運動量の漸化式を計算するメソッドがCalcNextMomentumですが、平方根部分のみを別途計算するCalcSqrtメソッドを用意しています。運動量を計算していって運動量の絶対値が初期運動量の絶対値以上となったらもうこれ以上の壁への衝突は発生しないものとして、物体同士で最後にぶつかるかどうかの判定へ移ります。

この時点での2つの物体の速度は、1回前のループで計算した運動量に対して計算した$v_1$と$-v_2$になるはずです。その大小関係ですので分子のみの差をとって判定しています。ここでルート部分が再利用できるため、メソッドを分けてわざわざルート部分の値を保存していたのです。

このプログラムは小数第7位までの計算($n=7$)まで正常に動きます。それ以上は浮動小数点型の有効桁数が足りずに正確に計算できず、いつまでたってもループを抜け出せなくなってしまいます。

漸化式を解く

この漸化式解けるのか…?

まとめ

というわけで、多分私がこの問題だけを見て答えを見ていなかったら、ここまで解いて詰んで終わっていたでしょう。あの「エネルギー保存則円の方程式運動量保存則直線の方程式とみたてて平面上に表すと、衝突回数が円周運動量保存則の直線傾き角の2倍割ったものと考えることができる」とかいう天才的なひらめき、いったいどういう練習をしたら思いつけるようになるんだか。

2021年4月24日土曜日

Tap.NET 0.2.0リリース

Tap.NETをリリースしてから3年近く経ちましたが、最近個人的にまたこの仮想NICを使う機会が増えて、しばしば自分で使っています。

そうすると初めていろいろと不満が出てくるわけです。特にクリティカルなのは、世の中はとっくに.NET Frameworkはオワコン化して.NET Coreに移り変わっているというのに、このライブラリは.NET Frameworkのしかもx86ビルドを要求してきます。どう見ても過去の遺物です。

というわけで、少なくとも.NET Core 3.1以上のAnyCPUビルドで使えるようにしたいなーと思い、エッセラホイサと移植作業を行いました。C++/CLIからC#で焼き直しましたので、ほぼ完全に焼き直しています。その際、いくつか破壊的変更もしています。ご了承下さい。

使い方

Nugetからインストールしてください。.NET Core 3.1のパッケージしか入れていませんが、今後作るソフトではこれだけ入っていれば困らないでしょう。

TAP.NET 0.2.0

インスタンス生成方法

旧バージョンではGUIDを与えて初期化するコンストラクターしかありませんでしたが、今バージョンからは無引数のコンストラクラーを用意し、ワンタッチでデバイスを開けるようにしました。今まで入れていなかったのは、単にC++/CLIで実装するのが面倒だっただけですが、C#に移植することでこういうのも簡単にできるようになりました。

using var tap = new Tap();

送信方法

これは以前と何ら変わりません。

using var tap = new Tap();

var ethernetPacket = new EthernetPacket(PhysicalAddress.Parse("90-90-90-90-90-90"), PhysicalAddress.Parse("80-80-80-80-80-80"), PacketDotNet.EthernetType.None) {
    PayloadPacket = new IPv4Packet(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.2")) {
        PayloadPacket = new UdpPacket(123, 456),
    },
};
tap.SendData(ethernetPacket.Bytes);

(これをコンパイルするにはPacket.NETが必要です)

Packet.NETの力でUDPパケットを作りだし、そのバイト列をSendDataメソッドに渡していますそれだけです。もちろんここにはEthernetパケットを渡してあげる必要があります。

受信方法

こちらは前バージョンから少し変えました。

using var tap = new Tap();

tap.DestinationMacFilter = PhysicalAddress.Parse("FF-FF-FF-FF-FF-FF");
tap.EthernetTypeFilter = TapDotNet.EthernetType.IPv4;

tap.DataReceived += (sender, e) => {
    Console.WriteLine(Packet.ParsePacket(LinkLayers.Ethernet, e.Data));
    Console.WriteLine();
};
tap.StartReceiving();

(これをコンパイルするにもPacket.NETが必要です)

データの受信を開始するにはStartReceiving()メソッド、停止するにはStopReceiving()メソッドを呼ぶように変更しています。ですがそんなに違和感のある仕様ではないでしょう。

データを受信したらDataReceivedイベントが発生しますが、イベントが大量に発生するとEventArgsのインスタンス化等でそれなりにコストがかかるため、フィルター機能を用意しています。Ethernetフレームのみを対象のフィルターで、DestinationMacFilter、SourceMacFilter、EthernetTypeFilterに何かしらの値を入れることで、その値を含むパケットを受信したときのみイベントが発生するようになります。これらのフィルターをnullにするとその項目は素通しとなります。EthernetTypeはFlagsの列挙体ですので、複数指定も可能です。細かな仕様が若干違う点もありますが、Ethernetフレームに対してフィルターを掛けられるのは以前のバージョンと同じです。

中身の実装の話

さて、C#/.NET Coreに移植するにあたっていくつか出てきた話を。

P/Invoke

C++/CLIからC#への移植と言っても、基本的にはC++/CLIから直接呼び出していたWind32APIをP/Invokeという機能でC#から呼び出すようにするだけです。TAP-Windowsを利用するためには、CreateFileW関数でデバイスをオープン、DeviceIoControl関数で有効化し、WriteFile関数でデータ送信、ReadFile関数でデータを受信、最後はCloseHandle関数でデバイスをクローズしますので、最低限この5つの関数をC#から使えるようにする必要があります。

internal class Kernel32
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    internal static extern IntPtr CreateFileW(
        [MarshalAs(UnmanagedType.LPWStr)] string filename,
        [MarshalAs(UnmanagedType.U4)] FileAccess access,
        [MarshalAs(UnmanagedType.U4)] FileShare share,
        IntPtr securityAttributes,
        [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
        [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
        IntPtr templateFile);


    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern bool CloseHandle(IntPtr hObject);

    [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    internal static extern bool DeviceIoControl(
        IntPtr hDevice,
        uint ioControlCode,
        [MarshalAs(UnmanagedType.LPArray)] [In] byte[] inBuffer,
        int ninBufferSize,
        [MarshalAs(UnmanagedType.LPArray)] [Out] byte[] outBuffer,
        int noutBufferSize,
        out uint bytesReturned,
        [In] IntPtr overlapped
    );

    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern bool WriteFile(
        IntPtr hFile, [In] byte[] lpBuffer,
        uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten,
        IntPtr lpOverlapped);

    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern bool ReadFile(
        IntPtr hFile, [Out] byte[] lpBuffer,
        uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead,
        IntPtr lpOverlapped);
}

その他後述しますが、IO中断絡みでの新規対応が必要でしたので、それ周りの関数も同様に準備する必要があります。

CreateFile関数ですが、System.IO.FileAccess列挙体などの値を直接使うようです。最初、Wind32APIで渡すDWORD値をそのまま入れたらうまくデバイスが開けなくてしばらく格闘していました。

// TAPデバイスを開く
hTap = Kernel32.CreateFileW(path, FileAccess.ReadWrite, FileShare.None, IntPtr.Zero, FileMode.Open, FileAttributes.System, IntPtr.Zero);
if(hTap.ToInt64() == -1)
    throw new InvalidOperationException("Cannot open the device");

Thread.Abort()メソッドの廃止

フレームワーク側の変更として、Thread.Abort()メソッドの廃止というものがありました。.NET Frameworkでは問題なく動作していましたが、.NET Coreではサポート外となるようです。

データの受信処理では、受信用のスレッドを生成し、そこでReadFileを呼び出してパケットを受信しています。今までは終了処理時にThread.Abort()メソッドで強制的にスレッドを停止させ終了していたのですが、その方法は使えなくなってしまいました。

その代替方法はCancellationTokenとのことですが、CancellationTokenはあくまでも処理の中断要求を指示して、別スレッドがそのフラグを見たときに中断処理を行うというものです。しかし、受信用スレッドではデータの受信が無ければReadFile関数から処理は帰って来ませんので、いつまで経ってもCancellationTokenのフラグを見ることができず終了できません。

そこで、Win32APIのCancelSynchronousIo関数を使います。これは、特定のスレッドで同期的にIO処理をしている際に、処理を中断させることが出きる関数です。この関数に与えるハンドルはIOデバイスのハンドルではなくスレッドのハンドルになるので注意が必要です。

public void StopReceiving()
{
    if(IsReceiving) {
        canceltokensource!.Cancel();

        // ReadFileの中断
        var thread = Kernel32.OpenThread(0x40000000, false, threadid);
        Kernel32.CancelSynchronousIo(thread);
        Kernel32.CloseHandle(thread);

        watcher!.Join();
        watcher = null;
        canceltokensource = null;
    }
}

void ReceiveThread()
{
    threadid = Kernel32.GetCurrentThreadId();

    var buffer = new byte[1518];

    while(!canceltoken.IsCancellationRequested) {
        if(Kernel32.ReadFile(hTap, buffer, (uint)buffer.Length, out var length, IntPtr.Zero)) {
            if(Filter(buffer, length)) {
                var data = new byte[length];
                Array.Copy(buffer, data, length);
                RaiseDataReceived(data);
            }
        } else {
            var error = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
            if(error != 0 && error != 995) {
                RaiseErrorHappened("ReadFile Error", error);
                break;
            }
        }
    }
}

C#で生成したスレッドではWin32のスレッドIDを知るすべはありませんので、まずはスレッド立ち上げ時にGetCurrentThreadId関数でスレッドIDを保存しておきます。

スレッド終了時は、それをもとにスレッドハンドルを取得し、CancelSynchronousIo関数を呼び出してIOの中止指示を出します。

ReadFile関数は、IOが中止されるとfalseを返し、エラーコード995(ERROR_OPERATION_ABORTED)を残します。これは意図したエラーですので、エラーイベントには流さないように配慮しています。

これにより、ReadFile関数がパケットの受信を待機している最中でも、強制的にそれを中断させてスレッドを終了させることができるようになりました。


てなところで、.NET CoreのAnyCPUからTAP-Windowsを触れるようになりました。

今までずっとprereleaseモードで公開していたからかダウンロード数はあまり伸びていませんが、類似ライブラリは今のところありませんので(多分)、それなりにみんな使ってくれないかなーと期待しています。

えっ?わざわざ仮想NICに外からパケットを流し込みたい人なんてそうそういない?そんなあ。

2021年3月21日日曜日

Directory.CreateDirectory()を実行する前にディレクトリが作成可能かを確認する

ここ数日悩んでいた件です。

C#にはDirectory.CreateDirectory()というメソッドがあり、指定したパスにディレクトリを作ることができます。しかし、当然どんなところにでも作れるわけではなく、例えば存在しないドライブに書き込もうとした、管理者権限じゃないと書き込めないフォルダだった("C:\Program Files"など)、書き込み権限の無いフォルダだったなどの理由で、このメソッドは実行時に失敗する(例外を吐く)可能性があります。

そこで、実際にディレクトリを作るわけではなく、事前にディレクトリを作ることができるのかを確認する方法を模索していました。

 

結論を言います。

良いから「ディレクトリを1回作ってみて、作れたらそのディレクトリを削除する」という方法で片付けろ。

 

いろいろ調べていたのですが、これ以上良い方法が見つかりませんでした(逆に何かいい方法知っている人いたら教えてください…)。

フォルダの書き込み権限を確認する

一応、C#でもフォルダの書き込み権限を確認することはできます。ただし、Windows依存のコードになります。

var access = new DirectoryInfo(path).GetAccessControl();
var rules = access.GetAccessRules(true, true, typeof(NTAccount));

ただし、このGetAccessControl()はFileSystemAclExtensions.GetAccessControl()拡張メソッドで、Nuget経由でパッケージをインストールする必要があります。

このGetAccessRules()メソッドが返す値が、いわゆるフォルダを右クリック→プロパティ→セキュリティ→詳細設定で出てくるフォルダの読み書き権限に相当するものとなります。

今実行中のアプリケーションとして書き込めるかどうかを知りたいだけなのに、ファイルシステムの権限情報が全部出てくるのです。今の自分はこのどれに相当するのかを見分けなければなりません。暗礁に乗り上げてきた気がしてきました。

実行中のアプリケーションのプリンシパルを取得する

実行中のアプリケーションのプリンシパルを取得しようとしたら、WindowsIdentity.Current()を呼ぶことになります。

そのメソッドが返したWindowsIdentityインスタンスのNameプロパティを見ると現在実行中のユーザー名、Groupsプロパティを見ると自身が属するグループ名が出てきます。これらを前項のプリンシパルと突き合わせれば良いのかと思いきや、どうもそれは完全ではないようです。例えば、ネットワークドライブ上のフォルダはそれでうまくいきませんでした。

 

そもそも、OS上では充分に抽象化された「ディレクトリ」いう概念でプログラムを書いていたのに、一気に具体的なファイルシステムがどうとか、Windowsのアクセスコントロール機能がどうとか、そういうレイヤーのプログラムを書くことを強いられてしまいます。世の中には多種多様な記録媒体やファイルシステムがあり、それらすべてについて正確に書き込みができるかどうかを判断するコードをこのレベルで書くのはそう簡単にできるものではありません。

ですので、最初に書いた通り、ひとまずディレクトリを作ってみて確かめるのがやはり一番いい方法という結論に私は達しました。 

ディレクトリを作って削除するのも、実際のファイルシステムを汚している感があったり、後のDeleteで誤ったフォルダを削除してしまわないように慎重にコードを書く必要があったり、そもそも例外発生前提でプログラムを書く必要があるのでそれなりに神経を使う必要があったりと、こちらも何かと苦労があります。ですから、書き込み権限を事前に確認して終わらせようという発想になっていたはずです。

はあ、こういうの、標準でDirectory.CanCreateDirectory()みたいなメソッドが用意されてワンタッチに判定してもらえるようになったりしないのかなあ。しないんだろうなあ。抽象化(=様々なOS/ファイルシステムで正常に動作するように)するのが難しいから用意されていないんだろうなあ…。

何かいい方法ご存知の方いれば教えてください。