The Man Who Fell From The Wrong Side Of The Sky:2019年6月分

2019/6/1(Sat)

[Windows] PowerShellで生活するために - New-TemporaryFileコマンドレット編(その5)

前回の続きを書こうと思ったんだが、気づいたことがあったので予定変更。

@New-TemporaryFileそしてCreateTempFileNameの作るファイルのアクセス権もガバガバだった件

タイトルなのに初回以降まったくその存在にすら触れていなかったNew-TemporaryFileコマンドレットだけど、こいつで作成する一時ファイルも%USERPROFILE%\AppData\Local\Temp\なんかの親ディレクトリの権限を継承してることが前提で、本質的に安全でないようやね…Windows APIのGetTempFileNameからしてアウトなんやなこいつ。

なのでNew-TemporaryFileコマンドレットの安全な実装も作りますかね…アーメンド。

@CreateFileをP/Invokeする

前回のCreateDirectoryと同様にCreateFileもP/Invokeするだけで基本的な考えは一緒なんだけど、CreateFileの戻り値がインド人なことに要注意。

using System.Runtime.InteropServices;

	[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
	internal static extern IntPtr CreateFile(
		String lpFileName,
		uint dwDesiredAccess,
		uint dwShareMode,
		ref SECURITY_ATTRIBUTES lpSecurityAttributess,
		uint dwCreationDisposition,
		uint dwFlagsAndAttributes,
		IntPtr hTemplateFile
	);
	...
	for (;;) {
		IntPtr fh = CreateFile(path,
			0x80000000|0x40000000, /* GENERIC_READ|GENERIC_WRITE */
			0,
			ref sa,
			1, /* CREATE_NEW */
			/* FILE_ATTRIBUTE_NORMAL|FILE_ATTRIBUTE_TEMPORARY|FILE_ATTRIBUTE_NOT_CONTENT_INDEXED */
			0x00000080|0x00000100|0x00002000,
			IntPtr.Zero
		)) {
		if (fh.ToInt64() != -1 /*INVALID_HANDLE_VALUE*/)
			return path;
		if (Marshal.GetLastWin32Error() != 80/*ERROR_FILE_EXISTS*/)
			break;
	}

密林の奥に住むStackOverflow民やQiita民は上記のようにHANDLEをIntPtrと定義してるケースが多いみたい、ところでCreateFileした後にちゃんとCloseHandleしてますか…(小声)?

ハンドルは開いたら閉じなければって見慣れたHANDLE型なら覚えてるだろうけど、IntPtrで定義しちゃうと元がHANDLEってこと忘れちゃう罠。 それにCloseHandleの定義も必要になるのでちょっとめんどくさい。

そんで楽をしたい人に耳寄りな情報、HANDLE型をラップする SafeHandleクラス、そしてファイルハンドルであればその継承クラスの SafeFileHandleクラスというのが標準で用意されているのだ。

こいつは IDisposableインタフェースを実装しているので usingステートメント内のブロックを抜けたらただちにDispose()を呼んで自動で解放をしてくれるし、ガベコレ対象になった時にも以下同文。そんで内部ではCloseHandleを呼んでるのでこれは安心ですね。

ちなみにSECURITY_ATTRIBUTES構造体も同様にメンバのSECURITY_DESCRIPTORのアンマネージドなメモリの解放漏れ防ぎてーなーと思ってIDisposable実装も考えたんだが、C#の構造体はインタフェースを実装できないのだ。ヘルパー書くのもかっこ悪いのでtry~finallyでお茶を濁したが、なんかいい方法ないですかね。

ということでざっと書き直し。

using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

	[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
	internal static extern SafeFileHandle CreateFile(
		String lpFileName,
		uint dwDesiredAccess,
		uint dwShareMode,
		ref SECURITY_ATTRIBUTES lpSecurityAttributess,
		uint dwCreationDisposition,
		uint dwFlagsAndAttributes,
		SafeFileHandle hTemplateFile
	);
	...
	SafeFileHandle HANDLE_NULL = new SafeFileHandle(IntPtr.Zero, false);
	for (;;) {
		using (SafeFileHandle fh = CreateFile(path,
			0x80000000|0x40000000, /* GENERIC_READ|GENERIC_WRITE *.
			0,
			ref sa,
			1, /* CREATE_NEW */
			/* FILE_ATTRIBUTE_NORMAL|FILE_ATTRIBUTE_TEMPORARY|FILE_ATTRIBUTE_NOT_CONTENT_INDEXED */
			0x00000080|0x00000100|0x00002000,
			HANDLE_NULL
		)) {
			if (!fh.IsInvalid)
				return path;
			if (Marshal.GetLastWin32Error() != 80/*ERROR_FILE_EXISTS*/)
				break;
		}
	}

さらに

現状のコードの全体はこんな感じ。

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using Microsoft.Win32.SafeHandles;

namespace unko
{
	class Unko
	{
		internal const int RETRY_MAX = 100;
		internal enum Errors : int
		{
			ERROR_FILE_EXISTS = 80,
			ERROR_ALREADY_EXISTS = 183
		}
		internal enum DesiredAccess : uint
		{
			GENERIC_ALL     = 0x10000000,
			GENERIC_EXECUTE = 0x20000000,
			GENERIC_WRITE   = 0x40000000,
			GENERIC_READ    = 0x80000000
		}
		internal enum ShareMode : uint
		{
			FILE_SHARE_READ   = 0x00000001,
			FILE_SHARE_WRITE  = 0x00000002,
			FILE_SHARE_DELETE = 0x00000004
		}
		internal enum CreationDisposition : uint
		{
			CREATE_NEW        = 1,
			CREATE_ALWAYS     = 2,
			OPEN_EXISTING     = 3,
			OPEN_ALWAYS       = 4,
			TRUNCATE_EXISTING = 5
		}
		internal enum FlagsAndAttributes : uint
		{
			FILE_ATTRIBUTE_READONLY            = 0x00000001,
			FILE_ATTRIBUTE_HIDDEN              = 0x00000002,
			FILE_ATTRIBUTE_SYSTEM              = 0x00000004,
			FILE_ATTRIBUTE_DIRECTORY           = 0x00000010,
			FILE_ATTRIBUTE_ARCHIVE             = 0x00000020,
			FILE_ATTRIBUTE_DEVICE              = 0x00000040,
			FILE_ATTRIBUTE_NORMAL              = 0x00000080,
			FILE_ATTRIBUTE_TEMPORARY           = 0x00000100,
			FILE_ATTRIBUTE_SPARSE_FILE         = 0x00000200,
			FILE_ATTRIBUTE_REPARSE_POINT       = 0x00000400,
			FILE_ATTRIBUTE_COMPRESSED          = 0x00000800,
			FILE_ATTRIBUTE_OFFLINE             = 0x00001000,
			FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000,
			FILE_ATTRIBUTE_ENCRYPTED           = 0x00004000,
			FILE_ATTRIBUTE_VIRTUAL             = 0x00010000,
			FILE_ATTRIBUTE_VALID_FLAGS         = 0x00007fb7,
			FILE_ATTRIBUTE_VALID_SET_FLAGS     = 0x000031a7
		}
		[StructLayout(LayoutKind.Sequential)]
		internal struct SECURITY_ATTRIBUTES
		{
			internal uint nLength;
			internal IntPtr lpSecurityDescriptor;
			internal bool bInheritHandle;
		}
		[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
		internal static extern SafeFileHandle CreateFile(
			String lpFileName,
			DesiredAccess dwDesiredAccess,
			ShareMode dwShareMode,
			ref SECURITY_ATTRIBUTES lpSecurityAttributess,
			CreationDisposition dwCreationDisposition,
			FlagsAndAttributes dwFlagsAndAttributes,
			SafeFileHandle hTemplateFile
		);
		[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
		internal static extern bool CreateDirectory(
			String lpPathName,
			ref SECURITY_ATTRIBUTES lpSecurityAttributes
		);
		private static readonly SafeFileHandle HANDLE_NULL = new SafeFileHandle(IntPtr.Zero, false);
		private static readonly FileSecurity fs = new FileSecurity();
		private static readonly DirectorySecurity ds = new DirectorySecurity();
		static Unko()
		{
			fs.AddAccessRule(new FileSystemAccessRule(
				WindowsIdentity.GetCurrent().Name,
				FileSystemRights.FullControl,
				InheritanceFlags.None,
				PropagationFlags.NoPropagateInherit,
				AccessControlType.Allow
			));
			ds.AddAccessRule(new FileSystemAccessRule(
				WindowsIdentity.GetCurrent().Name,
				FileSystemRights.FullControl,
				InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
				PropagationFlags.NoPropagateInherit,
				AccessControlType.Allow
			));
		}
		static String NewSafeTemporaryFile()
		{
			SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
			sa.nLength = (uint)Marshal.SizeOf(sa);
			sa.bInheritHandle = false;
			byte[] sdb = fs.GetSecurityDescriptorBinaryForm();
			int sdn = sdb.Length;
			sa.lpSecurityDescriptor = Marshal.AllocHGlobal(sdn);
			try {
				Marshal.Copy(sdb, 0, sa.lpSecurityDescriptor, sdn);
				String tmpdir = Path.GetTempPath();
				for (int i = 0; i < RETRY_MAX; ++i) {
					String path = Path.Combine(tmpdir, Path.GetRandomFileName());
					using (SafeFileHandle handle = CreateFile(path,
						DesiredAccess.GENERIC_READ|DesiredAccess.GENERIC_WRITE,
						0,
						ref sa,
						CreationDisposition.CREATE_NEW,
						FlagsAndAttributes.FILE_ATTRIBUTE_NORMAL|
						FlagsAndAttributes.FILE_ATTRIBUTE_TEMPORARY|
						FlagsAndAttributes.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
						HANDLE_NULL
					)) {
						if (!handle.IsInvalid)
							return path;
						if (Marshal.GetLastWin32Error() != (int)Errors.ERROR_FILE_EXISTS)
							break;
					}
				}
			} finally {
				Marshal.FreeHGlobal(sa.lpSecurityDescriptor);
			}
			throw new IOException("something wrong.");
		}
		static String NewSafeTemporaryDirectory() 
		{
			SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
			sa.nLength = (uint)Marshal.SizeOf(sa);
			sa.bInheritHandle = false;
			byte[] sdb = ds.GetSecurityDescriptorBinaryForm();
			int sdn = sdb.Length;
			sa.lpSecurityDescriptor = Marshal.AllocHGlobal(sdn);
			try {
				Marshal.Copy(sdb, 0, sa.lpSecurityDescriptor, sdn);
				String tmpdir = Path.GetTempPath();
				for (int i = 0; i < RETRY_MAX; ++i) {
					String path = Path.Combine(tmpdir, Guid.NewGuid().ToString("B"));
					if (CreateDirectory(path, ref sa))
						return path;
					if (Marshal.GetLastWin32Error() != (int)Errors.ERROR_ALREADY_EXISTS)
						break;
				}
			} finally {
				Marshal.FreeHGlobal(sa.lpSecurityDescriptor);
			}
			throw new IOException("something wrong.");
		}
		static void Main(String[] args)
		{
			Console.WriteLine(NewSafeTemporaryFile());
			Console.WriteLine(NewSafeTemporaryDirectory());
		}
	}
}

@次回

前回の予告どおりにコマンドレット化を試みる。

2019/6/2(Sun)

[Windows] PowerShellで生活するために - New-TemporaryFileコマンドレット編(その6)

前回の続き。

@コードをリファクタリングしてコマンドレット化する

古いコードを書き直すのはいい事なのだが、リファクタリングだああああと叫びながら古い家にガソリン撒いて焼き払った後にバイク小屋ひとつ建てられない事案が多過ぎるのだよな、そもそもファクタリング=債権買取だからリファクタリングってのは再び借金抱えるそういう意味なんじゃねえの(適当)。

ということで前回書いたコードをコマンドレットに書き直す、そもそもコマンドレットのletってなんやねん、Javaのアプレットとかサーブレットでも思ったのだが、いま調べたらカツレツ(小さく切り分けた)と同じで「小さい」を意味する接尾語らしい、ほんとどうでもいいですね…これから嫌な奴に出くわしたら器レットとか度量レット、チンコレットとでも呼ぶことにしよう(ぉ

@コマンドレットを実装するには

詳しい事は ドキュメント参照なんだがいくつかかいつまんで。

コマンドレットとなるクラスは必ず

のいずれかを継承する必要がある 、ドキュメントには読むと後者を継承することでMSH Runtimeとやらにアクセスきるとあるけど、MSHが何かの説明が何もないのでさっぱりである。おそらくMSHとはMonad(PowerShellの開発コード) ShellであってPowerShell自身を指してるっぽい、ドキュメントがまともに更新できてないってやつだ。

ドキュメント役に立たんときはオープンソースなんだしコードを読めなのだが、どうやらコマンドレットの実装内部からRunspaceと呼ばれている分散処理や非同期処理、ジョブ管理そしてワークフローの機能が触れるのが後者の模様、ただしその代償として直接クラスをインスタンス化できなくなる。

そもそも今回の場合、必要な情報は環境変数や関数経由でとれるものだし、操作する対象もファイルシステムでそもそもトランザクション保護無いから *1分割操作にならんようアトミックに腐心してるわけなので、Cmdletを継承すれば充分ちうことですわ。

にもかかわらず機能使わんのにPSCmdlet継承してるコードが多いのねー貧乏性だと「継承するだけならタダなんだしなんか高機能そうなPSCmdletを継承しとけ」となるとこだけど、逆にインスタンス化できない事でユニットテスト書く時に困るやつ。贅肉あった方が飢えには強いといいつつ飢饉ならどっちみち水無くて死ぬので、成人病リスクで考えたら痩せろってやつですね(ぉ

また継承しただけではダメでコマンドレット名をCmdletアトリビュートで指定する必要もある、動詞ではじめることが推奨されかつ VerbsCommonクラスに定義されてるものを使うのが基本、使える動詞の一覧はGet-Verbコマンドレットで取得できる。

Get-Verb
Verb        Group
----        -----
Add         Common
...
New         Common
...

ジャパニーズSIerが「Regist-*」コマンドレット作ろうとしてもそんな動詞存在しないので追加しないとダメなヤツだ!

他にもいろいろパラメーターはあるけど説明は省略、あとは ドキュメント読んで。

それにしても設計的にCmdletってinterfaceであるべきだと思うんだけど、一番根っこがabstract classってバッドデザインのテンプレやのう。 オーバーライドすべき関数にあらかじめデフォルト実装がご用意されてるので、逆にどれをオーバーライドすべきなのかドキュメント読まないと判らないヤツなんやな。

そんで基本的にオーバーライドするか考える必要のあるメソッドは以下の3つ(他にもあるけどまず必要ない)。

  • BeginProcessing() … 初期処理、一度だけ呼ばれる
  • ProcessRecord() … 処理、パイプで渡されたオブジェクトのレコード数だけ呼ばれる
  • EndProcessing() … 終了処理、一度だけ呼ばれる

パイプからデータ読んで逐次処理するならProcessRecord()をオーバーライドだけど、今回はパイプ無関係なのでEndProcessing()だけオーバーライドする。

戻り値の型はOutputTypeアトリビュートで指定、実際にどうやって返すかというとreturnステートメントではなく、Cmdlet::WriteOutput(Object)という関数を使ってパイプに書き込んでやる必要がある。

@コード完成

ちゅーことで、とりあえず雑なコードだけどこれで一旦は完成という事で。

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Management.Automation;
using Microsoft.Win32.SafeHandles;

namespace unko
{
	public static class NewTemporaryObject
	{
		internal const int RETRY_MAX = 100;
		internal enum Errors : int
		{
			ERROR_FILE_EXISTS    = 80,
			ERROR_ALREADY_EXISTS = 183
		}
		internal enum DesiredAccess : uint
		{
			GENERIC_ALL     = 0x10000000,
			GENERIC_EXECUTE = 0x20000000,
			GENERIC_WRITE   = 0x40000000,
			GENERIC_READ    = 0x80000000
		}
		internal enum ShareMode : uint
		{
			FILE_SHARE_READ   = 0x00000001,
			FILE_SHARE_WRITE  = 0x00000002,
			FILE_SHARE_DELETE = 0x00000004
		}
		internal enum CreationDisposition : uint
		{
			CREATE_NEW        = 1,
			CREATE_ALWAYS     = 2,
			OPEN_EXISTING     = 3,
			OPEN_ALWAYS       = 4,
			TRUNCATE_EXISTING = 5
		}
		internal enum FlagsAndAttributes : uint
		{
			FILE_ATTRIBUTE_READONLY            = 0x00000001,
			FILE_ATTRIBUTE_HIDDEN              = 0x00000002,
			FILE_ATTRIBUTE_SYSTEM              = 0x00000004,
			FILE_ATTRIBUTE_DIRECTORY           = 0x00000010,
			FILE_ATTRIBUTE_ARCHIVE             = 0x00000020,
			FILE_ATTRIBUTE_DEVICE              = 0x00000040,
			FILE_ATTRIBUTE_NORMAL              = 0x00000080,
			FILE_ATTRIBUTE_TEMPORARY           = 0x00000100,
			FILE_ATTRIBUTE_SPARSE_FILE         = 0x00000200,
			FILE_ATTRIBUTE_REPARSE_POINT       = 0x00000400,
			FILE_ATTRIBUTE_COMPRESSED          = 0x00000800,
			FILE_ATTRIBUTE_OFFLINE             = 0x00001000,
			FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000,
			FILE_ATTRIBUTE_ENCRYPTED           = 0x00004000,
			FILE_ATTRIBUTE_VIRTUAL             = 0x00010000,
			FILE_ATTRIBUTE_VALID_FLAGS         = 0x00007fb7,
			FILE_ATTRIBUTE_VALID_SET_FLAGS     = 0x000031a7
		};
		[StructLayout(LayoutKind.Sequential)]
		internal struct SECURITY_ATTRIBUTES
		{
			internal uint nLength;
			internal IntPtr lpSecurityDescriptor;
			internal bool bInheritHandle;
		}
		[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
		internal static extern SafeFileHandle CreateFile(
			String lpFileName,
			DesiredAccess dwDesiredAccess,
			ShareMode dwShareMode,
			ref SECURITY_ATTRIBUTES lpSecurityAttributess,
			CreationDisposition dwCreationDisposition,
			FlagsAndAttributes dwFlagsAndAttributes,
			SafeFileHandle hTemplateFile
		);
		[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
		internal static extern bool CreateDirectory(
			String lpPathName,
			ref SECURITY_ATTRIBUTES lpSecurityAttributes
		);
		private static readonly SafeFileHandle HANDLE_NULL = new SafeFileHandle(IntPtr.Zero, false);
		private static readonly FileSecurity fs = new FileSecurity();
		private static readonly DirectorySecurity ds = new DirectorySecurity();
		static NewTemporaryObject()
		{
			fs.AddAccessRule(new FileSystemAccessRule(
				WindowsIdentity.GetCurrent().Name,
				FileSystemRights.FullControl,
				InheritanceFlags.None,
				PropagationFlags.NoPropagateInherit,
				AccessControlType.Allow
			));
			ds.AddAccessRule(new FileSystemAccessRule(
				WindowsIdentity.GetCurrent().Name,
				FileSystemRights.FullControl,
				InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
				PropagationFlags.NoPropagateInherit,
				AccessControlType.Allow
			));
		}
		public static FileInfo NewSafeTemporaryFile()
		{
			return NewSafeTemporaryFile(Path.GetTempPath());
		}
		public static FileInfo NewSafeTemporaryFile(String tmpdir)
		{
			SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
			sa.nLength = (uint)Marshal.SizeOf(sa);
			sa.bInheritHandle = false;
			byte[] sdb = fs.GetSecurityDescriptorBinaryForm();
			int sdn = sdb.Length;
			sa.lpSecurityDescriptor = Marshal.AllocHGlobal(sdn);
			try {
				Marshal.Copy(sdb, 0, sa.lpSecurityDescriptor, sdn);
				for (int i = 0; i < RETRY_MAX; ++i) {
					String path = Path.Combine(tmpdir, Path.GetRandomFileName());
					using (SafeFileHandle handle = CreateFile(path,
						DesiredAccess.GENERIC_READ|DesiredAccess.GENERIC_WRITE,
						0,
						ref sa,
						CreationDisposition.CREATE_NEW,
						FlagsAndAttributes.FILE_ATTRIBUTE_NORMAL|
						FlagsAndAttributes.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
						HANDLE_NULL
					)) {
						if (!handle.IsInvalid)
							return new FileInfo(path);
						if (Marshal.GetLastWin32Error() != (int)Errors.ERROR_FILE_EXISTS)
							break;
					}
				}
			} finally {
				Marshal.FreeHGlobal(sa.lpSecurityDescriptor);
			}
			throw new IOException();
		}
		public static DirectoryInfo NewSafeTemporaryDirectory()
		{
			return NewSafeTemporaryDirectory(Path.GetTempPath());
		}
		public static DirectoryInfo NewSafeTemporaryDirectory(String tmpdir)
		{
			SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
			sa.nLength = (uint)Marshal.SizeOf(sa);
			sa.bInheritHandle = false;
			byte[] sdb = ds.GetSecurityDescriptorBinaryForm();
			int sdn = sdb.Length;
			sa.lpSecurityDescriptor = Marshal.AllocHGlobal(sdn);
			try {
				Marshal.Copy(sdb, 0, sa.lpSecurityDescriptor, sdn);
				for (int i = 0; i < RETRY_MAX; ++i) {
					String path = Path.Combine(tmpdir, Guid.NewGuid().ToString("B"));
					if (CreateDirectory(path, ref sa))
						return new DirectoryInfo(path);
					if (Marshal.GetLastWin32Error() != (int)Errors.ERROR_ALREADY_EXISTS)
						break;
				}
			} finally {
				Marshal.FreeHGlobal(sa.lpSecurityDescriptor);
			}
			throw new IOException();
		}
	}
	[Cmdlet(VerbsCommon.New, "SafeTemporaryFile", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low)]
	[OutputType(typeof(System.IO.FileInfo))]
	public class NewSafeTemporaryFileCommand : Cmdlet
	{
		[Parameter(Position = 0)]
		[ValidateNotNullOrEmpty]
		public String LiteralPath { get; set; } = Path.GetTempPath();
		protected override void EndProcessing()
		{
			String tmpdir = LiteralPath;
			if (ShouldProcess(tmpdir)) {
				try {
					WriteObject(NewTemporaryObject.NewSafeTemporaryFile(tmpdir));
				} catch (IOException e) {
					ThrowTerminatingError(new ErrorRecord(
						e, "NewSafeTemporaryFileWriteError",
						ErrorCategory.WriteError, tmpdir
					));
				}
			}
		}
	}
	[Cmdlet(VerbsCommon.New, "SafeTemporaryDirectory", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low)]
	[OutputType(typeof(System.IO.DirectoryInfo))]
	public class NewSafeTemporaryDirectoryCommand : Cmdlet
	{
		[Parameter(Position = 0)]
		[ValidateNotNullOrEmpty]
		public String LiteralPath { get; set; } = Path.GetTempPath();
		protected override void EndProcessing()
		{
			String tmpdir = LiteralPath;
			if (ShouldProcess(tmpdir)) {
				try {
					WriteObject(NewTemporaryObject.NewSafeTemporaryDirectory(tmpdir));
				} catch (IOException e) {
					ThrowTerminatingError(new ErrorRecord(
						e, "NewSafeTemporaryDirectoryWriteError",
						ErrorCategory.WriteError, tmpdir
					));
				}
			}
		}
	}
}

@Import-ModuleでDLL(アセンブリ)を読み込んだ場合

このコードをcsc.exeなりVisual StudioなりでビルドしDLL(アセンブリ)を作成したら、あとはImport-Moduleコマンドレット読み込むだけで使えるようになる。

Import-Module 'unko.dll' -Verbose
VERBOSE: Importing cmdlet 'New-SafeTemporaryFile'.
VERBOSE: Importing cmdlet 'New-SafeTemporaryDirectory'.

これでようやく安全な一時ファイルとディレクトリを作成するためのオレオレコマンドレット、New-SafeTemporaryFileとNew-SafeTemporaryDirectoryが使えるようになったよ…

New-SafeTemporaryFile | Get-Acl
New-SafeTemporaryDirectory | Get-Acl

    Directory: C:\Users\tnozaki\AppData\Local\Temp

Path                                   Owner         Access                          
----                                   -----         ------                          
c2ddxpwq.i5b                           X200S\tnozaki X200S\tnozaki Allow  FullControl
{5793d2b7-02ce-47b6-b21c-a38e5e3d7cc0} X200S\tnozaki X200S\tnozaki Allow  FullControl

元のNew-TemporaryFileにはない機能として-LiteralPathオプションで作成先の一時ディレクトリを変更可能とした。これはファイルのアトミックな更新をしたい時に一時ファイル作って書き込みが終わってからリネームするなんてよくやる基本テクだけど、一時ファイルが別ファイルシステム上に存在する場合はリネームは失敗するかコピー扱いなので期待通りの動作にならない、そんな時に有用なオプションのはず。

@Add-TypeでDLL(アセンブリ)を読み込んだ場合

ところが過去回で説明したAdd-Typeコマンドレットを使ってロードした場合が問題なのだ。

Add-Type -Path 'unko.dll'

あるいは-TypeDefinitionをつかってPowerShell内にC#コードを埋め込んで

Add-Type -TypeDefinition @'
	...
'@

このケースだとアセンブリ中のクラスと関数は可視なんだけども

[unko.NewTemporaryObject]::NewSafeTemporaryFile() | Get-Acl
[unko.NewTemporaryObject]::NewSafeTemporaryDirectory() | Get-Acl

    Directory: C:\Users\tnozaki\AppData\Local\Temp

Path                                   Owner         Access                          
----                                   -----         ------                          
zjmqedyr.u25                           X200S\tnozaki X200S\tnozaki Allow  FullControl
{ceb01460-d7c1-4967-a78d-3a97f13fac66} X200S\tnozaki X200S\tnozaki Allow  FullControl

でもコマンドレットは不可視なのよね。

New-SafeTemporaryDirectory

New-SafeTemporaryDirectory : The term 'New-SafeTemporaryDirectory' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spell
ing of the name, or if a path was included, verify that the path is correct and try again.
At line:3 char:1
+ New-SafeTemporaryDirectory
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (New-SafeTemporaryDirectory:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

これはAdd-Typeした後に改めてImport-Moduleすればいいようなのだけど、その場合は-Nameオプションでモジュール名を指定する必要があって、そのためには Module Manifestというファイルを書かなければならないようだ、うへーめんどくせえ何の為のアトリビュートとかnamespaceなんだか…ばっかじゃねぇの。

@次回

あーめんどくせ、Module Manifestの書き方を学んでいいかげんこの編は最終回にしたい。

*1:トランザクションNTFSはもう死んだので…

2019/6/3(Mon)

[Windows] PowerShellで生活するために - Move-Itemコマンドレット編(その1)

前回の続き、別の問題に気づいたのでちょっとだけ脱線するよ。

@Move-Itemや[System.IO.File]::Move()を使ってのファイル移動はアトミックではない

こいつら使ってファイルを移動する時

  • 単なるリネームで済む場合(これはアトミック)
  • ファイルシステムを跨いだ場合など、ファイルコピーを伴う場合(もちろん非アトミック)

でエラーにするか否か挙動を変更できないのねこれ、Move-Itemコマンドレットには最初から期待してなかったけど.Net Frameworkもダメなんやな。

 9 internal partial class Interop
10 {
11     internal partial class Kernel32
12     {
13         const uint MOVEFILE_REPLACE_EXISTING = 0x01;
14         const uint MOVEFILE_COPY_ALLOWED = 0x02;
           ...
19         [DllImport(Libraries.Kernel32, EntryPoint = "MoveFileExW", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
20         private static extern bool MoveFileExPrivate(string src, string dst, uint flags);
           ...
29         internal static bool MoveFile(string src, string dst, bool overwrite)
30         {
31             src = PathInternal.EnsureExtendedPrefixIfNeeded(src);
32             dst = PathInternal.EnsureExtendedPrefixIfNeeded(dst);
33
34             uint flags = MOVEFILE_COPY_ALLOWED;
35             if (overwrite)
36             {
37                 flags |= MOVEFILE_REPLACE_EXISTING;
38             }
39
40             return MoveFileExPrivate(src, dst, flags);
41         }
42     }
43 }

ちなみにWindows APIのMoveFileEx()ならフラグにMOVEFILE_COPY_ALLOWEDを渡さない限りはエラーになってくれるんだが、[System.IO.File]::Move()のコードを追っていくと、上記のコードの通り常時おったててるしまっとる。

これまでUNIXでよく訓練されたプログラマなら、そもそも「移動」ではなく「名前変更」つまりRename-Itemコマンドレットを使うケースなのでは…と考えるとこでしょう、ワイもそう思いました。

しかしだな、Rename-Itemは-ForceオプションをつけようがMOVEFILE_REPLACE_EXISTINGは建たないようでファイルの置換ができないという別の問題があるのですわ…

New-Item -ItemType File 'unko1'|Out-Null
Copy-Item 'unko1' 'unko2'
Rename-Item 'unko2' 'unko1'

Rename-Item : 既に存在するファイルを作成することはできません。
At line:3 char:1
+ Rename-Item 'unko2' 'unko1'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : WriteError: (C:\Users\tnozaki\unko2:String) [Rename-Item], IOException
    + FullyQualifiedErrorId : RenameItemIOError,Microsoft.PowerShell.Commands.RenameItemCommand

うーんこの。

まぁNew-Itemなんかも既にファイルが存在する場合は上書きせずエラーになったりするし、見かけ倒しの安全性にふってるんだろうな、そのかわりまともなセキュアコーディングができねえとも考えずにな…

これどんな時に困るかちゅーと

  • ファイルロックによる排他を行わずにファイルの内容更新したい
  • でも更新中のファイル内容を他には絶対に見せたくない

なんて時に

  • 一時ファイル作って元ファイルの内容をコピー
  • 一時ファイル側を更新
  • 一時ファイルを元ファイルに上書きリネーム

するというテクニックが広く使われているのだけど *1、移動にコピーが必要となる場合(別ドライブを跨ぐ時など)には失敗してくれんと、コピー途中の中途半端な状態のファイルが他に見えちゃうのだよな。

まーこれ一時ファイルと元ファイルが同じファイルシステムにあることをチェックすれば事足りるし、UNIXでもシェルスクリプトからじゃrename(2)使えないからmv(1)でお茶濁すのでPowerShellと事情は一緒だから、新規にコマンドレット書くほどの問題かというのは微妙ではある。

*1:例として挙げるならpasswd(1)のコードが最も有名だろう、 過去回でTOCTTOUとシンボリックリンク攻撃を説明の時にとりあげてるのでそっち参照のこと。

2019/6/5(Wed)

[Windows] MoveFileEx vs ReplaceFile

うーん ReplaceFileって本当にAtomicかすげー怪しいと思ってたんですが、なんで自分は手堅く MoveFileEx使ってますかね…

これReplaceFileはAtomicだよ派のソースを調べると このPDF

Under UNIX, rename() is guaranteed to
atomically overwrite the old version of the file. Under
Windows, the ReplaceFile() call is used to atomically
replace one file with another. 

の一文、そしてReFS導入も影響しての TxF廃止に伴う代替案にReplaceFileが挙げられてることあたりか、じゃあ大丈夫なのかねぇでもどうやって実現しとるのやら。

ちなみにJava7で導入された java.nio.file.Path::move()とATOMIC_MOVEフラグの実装、Windowsの場合は sun.nio.fs.WindowsFileCopyで、Windows APIのMoveFileExを使っている。

266     static void move(WindowsPath source, WindowsPath target, CopyOption... options)
267         throws IOException
268     {
269         // map options
270         boolean atomicMove = false;
271         boolean replaceExisting = false;
272         for (CopyOption option: options) {
273             if (option == StandardCopyOption.ATOMIC_MOVE) {
274                 atomicMove = true;
275                 continue;
276             }
...
298         // atomic case
299         if (atomicMove) {
300             try {
301                 MoveFileEx(sourcePath, targetPath, MOVEFILE_REPLACE_EXISTING);
302             } catch (WindowsException x) {
303                 if (x.lastError() == ERROR_NOT_SAME_DEVICE) {
304                     throw new AtomicMoveNotSupportedException(
305                         source.getPathForExceptionMessage(),
306                         target.getPathForExceptionMessage(),
307                         x.errorString());
308                 }
309                 x.rethrowAsIOException(source, target);
310             }
311             return;
312         }

これはGolangのos.Renameも一緒、該当コードは src/internal/syscall/windows/syscall_windows.goにある。

134 func Rename(oldpath, newpath string) error {
135         from, err := syscall.UTF16PtrFromString(oldpath)
136         if err != nil {
137                 return err
138         }
139         to, err := syscall.UTF16PtrFromString(newpath)
140         if err != nil {
141                 return err
142         }
143         return MoveFileEx(from, to, MOVEFILE_REPLACE_EXISTING)
144 }

そしてPythonの os.rename()の実装である POSIX moduleもやっぱりMoveFileEx()を使ってる。

4043 static PyObject *
4044 internal_rename(path_t *src, path_t *dst, int src_dir_fd, int dst_dir_fd, int is_replace)
4045 {
...
4049 #ifdef MS_WINDOWS
4050     BOOL result;
4051     int flags = is_replace ? MOVEFILE_REPLACE_EXISTING : 0;
...
4065 #ifdef MS_WINDOWS
4066     Py_BEGIN_ALLOW_THREADS
4067     result = MoveFileExW(src->wide, dst->wide, flags);
4068     Py_END_ALLOW_THREADS
...

あとlibuvもファイルシステム系システムコールラッパー( src/win/fs.c)ではMoveFileEx使ってる。

1314 static void fs__rename(uv_fs_t* req) {
...
1375     if (MoveFileExW(src, dst, MOVEFILE_REPLACE_EXISTING) != 0) {
1376       SET_REQ_RESULT(req, 0);
1377       return;
1378     }
...

そしてCygwinのrename(2) syscall emulationはもっとパラノイアックにKERNEL32でなくNTDLL叩いてますな。

2241 static int
2242 rename2 (const char *oldpath, const char *newpath, unsigned int at2flags)
2243 {
...
2713       if (use_posix_semantics)
2714         pfri->Flags = noreplace ? 0
2715                                 : (FILE_RENAME_REPLACE_IF_EXISTS
2716                                    | FILE_RENAME_POSIX_SEMANTICS
2717                                    | FILE_RENAME_IGNORE_READONLY_ATTRIBUTE);
...
2728       status = NtSetInformationFile (fh, &io, pfri,
2729                                      sizeof *pfri + pfri->FileNameLength,
2730                                      use_posix_semantics
2731                                      ? FileRenameInformationEx
2732                                      : FileRenameInformation);

ちゃんとWindows10で導入されたFILE_RENAME_POSIX_SEMANTICSとFileRenameInformationEx使ってるのさすがなんやな。

さて本日のオチ担当は

これMOVEFILE_COPY_ALLOWED指定してるからAtomicちゃうよね…直すべきだと思うんだけど多分直すと世界中で動かなくなるコードが大量発生するパターンだろうか。

そもそもgit annotateからコミットメッセージ読んだら「ドライブ跨いだリネーム」云々書いてあるのでそもそもアトミックなんぞ一切保障しない POSIX renameとは何の関係も無いAPIだといわれりゃそれまでではある。

というかPOSIX.1-2017出てたのね、2008の改定だからそう大きく変更無いと思うけどここ何年もまったく動向追ってねえからさっぱりわからん…

2019/6/9(Sun)

[NなんとかBSD] /etc/rc.d/ntpdateの実装とIPv6

タイトルでネットワークプログラミングの話かと鼻息荒くなった皆さん、ここではありません。 前もって言っておくがとてつもなくしょーもない話なので、netinet*の下の話が読みたい奴はブラウザそっ閉じして他のブログ読みに行ってどうぞ。

今年のIPv6普及元年は2001年以来の豊作とか揶揄され続けてはや幾年、スマホの普及により令和元年という時代にIPv6が使えない環境なぞクソクソアンクソ認定していい頃やろ、どうもお疲れ様でした。

ところでdjb先生と落合じゃない方のノビーの前で これとか これをみんなで音読しながら囲う会の開催はしないんですかね?

まあええわIPv6は必要だったし普及にも成功した現在、NGNの例のアレとかISPのAAAAフィルタなんてものは過去の一体あれは何だったんだ案件でテレホマンと同じインターネット老人会とかいう寒いネタに添えられるツマとして生き残るだけだろう。

しかしまだv4でしか通信できないケースも地雷のように残ってる、例を挙げればワイの環境だとVMware NetworkのNAT越えとかドコモ版iPhone 5sでのテザリングがどっちも対応しましたといいながらバグってんのかまともに動かへんのやな、もうどっちも捨ててえなあ… *1。 つってもインターネットブラウザとかならどうせv4にフォールバックするからタイムアウト待ち分の遅延が無視できりゃ問題ないし、なんならv4優先する設定にすりゃいいだけだ。

そんでもntpd(8)はアカンのだ、DNSがAAAAレコード返してきた場合はv6を優先するのだがv4にはフォールバックしてくれんようで

     remote           refid      st t when poll reach   delay   offset  jitter
==============================================================================
 ntp-a3.nict.go. .INIT.          16 u    -   64    0    0.000    0.000   0.000

とstratumが16(接続失敗)のまま時刻同期できなくなってしまうのだ、よって回避策が必要になる。

それはとても消極的な方法ではあるけれど、ntp.confの中で

server -4 ntp.nict.jp

と-4を指定しプロトコルバージョンを強制すりゃええだけではある。

ちなみにntpdate(1)の方は古い4.8.4あたりの実装だと

# ntpdate ntp.nict.jp

を実行した時に

 9 Jun 21:09:59 ntpdate[334]: sendto(ntp-a3.nict.go.jp): Undefined error: 0
transmit(2001:df0:232:eea0::fff4)
...
2001:df0:232:eea0::fff4: Server dropped: no data
...
 9 Jun 21:10:03 ntpdate[334]: no server suitable for synchronization found

と、こちらもv6からv4にはフォールバックせずにエラーになる(最新の安定板の4.8.12はだいじょうぶ)。

そんでこっからが本題のほんとしょーも無い話。

実はntp.confを参照するのはntpd(8)だけではない、NなんとかBSDは/etc/rc.confで

ntpdate=YES

が指定されている場合、起動時に/etc/rc.d/ntpdateというスクリプトが実行され、ntp.confに指定されてる一番最初のserver or peerに対してntpdate(1)を実行するという機能がある。 ユースケースとしては

というパターンですかね。

ところが実際にコードをみれば一目瞭然なんだけど、ntp.conf中の-4や-6オプションはこのスクリプトは読み飛ばすだけでntpdate(1)には教えてくれないのだ。

18 ntpdate_start()
19 {
20         if [ -z "$ntpdate_hosts" ]; then
21                 ntpdate_hosts=$(awk '
22                         /^#/                            { next }
23                         /^(server|peer)[ \t]*127.127/   { next }
24                         /^(server|peer)/                { if ($2 ~ /^-[46]/)
25                                                             print $3
26                                                           else
27                                                             print $2 }
28                 ' </etc/ntp.conf)
29         fi
30         if [ -n "$ntpdate_hosts"  ]; then
31                 echo "Setting date via ntp."
32                 $command $rc_flags $ntpdate_hosts
33         fi
34 }

よってrc.conf(あるいはrc.conf.d/ntpdateの中)で

ntpdate_flags="-4 -b -s"

と改めて-4オプションを指定するというクソダサなkludge入れる必要がある、まぁN5以降は4.8.12なのでv4にフォールバックするからもはやどうでもいい気がしないでもないが。

むしろこっちの方が問題かな、NTPサーバが負荷分散にDNSラウンドロビンしてるような場合

server ntp.nict.jp
server ntp.nict.jp
server ntp.nict.jp
server ntp.nict.jp

と同じDNS名を複数回書くなんて記事を書く人がようけおるのだが、これは今時のntp.confでは

pool ntp.nict.jp

と書けるんですわ。

あっ(察し)、お気づきでしょうさっきの/etc/rc.d/ntpdateのスクリプトはntp.conf中のserverかpeerかしかキーワードをひっかけてないので、ナウいpoolを使うとこいつを認識せず時刻同期が実行されないのだ、これN6どころかHEADでもそのまんまなんだな…ダサ過ぎる。

スクリプトの修正はたいした手間ではないけど、本当はこれntpdate(1)の方にntp.confを読むオプションつけて貰った方がいいよね案件な気がしないでもない。

[追記] オレオレN6は 雑に直した

*1:なおドコモの3G終了あるいはワイが死ぬかクックが改心してSIMフリーで実売2万前後のiPhone SEXを出すまではiPhone 5s使い続けるもよう、もうセキュリティとかどーでもいいや。

2019/6/11(Tue)

[音楽] Tycho/Weather

突然 06.11.19 JAPANなんてツイーヨするから来日公演の情報を見落としてたのかと軽くパニクって当日券探しに走ったのだが勘違いであった。

これは明日発売の新アルバム「 Weather」の一曲が「JAPAN」というらしい、お願いなのでポールマッカートニー取調係の菊池案件はやめてねグルーヴ。

前作で Ghostly Internationalの契約が切れたようで Ninja Tuneというインディペンデントに移籍したようだけどちゃんと日本盤も出るようで安心…

…ではなかった、これまでは PLANCHAから出てたけど今回からは BEATNIKに代わったのね、ここ呼び屋としてクッソやる気ねーとこやしこのアルバムのプロモーションは単独来日公演無しでいつものフジロック(笑)だけで終わりっぽくて悲しい、こういう映像も重要なバンドは野外より小屋で観たいのだけどねぇ(まぁ日本の小屋の映像設備だいぶアレやけど)。 音楽が売れねえって自分で首絞めてるだけなんやな。

そいやPLANCHAは1stアルバムの Past Is Prologueの再発したけどただの再プレスなのが残念、このアルバムは元々 Sunrise Projectorというタイトルだったものをレーベル移籍でタイトル変更と曲の入換やっとるのでそこで落ちた曲とか、 セルフリリースEPだった The Science Of Patternsを収録してくれたなら買い直したんだけどね…

あと傑作EPである Coastal BrakeもCD出してくれねえかな、まぁ Bleepでmp3でなくflacで買えるんだけどさ…

内容については前作 Epochの後、アルバム3部作ひとくぎりついたって発言あったけど、音楽性は相変わらず(歌モノも新機軸ってほどでなし)なので契約の話だったんだろうな。

そうそう個人的にはGhostlyから離れたことで Warp Records系の音楽配信サイトBleepでの扱いがなくなってしまうのでウンコmp3しか選択肢無くなるのがつらいねのよね、まぁ聴くだけならシングルやEPはアマゾンプライム会員特典になってるので試聴してどうぞ。

それと 自分とこのショップで過去作品関連のものが全部消えてしまったのも悲しい、欲しいパーカーあったんだよなー。

[追記] Bleepでもアルバムは取り扱うようで24bit flacのハイレゾ配信きてた、ワイはCD買うけどね24bitから16bitへのダウンコンバートめんどくせえし。

2019/6/18(Tue)

[オレオレN6] CESU-8 and Java Modified UTF-8 support for Citrus iconv

ふと こんなPRをみかけたので、関係ないけどCitrus iconvもCESU-8とかJava Modified UTF-8ってサポート無かったなと思い出したので 実装した

おそらくNなんとかBSDの古いCitrus iconv実装へのバックポートもたいした手間ではない気がするけどあっち今どうなってるか全く知らんし、何年か前に前に__STDC_ISO_10646__化するブランチをみた記憶があるので、libc/citrusの下が大きく変わっててバックポート難しい可能性もあるけど、知ったこっちゃないしサポートはいたしかねます。

コミットした時点ではMySQL utf8mb3もCESU-8と同じものと勘違いしてたのだが(Oracle買収による記憶改竄)、そもそもアレ基本面しかサポートしてないから違うわな。

2019/6/19(Wed)

[IoT] 監視カメラ欲しい

ちょいと認知症の老人対策に室内カメラの導入を考えてるんだが、数年前あれほどIoTとか大騒ぎしてた割に国内はロクな製品が無いのねぇ。 あの偉そうにロクロ回してた連中は今何してんのかね、ビッグデータ?それともブロックチェーン?

まぁ中華製の数千円クラスがAmazonあたりにあふれてるけど、どれもレビューが信用ならんし逆に選べないので困る。セキュリティガバガバそうだし。

つーか予算もそんなに無いので使ってない古いAndroidスマホを流用できるソリューションがあればありがたいんだけどね…ちょうどISW11MとかISW12HTはSDカードスロットもあるし。

要求仕様としては外からのリモートやアラートは不要、1~数日分を記録し古いものから上書き、保存先はSDカードあるいはNAS、WiFi対応でPCから接続してストリーミング再生できるあたりなんだけどね、これだと業務用の中古監視カメラ探した方がいいのかな。

というか金あったらCマウントとかレンズ沼の血が騒いで以下略

2019/6/27(Thu)

[近況] SWATTING

数年前に発症した認知症老人の被害妄想と異常行動がここ数か月突然悪化し手に負えない状態なんだが、ついに目を離した隙にドアの開け方を忘れ閉じ込められたと被害妄想爆発させベランダで大声で叫んで通報になり、救急隊と消防そして警察までやってくる事に。 いやーきついっす、もう不眠で体重10kg減ったしもう21g減量した方が楽かなーという気分。

2019/6/28(Fri)

[I18N] Citrus Johab encoding module's bug

なんやこれと思ったらオレオレN6で何年か前に修正した これの話で、正しい修正は これちゅーことだな。

まーmbrtowc/wcrtombだけテストしてiconvはテストしてなかった俺が悪いのだが、TNFもはやコードも読まずに指摘されたがまま正解も考えずに commitする輩しかおらんのやなー。

何年か前にFの誰か知らんがコード読まずに適当に警告消すだけの変更ぶちこんで セキュリティホール出す羽目になった時といい、よく判らないコードを判ろうとせずに触るという人ってほんと謎なんだよな。

2019/6/29(Sat)

[WWW] inoreaderとチラシの裏

どうもinoreaderはこのチラシの裏が嫌いなのかトラブル多い、ここんとこ 記事が更新されたタイミングで配信してる直近5日分記事がそのまま全部新着記事になるので重複しまくりである。 エスパーするとレスポンスヘッダのLast-Modifiedは正常に扱えてるけど、RSS中のpubDateの日付がパースエラーかなんかが原因で無視されてるあたりか。 今は

<pubDate>Fri, 28 Jun 2019 00:00:11 GMT</pubDate>

で返してるんだけど、とりあえずタイムゾーン名でなく時差で返すようにして様子みてみるか…

などと相手のせいにしていたら、ワイのボーンヘッドにより時刻の代わりに秒に記事番号埋めてたのが新着記事のたびに変わってしまっとるので、更新扱いとなり新着に上がってくるというオチであった。 ならfeedlyなんかの他のRSSリーダーは記事の重複チェックはpubDateだけでなく記事そのものでもやってて、これまで発覚しなかったってだけだな…

ちな 前回のトラブルは、nginxに追加したPOODLE対策がOpenSSLでなくNSS使ってるクライアントだとハマるってやつ。