標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

VC++で書いたアドオンDLLの中で独自ダイアログが表示できなかった件

とあるアプリのアドオンを VC++ で書いていたんだけれども、自作のダイアログを出そうとしてもなぜか DoModal が素通りしてしまう。

しかも、Visual Studio 自体もなぜか不安定で、クラスウィザードのクラッシュも多発して、原因の切り分けができない!!!

1日半ほど苦闘した結果、ようやく以下の情報に巡り合った

どうやら、EXE側からDllMainの第一引数で与えられているハンドルを DLL側で適切に設定しなくては、DLL側でリソースを使えないらしい (クラッシュの原因は別件なので、ここでは省く)

昨今、ウェブの情報も永遠ではないので、ミラーというかバックアップとして、こちらのソースも晒しておこう。

// DLL のモジュールハンドル
extern "C" HINSTANCE _hdllInstance = NULL;
HINSTANCE GetInstance()
{
    return _hdllInstance;
}

BOOL APIENTRY DllMain(
    HINSTANCE hInstance, 
    DWORD dwReason, 
    LPVOID pReserved)
{
    _hdllInstance = hInstance;
    ; // 中略
}

void example()
{
    const HINSTANCE hInstance = AfxGetResourceHandle();
    AfxSetResourceHandle(GetInstance());

    CSelectDwg dlg1;
    dlg1.DoModal();

    AfxSetResourceHandle(hInstance);
}

NYAGOS 4.4.2_1 を公開しました。

4.4.2_0(報告記事リンク)を出して間もないのですが、看過できない不具合がありましたので、修正版を公開しました。

修正点は以下の2点

  • diskfree: 行末の空白を削除

これは大した問題ではなく、余計な空白が多くて要らぬ改行をしてしまうので、空白を trim したというだけの話です。問題は次:

  • ~"\Program Files" の最初の引用符が消えて、Files が引数に含まれない不具合を修正

これは 4.4.2_0 の「~ユーザ名 の展開を実装」でエンバグしてしまったようです。~ の後にユーザ名がなかった時に元通りに戻さないといけないのですが、その際の二重引用符の扱いが不適切で、引用の始まり・終わりの対応の認識がおかしくなってしまいました。

このあたりのパーサーのソースは結構〈地獄〉になっているので、いずれちゃんと書き直したいところですが、今回のところは必要最小限の不具合修正で止めておきました。

以上、よろしくお願いいたします。

NYAGOS 4.4.2_0 を公開してしまいました

まだまだ修正する気満々だったのですが、リリースバイナリに未反映の修正がたまりすぎると不具合が発生した時、マスターと開発版をまた別管理しなくちゃいけなくなったります。それは避けたいので、4月になったし、いい加減リリースすることにしました。

さて、今回の修正は以下のとおりです。

OLEオブジェクトからLuaオブジェクトへの変換が日付型などでパニックを起こす不具合を修正

今回、GopherLua 向けの OLE用関数を zetamatta/glua-oleに分離したのですが、その際、まがりなりにも一通りの型を使えないとまずいだろうとやってる時に問題を確認しました。

Luaの数値が実数として OLE に渡されるべきだったのに、整数として渡されていた。

これは理由があって、trash.lua

local shellApp = nyagos.create_object("Shell.Application")
local trashBox = shellApp:NameSpace(10)

というコードがあるんですが、これ実数をそのまま渡すと〈ゴミ箱〉を表す定数「10」が「10.0」になって NameSpace メソッドに期待どおりの ID番号が渡りません。それで実数ではなく整数を使っていたのですが、これは Lua の仕様としてはアウトです。なので、まずは通常動作ではあくまで実数を使うようにしました。

Lua: 関数: nyagos.to_ole_integer(n) を追加

shellApp:NameSpace に整数を渡すために、同関数を作りました。この関数の戻り値は整数値をカプセル化した UserData です。OLE 関数側で中身の整数を開封します。

Lua: OLEObject に列挙用オブジェクトを得るメソッド _iter() を追加

zetamatta/glua-oleを作る時に FileSystemObject を使ってテストしようと思ったら、コレクションを列挙するメソッドがなくて、ファイルリストの表示すら出来ないことに気づきまして。

Lua: OLEObject を開放するメソッド OLEObject:_release() を追加

Excel でテストしている時、Excel のゾンビプロセスが残ることから気づきました。通常の lua53.dll だと __gc が呼ばれるのでそこで自動解放できるんですが、GopherLua では Go 言語の Garbage Collection にゆだねているので、リソース解放する場所がありません。それでやむをえず、このようなかっこ悪い方法で逃げました。

trash.lua が COM の解放漏れを起こしていた問題を修正

これは先の _release メソッドを呼ぶようにしただけです。これを呼ばなくても、特に実害はないようですが。

Lua: create_object生成された IUnkown インスタンスが解放されていなかった不具合を修正

これも Excel テスト中に確認しました。

~ユーザ名」の展開を実装

せっかく "os/user" パッケージがあって、ユーザのホームディレクトリが簡単に取得できるようになったので、使わなきゃ損ということで

バッチファイル以外の実行ファイルの exit status が表示されなくなっていた不具合を修正

昔の "os/exec" では、非0なプロセスが終了した時に得られる error は Error() で 「exit status %d」みたいな文字列になってたような気がするんですが、いつの間にか、そういうのなくってて、動作が変わってしまい…いや、勘違いかな?

%COMSPEC% が未定義の時に CMD.EXE を用いるエイリアス(ren,mklink,dir,...)が動かなくなっていた不具合を修正

まさか、COMSPEC が未定義な環境があるとは思いもしませんでした。

全角空白(%U+3000%)がパラメータの区切り文字と認識されていた点を修正

まさか unicode.IsSpace で全角空白が true になるとは…

(#359) -c,-k オプションで CMD.EXE のように複数の引数をとれるようにした

この辺から

存在しないディレクトリ\何か を補完しようとすると The system cannot find the path specified. と表示される不具合を修正

エラーも何も出ないのが正しい挙動です(でも、ビープ音くらいはあってもよかったかも)

(#360) 幅ゼロやサロゲートペアな Unicode<NNNNN> と表示するようにした

まさか、コマンドプロンプトサロゲートペアな Unicode が表示できないとは思いませんでした。

サロゲートペアな Unicode をそのまま出力するオプション --output-surrogate-pair を追加

今はダメですが、将来コマンドプロンプトサロゲートペアな Unicode が表示できるようになった時、それができるかを試験できるよう、今のうちにオプションを追加しておきました。

suコマンドで、ネットワークドライブが失なわれないようにした

現在のネットワークドライブをリストアップしてから

CMD.EXE /S /C ""net use Z: "\\vmware-host\Shared Folders" & … & cd "Z:版のカレントディレクトリ" & nyagos.exe "

みたいな呼出しを shellexecute 関数の verb=runas でやってます。

内蔵の mklink コマンドを実装し、CMD.exe /c mklinkエイリアス mklink を削除

念願のジャンクションの作り方が分かったので

(#197) ソースがディレクトリで -s がない時、ln はジャンクションを作成するようにした

mklinik /J ができるようになったので、ln の方も実装します。ln foo ~/ で済むことが、mklink だと mklink /J ~\foo .\foo と書かなくちゃいけないので。

ゼロバイトの Lua ファイルを削除(cdlnk.lua, open.lua, su.lua, swapstdfunc.lua )

0バイトのファイルを残していたのは、以前のバージョンのファイルが残っていた場合に機能しないように上書きするためなんですが、もうそろそろ残ってる環境もないだろうということで消しました。

UNCパスがカレントディレクトリでもバッチファイル・ren,assoc,dir,for を動くようにした

CMD.EXE ってカレントディレクトリが UNC パスだと、勝手に C:\Windows に chdir してしまうため、相対パスがかわって期待通りにコマンドが動かなくなってしまいます。そこで、直前にUNCパスのトップに空いているネットワークドライブを自動マウントして、操作対象のファイルとの相対パスを維持するようにしました。ドライブは実行後にアンマウントします。

(#363) nyagos.alias.COMMAND="string" 中では逆クォート置換が機能しない問題を修正

言われるまで気づきませんでした。

(#259) アプリケーションをダイアログで選んでファイルを開くコマンド select を実装

select book1.xlsx とかやりますと、こんなん出るんですわ
select コマンドイメージ

これ背景としては、わたくし Visual Studio が複数バージョン入った環境で仕事をしていて、プロジェクトによって起動させるべきバージョンが違うのですよね。で、GUI だと右クリックよりの「プログラムから開く」で起動バージョンが選べるので、コマンドラインからでもできるようにしました。

diskfree の出力フォーマットを修正

ディスクのファイルシステムと、ラベルも表示するようになりました。こんな感じです。
diskfreeイメージ

以上です

NYAGOS保守ノート:more が Ctrl-C で死んでしまう

more が Ctrl-C で nyagos ごと死んでしまうという不具合です。これは常に発生しているわけではなくて、nyagos の中から nyagos を呼んで、その中の more を実行した時に限られます。

つまり、more を実行中の nyagos を殺しているのは、親プロセスの nyagos なんですよね。shell/interpreter.gostartAndWaitProcess() で外部プロセスの終了を待っている時に Contextのキャンセルをキャッチすると、終了を待っている子プロセスを kill するようになってます。

これを子プロセス側が拒否するには、コンソールのモードで ENABLE_PROCESS_INPUT ビットを0にします(SetConsoleMode)。実際 readline はそれで無効になっています。ならば more も同様に0にすればよさそうなもんなんですが…そうするとエコーもオフになるんですよね。一応、ENABLE_ECHO_INPUT というビットもあって、こちらを1にすると ENABLE_PROCESS_INPUTが0でもエコーしてくれるんですが…こちらだと CR がそのまま解釈されて、Enterキー入力しても復帰はしても改行してくれない(LFがないから)。まぁ、実害はないんですが、これはユーザから不具合報告が来そうな挙動です。うーん、コンソールのモードを触るのは間違いなんかなー。

ちなみに os.Signal 方面での禁止は効かないようです。まぁ、言ってみれば SIGKILL で殺されているわけですからね。うーん、やはりアプローチがいけないのかなぁ。"os/exec".Cmd.Wait の内部実装を真似てやったんですが。見直しが必要かなぁ。

というわけで、現在頓挫中です…はい

Windows でジャンクション作れない・リベンジ編

「DeviceIoControl用の構造体」を作るのに、Microsoft ご本家の Go 言語パッケージ Microsoft/go-winio: Win32 IO-related utilities for Goを使ったら、うまく動作するようになった!やったね、たえちゃん、nyagosの機能が増えるよ

junction_run.go

package main

import (
    "errors"
    "os"

    "golang.org/x/sys/windows"
    "golang.org/x/xerrors"

    "github.com/Microsoft/go-winio"
)

const (
    _FSCTL_SET_REPARSE_POINT          = 589988
    _INVALID_HANDLE_VALUE = ^windows.Handle(0)
)


func MountPointCreate(mountPointPath, target string) error {
    _mountPointPath, err := windows.UTF16FromString(mountPointPath)
    if err != nil {
        return xerrors.Errorf("UTF16FromString(%s): %v", mountPointPath, err)
    }

    err = windows.CreateDirectory(&_mountPointPath[0], nil)
    if err != nil {
        return xerrors.Errorf("windows.CreateDirectory(%s): %v", mountPointPath, err)
    }
    ok := false
    defer func() {
        if !ok {
            os.Remove(mountPointPath)
        }
    }()

    handle, err := windows.CreateFile(&_mountPointPath[0],
        windows.GENERIC_WRITE,
        0,
        nil,
        windows.OPEN_EXISTING,
        windows.FILE_FLAG_BACKUP_SEMANTICS,
        0)
    if err != nil {
        return xerrors.Errorf("windows.CreateFile(%s): %v", mountPointPath, err)
    }
    if handle == _INVALID_HANDLE_VALUE {
        return errors.New("windows.CreateFile: invalid handle value")
    }
    defer windows.CloseHandle(handle)

    rp := winio.ReparsePoint{
        Target:       target,
        IsMountPoint: true,
    }

    data := winio.EncodeReparsePoint(&rp)

    var size uint32

    err = windows.DeviceIoControl(
        handle,
        _FSCTL_SET_REPARSE_POINT,
        &data[0],
        uint32(len(data)),
        nil,
        0,
        &size,
        nil)

    if err != nil {
        return xerrors.Errorf("windows.DeviceIoControl: %v", err)
    }
    ok = true
    return nil
}

func main() {
    if len(os.Args) < 3 {
        println("go run junction.go DST SRC")
        return
    }
    if err := MountPointCreate(os.Args[1], os.Args[2]); err != nil {
        println(err.Error())
        os.Exit(1)
    }
}
$ go run junction_run.go Hoge c:\tmp
$ ls -ld Hoge\
-rw---- 0 Mar 24 20:06:42 Hoge@ -> c:\tmp
$ cd Hoge\
$

Windows でジャンクション作れない

【次回】 | 【完成品】

junction_go.run:

package main

import (
    "os"
    "unsafe"
    "errors"

    "golang.org/x/sys/windows"
    "golang.org/x/xerrors"
)

const (
    _MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16384
    _FSCTL_SET_REPARSE_POINT          = 589988
    _INVALID_HANDLE_VALUE = ^windows.Handle(0)
)

type _MountPointInfo struct {
    Tag                   uint32
    DataLength            uint16
    Reserved              uint16
    TargetOffset          uint16
    TargetByteLength      uint16
    DescriptionOffset     uint16
    DescriptionByteLength uint16
    Buffer                [(_MAXIMUM_REPARSE_DATA_BUFFER_SIZE - 4 - 2*6) / 2]uint16
}

func MountPointCreate(mountPointPath, target string) error {
    _mountPointPath, err := windows.UTF16FromString(mountPointPath)
    if err != nil {
        return xerrors.Errorf("UTF16FromString(%s): %v", mountPointPath, err)
    }
    _target, err := windows.UTF16FromString(target)
    if err != nil {
        return xerrors.Errorf("UTF16FromString(%s): %v", target, err)
    }
    var info _MountPointInfo

    info.Tag = windows.IO_REPARSE_TAG_MOUNT_POINT

    info.TargetOffset = 0
    info.TargetByteLength = uint16(len(_target) * 2 )
    for i := 0 ; i < len(_target) ; i++ {
        info.Buffer[i] = _target[i]
    }
    // copy(info.Buffer[:], _target)
    info.Buffer[len(_target)] = 0

    info.DescriptionOffset = uint16((len(_target)+1)  * 2)
    info.DescriptionByteLength = 0

    info.DataLength = uint16(8 + (len(_target)+1+1)*2)

    err = windows.CreateDirectory(&_mountPointPath[0], nil)
    if err != nil {
        return xerrors.Errorf("windows.CreateDirectory(%s): %v", mountPointPath, err)
    }

    handle, err := windows.CreateFile(&_mountPointPath[0],
        windows.GENERIC_WRITE,
        0,
        nil,
        windows.OPEN_EXISTING,
        windows.FILE_FLAG_BACKUP_SEMANTICS,
        0)
    if err != nil {
        return xerrors.Errorf("windows.CreateFile(%s): %v", mountPointPath, err)
    }
    if handle == _INVALID_HANDLE_VALUE {
        return errors.New("windows.CreateFile: invalid handle value")
    }
    defer windows.CloseHandle(handle)

    var size uint32

    err = windows.DeviceIoControl(
        handle,
        _FSCTL_SET_REPARSE_POINT,
        (*byte)(unsafe.Pointer(&info)),
        uint32(8+info.DataLength),
        nil,
        0,
        &size,
        nil)

    if err != nil {
        return xerrors.Errorf("windows.DeviceIoControl: %v", err)
    }
    return nil
}

func main() {
    if len(os.Args) < 3 {
        println("go run junction.go DST SRC")
        return
    }
    if err := MountPointCreate(os.Args[1], os.Args[2]); err != nil {
        println(err.Error())
        os.Exit(1)
    }
}

これ、動作するけど、ジャンクションにならなんだよな…

$ go run junction_run.go Hoge c:\tmp
$ cd Hoge\
chdir Hoge\: The filename, directory name, or volume label syntax is incorrect.
exit status 1
$ ls -l Hoge\
-rw---- 0 Mar 24 17:43:26 Hoge@ -> c:\tmp
$

これ、バッファのサイズとか、ちょっとでも触るとAPIがエラーを返してくる。で、現在のソースだと実行は成功するんだけど、できたディレクトリはジャンクションとして機能しない。リパースポイント先は記録されているのに(nyagos の ls で表示できるから)

うーん、なんでだろ(というか、この記事は事実上、あきらめの儀式である)

参考リンク

追記

実は、2時間後に解決したのだった…

appveyer の Go が 1.12 になってくれない

もうパッケージの多くが、Go 1.12 名指しになってるのに!

go.mod を騙し騙しで調整し、Go 1.12 で追加された関数の使用を我慢しつづけてきたが、さすがにもう待てん!

というわけで、Go 1.12 をビルド時にダウンロードさせるようにした(→ appveyer.yml: download Go 1.12 · zetamatta/nyagos@3c2dddb)。Go 1.10 の時にもやったしね(あの時は strings.Builder を使いたかったからだったかな)

変更したのは appveyor.yml のインストール記述の先頭の以下の行の追加だけ!

install:
 - rd C:\Go /s /q
 - appveyor DownloadFile https://dl.google.com/go/go1.12.windows-386.zip
 - 7z x go1.12.windows-386.zip -oC:\ >nul

以上、開発現場からでした。