ぷろじぇくと、みすじら。

ドメインに参加しているとMicrosoftアカウントで同期できない

Created at:

問題

Windows 10でドメインに参加しているデバイスでMicrosoft アカウントを登録してもアカウントの同期設定を有効にできない問題があります。その一方で以前からある環境は有効なままとなっていることもあります。

発生する環境

理由

設定とデータのローミングに関する FAQにあれこれ書いてありますが、要するにWindows 10 Creators Update以降ではいわゆる企業のアカウント(AD, Azure AD)とMicrosoftアカウントを結びつけることができなくなった影響です。

企業の所有の下で Azure AD に接続された Windows 10 デバイスの Microsoft アカウントは、今後ドメイン アカウントに接続できません。 Microsoft アカウントをドメイン アカウントに接続してそのユーザーの全データを Microsoft アカウントに同期させる機能 (Microsoft アカウントと Active Directory との接続による Microsoft アカウントでのローミング機能) は、Active Directory と Azure AD とが接続された環境に参加している Windows 10 デバイスからは削除されます。

Feedback Hubにもフィードバックが起票されていますが、企業アカウントの場合にはMSAではなくてAzure ADのEnterprise State Roamingを使ってくださいと。

In the Windows 10 Anniversary Update, domain joined users who connected their Microsoft Account (MSA) could roam settings and data between Windows devices. Many IT Pros told us that this functionality was not consistent with their policies for managing information owned by the organization. They did not want their PCs roaming to an individual’s personal cloud. A group policy to prevent users from connecting their MSAs did and does exist, but this setting also prevents users from easily accessing their personal Microsoft services. To address IT Pro concerns, we removed the ability for domain joined machines to roam with an MSA. Enterprises can still enable Enterprise State Roaming with Azure Active Directory.

ややこしいのが同期できている環境も場合によってはあるというところでしょうか。

どうやらCreators Update前にすでに同期していた環境であれば設定が引き継がれているためMicrosoftアカウントとの同期が可能なままということのようです。そのため新しくセットアップした場合に設定できずにハマります。

現時点で一応機能は残っているのでレジストリをいじると動かせるという話もありますが、今後を考えるとやめておいた方がよい気もします。

まとめ

つまり要するにどういうことなのかという話ですが…

確かに一貫性はありそうではありますが…うーん、今までできていたので不便ですね…。

まあとりあえずこれを踏まえて新しく設定するのであればアカウントの種類の使い分けとしては以下のような感じにするとよさそうです。

メモ: Enterprise State Roamingで同期するとテーマが同期されない

ところでAzure Active Directoryに参加したデバイスでEnterprise State Roamingを利用した同期を行った際、テーマの設定(背景、ロック画面、アクセントカラー)が同期されませんでした。ExplorerやEdge、タスクバーなどの設定は同期されているので謎です…。

ASP.NET CoreでLet's EncryptのChallengeのような拡張子なしのファイルを返す

Created at:

ASP.NET Coreの StaticFiles ミドルウェアは拡張子が既知のMIMEタイプに見つからない場合(不明なMIMEタイプ)、ファイルが見つからないものとして扱います。ところが場合によってこの挙動では困る場合があります。

例えばLet's Encryptは認証のために /.well-known/acme-challenge/Kk5osRC8Q2IKAVgreqBx6lCNgZ-nxzyKje8HJa611_c のようなパスにアクセスを試みますが、標準の挙動のままであればファイルがあっても404が返ってエラーとなります。わかりやすい組み合わせではAzure App Serviceで.NET CoreとLet's Encrypt Site Extensionを利用している場合に発生します。

解決方法1. StaticFiles のオプションで不明な種類も返せるようにする

UseStaticFiles メソッドには StaticFileOptions という StaticFiles ミドルウェアの設定を渡せますが、その際に不明な種類を返せるようにする ServeUnknownFileTypes プロパティがあるのでそれを true にすることで返せるようになります。

app.UseStaticFiles(new StaticFileOptions
{
    ServeUnknownFileTypes = true,
    DefaultContentType = "application/octet-stream"
});

ただしこの設定を行うと、アプリケーション全体に反映されてしまうので意図せずファイルが公開される可能性が増えるのでやらなくていいのであれば設定したくないものでもあります。

解決方法2. ファイルの種類を提供する IContentTypeProvider を実装する

StaticFiles ミドルウェアはファイルの種類を取得するためのプロバイダー IContentTypeProvider を受け取ることもできます。この IContentTypeProvider にはファイルパスも渡ってくるのでパスを見て判断できます。

例えばLet's Encryptが来るのは /.well-known/acme-challenge/ 以下と分かっているのでそこだけ知っているかのように処理をして、それ以外はデフォルトの FileExtensionContentTypeProvider や指定された IContentTypeProvider に任せます。

public class LetsEncryptWellKnownContentTypeProvider : IContentTypeProvider
{
    private IContentTypeProvider _baseProvider;

    public LetsEncryptWellKnownContentTypeProvider()
        : this(new FileExtensionContentTypeProvider())
    { }

    public LetsEncryptWellKnownContentTypeProvider(IContentTypeProvider baseProvider)
    {
        _baseProvider = baseProvider;
    }

    public bool TryGetContentType(string subpath, out string contentType)
    {
        if (subpath.StartsWith("/.well-known/acme-challenge/"))
        {
            contentType = "application/octet-stream";
            return true;
        }

        return _baseProvider.TryGetContentType(subpath, out contentType);
    }
}

IContentTypeProvider を実装したら、StaticFileOptionsContentTypeProvider プロパティにセットすれば完了です。

app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = new LetsEncryptWellKnownContentTypeProvider()
});

ASP.NET CoreのJSON設定をファイル以外から読み込む

Created at:

大抵の場合ではASP.NETのConfigurationBuilderでAddJsonFileを呼んでファイルパスを指定してJSONファイルを読み込みますが、もしかすると物理的なファイルではなく例えばStringから読み込みたいといった場合があるかもしれません。

そんな時は IFileProvider を実装することで対応できます。

例えば、AddJsonFileに指定されたパスをJSONそのものとして扱ってしまえというようなものも作れて、以下のようなコードで実現できます。

// Install-Package Microsoft.Extensions.Configuration
// Install-Package Microsoft.Extensions.Configuration.Json
// Install-Package Microsoft.Extensions.FileProviders.Abstractions
void Main()
{
    var configuration = new ConfigurationBuilder()
        .AddJsonFile(new VirtualFileProvider(), @"{
            ""Foo"": ""barbaz"",
            ""Hoge"": 1234
        }", false, false)
        .Build();

    configuration.GetChildren().Dump();
}

public class VirtualFileProvider : IFileProvider
{
    public IDirectoryContents GetDirectoryContents(string subpath)
    {
        return new NotFoundDirectoryContents();
    }

    public IFileInfo GetFileInfo(string subpath)
    {
        return new VirtualFileInfo("virtual.json", Encoding.UTF8.GetBytes(subpath));
    }

    public IChangeToken Watch(string filter)
    {
        return NullChangeToken.Singleton;
    }
}

public class VirtualFileInfo : IFileInfo
{
    private byte[] _bytes;

    public VirtualFileInfo(string path, byte[] bytes)
    {
        _bytes = bytes;
        PhysicalPath = path;
    }

    public bool Exists => true;

    public long Length => _bytes.Length;

    public string PhysicalPath { get; }

    public string Name => Path.GetFileName(PhysicalPath);

    public DateTimeOffset LastModified => DateTime.Now;

    public bool IsDirectory => false;

    public Stream CreateReadStream()
    {
        return new MemoryStream(_bytes);
    }
}

要するにファイルを表す IFileInfo を返す、IFileProvider を実装して AddJsonFile メソッドに渡すことでその IFileProvider を通して内容を読み込むことになります。

上記の例では指定されたパスをそのままbyte配列にして IFileInfo で Stream として返すというだけのものです(重要ではないプロパティは適当なものを返す)。

テストやちょっと試したいときなどにファイルを作れない/作りたくないといった場合、他のデータソースから取得するといったパターンが必要になったときには役立つかもしれません。

既存の.NET アセンブリにMono.Cecilでコードを差し込む

Created at:

すでにビルドされた.NETのアプリケーションやもしかするとUnityのゲームかもしれませんが、そういった一度作ってしまったアプリケーションに後からコードを差し込みたくなることがまれにあります。

例えばビルドしたアプリケーションをテストするために、後からコードを差し込んで診断用のコードを動かしたいといったパターンです。テスト用のコードをアプリケーションには含めておきたくない場合にはプラグインするかコードを差し込むかになります。

ここではMono.Cecilを利用してコードを後から差し込む方法を説明します。

Mono.Cecilとは

Mono.Cecilは.NETアセンブリを解析したり、書き換えたりするための便利なライブラリです。このライブラリを使うことで既存のコードを書き換えて新しいアセンブリを出力できます。

簡単な差し込み方

Mono.Cecilでコードを書き換えて既存のコードに自分のコードを差し込んでみます。

例えば以下のようなプログラム ConsoleApp1.exe があるとします。

using System;

namespace ConsoleApp1
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello Konnichiwa!");
        }
    }
}

そこでこの Console.WriteLine の呼び出しより前に処理を差し込みます。差し込みたい処理は以下のようなコードで別なアセンブリ(.dll)にしておきます。

using System;
namespace ClassLibrary1
{
    public class Class1
    {
        /// <summary>
        /// 差し込んだ側から呼び出してほしいメソッド
        /// </summary>
        public static void Initialize()
        {
            Console.WriteLine("差し込み案件です!");
        }
    }
}

そしてstaticなメソッドにしておくことで呼び出すためのILを最低限とでき、コードを差し込んだ側のスタックやローカル変数のつじつま合わせを考えなくてよくなるので単純になります。もちろん元のコードやオブジェクトに依存した処理を行いたいときは何らかの処理が必要となります。

ILの書き換えをする

というわけでILの書き換えをして新たに作ったアセンブリのメソッドを呼び出すように書き換えます。具体的には以下のようなコードとなります。

// ConsoleApp1.exe と ClassLibrary1.dll があるディレクトリ
var baseDir = @"Path\To\ConsoleAppDirectory";

// 対象となるアセンブリと型、メソッド名
var targetAssemblyName = "ConsoleApp1.exe";
var targetTypeName = "ConsoleApp1.Program";
var targetMethodName = "Main";

// 差し込みたいアセンブリと型、メソッド名
var injectAssemblyName = "ClassLibrary1.dll";
var injectTypeName = "ClassLibrary1.Class1";
var injectMethodName = "Initialize";

// 出力アセンブリ名
var outputAssembly = "ConsoleApp1_Injected.exe";

var assemblyResolver = new DefaultAssemblyResolver();
assemblyResolver.AddSearchDirectory(baseDir);

var asm = AssemblyDefinition.ReadAssembly(
    Path.Combine(baseDir, targetAssemblyName),
    new ReaderParameters
    {
        AssemblyResolver = assemblyResolver,
    }
);
var targetType = asm.MainModule.GetType(targetType);
var targetMethod = targetType.Methods.First(x => x.Name == targetMethodName);

var asmInject = AssemblyDefinition.ReadAssembly(Path.Combine(baseDir, injectAssemblyName));
var injectType = asmInject.MainModule.GetType(injectTypeName);
var injectMethod = injectType.Methods.First(x => x.IsStatic && x.Name == injectMethodName);
var injectMethodRef = targetMethod.Module.Import(injectMethod);

var ilProcessor = targetMethod.Body.GetILProcessor();
ilProcessor.InsertBefore(targetMethod.Body.Instructions[0], Instruction.Create(OpCodes.Call, injectMethodRef));

asm.Write(Path.Combine(baseDir, outputAssembly));

まず、AssemblyResolverを用意します。これはCecilでアセンブリを追加したりするときに正しく見つけるために必要となります。

var assemblyResolver = new DefaultAssemblyResolver();
assemblyResolver.AddSearchDirectory(baseDir);

次に差し込み先となるアセンブリを読み込んで、型とメソッド(差し込み先)を取得します。

// 差し込み先アセンブリを読む
var asm = AssemblyDefinition.ReadAssembly(
    Path.Combine(baseDir, targetAssembly),
    new ReaderParameters
    {
        AssemblyResolver = assemblyResolver,
    }
);
// 型を探してくる
var targetType = asm.MainModule.GetType(targetType);
// 差し込み先メソッドを探してくる
var targetMethod = targetType.Methods.First(x => x.Name == targetMethod);

差し込む先が見つかったら次は差し込みたい処理を読み込んで探します。

// 差し込みたいアセンブリを読み込む
var asmInject = AssemblyDefinition.ReadAssembly(Path.Combine(baseDir, injectAssemblyName));
// 差し込みたい処理の型を探す
var injectType = asmInject.MainModule.GetType(injectTypeName);
// 差し込みたいメソッドを探す
var injectMethod = injectType.Methods.First(x => x.IsStatic && x.Name == injectMethodName);

差し込み先に参照を追加し、その差し込み先内でメソッドの参照を得ます。これは差し込み先で差し込まれる側のアセンブリに対する参照情報を使うので必要です(MethodDefinitionは直接アセンブリをまたぐことはできない)。

var injectMethodRef = targetMethod.Module.Import(injectMethod);

そして呼び出すためのコードを差し込み先のメソッドに書き込みます。

// 差し込み先のメソッドのILを操作するILProcessorを取得
var ilProcessor = targetMethod.Body.GetILProcessor();
// 一番最初のIL Instructionの前に差し込み先メソッドを呼ぶ call opcode を差し込む
ilProcessor.InsertBefore(targetMethod.Body.Instructions[0], Instruction.Create(OpCodes.Call, injectMethodRef));

これが…

IL_0000:  nop
IL_0001:  ldstr      "Hello Konnichiwa!"
IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
IL_000b:  nop
IL_000c:  ret

こうじゃ。

IL_0000:  call       void [ClassLibrary1]ClassLibrary1.Class1::Initialize()
IL_0005:  nop
IL_0006:  ldstr      "Hello Konnichiwa!"
IL_000b:  call       void [mscorlib]System.Console::WriteLine(string)
IL_0010:  nop
IL_0011:  ret

最後に書き換えたアセンブリを書き出して終了です。

asm.Write(Path.Combine(baseDir, outputAssembly));

実行結果

これで書き換えたアセンブリ (ConsoleApp1_Injected.exe) を実行すると以下のような出力となります。

差し込み案件です!
Hello Konnichiwa!

まとめ

Mono.Cecilで簡単にアセンブリを書き換えられるのでビルドしてしまったアプリの魔改造がはかどります。普段、ビルド後のアセンブリを書き換えたい機会は多くないと思いますがいざというときに処理を差し込む一つの方法としては覚えておいて損はないのではないでしょうか。

サンプルコードは https://github.com/mayuki/Sample.MonoCecil に置いておきますのでどうぞご利用ください。

Google HomeからBRAVIA(Android TV)の音量を変更できない問題

Created at:

Google Homeでは「OK Google, (デバイス)の音量を何%にして」と呼びかけるとAndroid TVというかChromecastであれば音量設定できる、ということ機能があります。

ところがSONYのBRAVIA (Android TV)と外部サウンドシステム(サウンドバーなど)を組み合わせていると正常に動作しないようです。Nougatな機種ではわかりませんが、現状初代のMarshmallowな機種(X8500C)では挙動がおかしく正常に設定できません。

解決方法

いろいろ試した結果、この問題を解決する方法は2つありますが根本解決はできないようです(場合によってはどうにもならない…)。

一つはサウンドシステムの利用をやめてテレビからのオーディオ出力にする、ですがこれは現実的ではありません。

もう一つはサウンドシステムがHT-NT3のような単体でChromecastに対応しているのであれば、サウンドシステムの名前を「スピーカー」とすることで「スピーカーの音量を10%にして」ということで直接コントロールできます。

ちなみにHT-NT3を接続して操作した場合には音量10%の指定はテレビで音量10、サウンドバー側でVol 5相当になります。

まあしかし他にもちゃんとキャストされないことがあったり全体的に挙動が怪しいのできびしいですね…。SONYさん頑張ってください…。